<?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>Forem: Patrick Rary</title>
    <description>The latest articles on Forem by Patrick Rary (@mogacode).</description>
    <link>https://forem.com/mogacode</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3944426%2F37860d36-879f-4f1b-9943-a4e4827a8519.png</url>
      <title>Forem: Patrick Rary</title>
      <link>https://forem.com/mogacode</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/mogacode"/>
    <language>en</language>
    <item>
      <title>7 bugs I caught in my MCP server before publishing (and why I almost shipped a data-corruption disaster)</title>
      <dc:creator>Patrick Rary</dc:creator>
      <pubDate>Fri, 22 May 2026 04:59:39 +0000</pubDate>
      <link>https://forem.com/mogacode/7-bugs-i-caught-in-my-mcp-server-before-publishing-and-why-i-almost-shipped-a-data-corruption-5dfd</link>
      <guid>https://forem.com/mogacode/7-bugs-i-caught-in-my-mcp-server-before-publishing-and-why-i-almost-shipped-a-data-corruption-5dfd</guid>
      <description>&lt;p&gt;I shipped &lt;code&gt;elementor-mcp-agent&lt;/code&gt; v1.0 today — an open-source &lt;a href="https://modelcontextprotocol.io" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt; server that lets Claude (and any MCP client) drive WordPress Elementor across many client sites. It's MIT, on npm as &lt;code&gt;elementor-mcp-agent&lt;/code&gt;, listed in the official MCP Registry.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/Mogacode-ma/elementor-mcp-agent" rel="noopener noreferrer"&gt;github.com/Mogacode-ma/elementor-mcp-agent&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What I want to write about isn't the architecture — it's the &lt;strong&gt;7 bugs I caught only because I forced myself to test end-to-end against a live WordPress install before publishing&lt;/strong&gt;. Three of them would have silently corrupted data on a client site in production. The others would have just looked broken. All of them would have been a customer-support nightmare.&lt;/p&gt;

&lt;p&gt;This post is the bug list, with context. If you're building an MCP server (or any automation against an opinionated SaaS), these patterns apply.&lt;/p&gt;




&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;I run a small agency. We manage ~50 WordPress + Elementor Pro sites for clients across Belgium, France, Luxembourg and Morocco. Day-to-day operations are predictable: text updates, image swaps, template synchronization, plugin updates, backups. Cumulatively painful.&lt;/p&gt;

&lt;p&gt;MCP felt like the right shape for it: type-checked tools, explicit side effects, confirmation dance for destructive ops. I built 24 tools across 7 categories (sites, pages, widgets, templates, WP-CLI, screenshots, fleet versions).&lt;/p&gt;

&lt;p&gt;My v0.1 passed &lt;code&gt;npm run build&lt;/code&gt;, &lt;code&gt;npm test&lt;/code&gt;, ESLint, &lt;code&gt;tsc --noEmit&lt;/code&gt;. I almost published it without touching a real Elementor install.&lt;/p&gt;

&lt;p&gt;I'm glad I didn't.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bug #1 — WordPress REST API silently drops writes to unregistered postmeta keys
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The symptom&lt;/strong&gt;: I implemented automated backups by writing the current &lt;code&gt;_elementor_data&lt;/code&gt; to a timestamped postmeta key (&lt;code&gt;_elementor_data_backup_2026-05-22T04-35-37-847Z&lt;/code&gt;) via &lt;code&gt;PUT /wp-json/wp/v2/pages/{id}&lt;/code&gt;. The API returned 200 OK. My code marked the backup as successful. The subsequent &lt;code&gt;elementor_find_replace&lt;/code&gt; applied with confidence.&lt;/p&gt;

&lt;p&gt;When I tried to verify via &lt;code&gt;wp post meta list 8 | grep backup&lt;/code&gt;, the result was empty.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The root cause&lt;/strong&gt;: WordPress REST API requires &lt;code&gt;register_meta()&lt;/code&gt; with &lt;code&gt;show_in_rest: true&lt;/code&gt; for a postmeta key to be writable through REST. Plugins like Elementor register their canonical keys (&lt;code&gt;_elementor_data&lt;/code&gt;, &lt;code&gt;_elementor_page_settings&lt;/code&gt;). Custom keys you invent at runtime are &lt;strong&gt;silently dropped&lt;/strong&gt; by REST.&lt;/p&gt;

&lt;p&gt;200 OK. No warning. No error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;: switched backup to WP-CLI via SSH (always works). Local JSON file as fallback when SSH isn't configured. REST is no longer trusted for non-canonical writes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson&lt;/strong&gt;: any REST API that requires schema registration for meta-like fields will silently swallow your writes when the schema isn't there. Test that the value actually persists. Don't trust the response code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bug #2 — &lt;code&gt;wp&lt;/code&gt; binary not in non-interactive SSH PATH
&lt;/h2&gt;

&lt;p&gt;After fixing #1, I tried again. New error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/bin/bash: line 1: wp: &lt;span class="nb"&gt;command &lt;/span&gt;not found
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'd been testing in my local terminal where &lt;code&gt;wp&lt;/code&gt; was aliased. On managed WordPress hosts (Infomaniak, in this case), &lt;code&gt;wp-cli&lt;/code&gt; is usually installed at &lt;code&gt;~/bin/wp.phar&lt;/code&gt; and invoked as &lt;code&gt;php ~/bin/wp.phar&lt;/code&gt;. The non-interactive shell that &lt;code&gt;ssh user@host "command"&lt;/code&gt; uses has a stripped-down PATH that doesn't include &lt;code&gt;~/bin&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;: auto-detection at first SSH connection — probe &lt;code&gt;command -v wp&lt;/code&gt;, fall back to &lt;code&gt;~/bin/wp.phar&lt;/code&gt;, then &lt;code&gt;~/wp-cli.phar&lt;/code&gt;. Cache per-site. Allow explicit override via &lt;code&gt;ssh.wp_cli_path&lt;/code&gt; config field.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson&lt;/strong&gt;: non-interactive SSH sessions have a different PATH than interactive ones. If your tool spawns remote commands, never assume a binary is in PATH. Probe or accept an explicit path.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bug #3 — SSH banner pollutes stderr and corrupts output parsing
&lt;/h2&gt;

&lt;p&gt;OpenSSH recently started printing a warning on connections that don't use a post-quantum key exchange algorithm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;** WARNING: connection is not using a post-quantum key exchange algorithm.
** This session may be vulnerable to "store now, decrypt later" attacks.
** The server may need to be upgraded. See https://openssh.com/pq.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It goes to stderr. My code captured stderr and concatenated it into the error message. So a successful &lt;code&gt;wp post meta get&lt;/code&gt; call would still surface this warning as if it were an error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;: filter known-benign stderr lines after capture. Whitelist by substring (&lt;code&gt;post-quantum&lt;/code&gt;, &lt;code&gt;openssh.com/pq&lt;/code&gt;, &lt;code&gt;decrypt later&lt;/code&gt;, etc.) before deciding the operation failed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson&lt;/strong&gt;: stderr is not synonymous with "error." Tools that pipe stderr into error-handling paths need to filter benign banners.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bug #4 — "Default Kit" returned as a widget in template_type=widget filters
&lt;/h2&gt;

&lt;p&gt;The query &lt;code&gt;/wp-json/wp/v2/elementor_library?meta_key=_elementor_template_type&amp;amp;meta_value=widget&lt;/code&gt; was supposed to return the global widgets on a site. It returned the Default Kit (Elementor's site-wide design tokens) which has &lt;code&gt;_elementor_template_type=kit&lt;/code&gt;, not &lt;code&gt;widget&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The root cause&lt;/strong&gt;: WordPress REST &lt;code&gt;meta_value&lt;/code&gt; filtering is unreliable for meta keys that aren't &lt;code&gt;register_meta&lt;/code&gt;-declared. In practice, it falls back to a &lt;code&gt;meta_key&lt;/code&gt; presence check, ignoring the value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;: fetch all &lt;code&gt;elementor_library&lt;/code&gt; entries (no meta_value filter), then filter client-side on &lt;code&gt;meta._elementor_template_type === 'widget'&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson&lt;/strong&gt;: don't trust server-side filters on unregistered meta. Always validate the response structure matches your intent.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bug #5 — &lt;code&gt;_elementor_page_settings&lt;/code&gt; is an object via REST, a string via WP-CLI
&lt;/h2&gt;

&lt;p&gt;When I added &lt;code&gt;duplicate_elementor_page&lt;/code&gt;, the copy path did:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;read source → copy data + page_settings → write to new page via REST PUT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It failed with:&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="s2"&gt;"Le paramètre meta._elementor_page_settings n'est pas de type object."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cause: &lt;code&gt;GET /wp-json/wp/v2/pages/{id}?context=edit&lt;/code&gt; returns &lt;code&gt;meta._elementor_page_settings&lt;/code&gt; as a &lt;strong&gt;parsed object&lt;/strong&gt; (Elementor registers it that way). &lt;code&gt;wp post meta get&lt;/code&gt; returns it as a &lt;strong&gt;serialised JSON string&lt;/strong&gt;. My code typed it as &lt;code&gt;string&lt;/code&gt; everywhere, so when REST returned an object and I passed it back via REST PUT (expecting JSON string serialization in my templating), the API rejected the type mismatch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;: normalize on read. If you got a string, &lt;code&gt;JSON.parse()&lt;/code&gt; it before REST writes. If you got an object, &lt;code&gt;JSON.stringify()&lt;/code&gt; it before WP-CLI writes. Don't assume one transport's representation matches the other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson&lt;/strong&gt;: when two transports read/write the same field with different serialization rules, normalize at the boundary, not in the middle.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bug #6 — Headless Chrome cold-start exceeds 30s
&lt;/h2&gt;

&lt;p&gt;I built a &lt;code&gt;screenshot_page&lt;/code&gt; tool using &lt;code&gt;chrome --headless --screenshot=...&lt;/code&gt; via &lt;code&gt;child_process.spawn&lt;/code&gt;. Locally it was instant. On the first call after a long idle period, it timed out at 30s. Chrome was downloading codec libraries or whatever it does on first-launch in headless mode.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;: bump the default timeout from 30s to 60s. Document that this is a one-time cost per Chrome cold start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson&lt;/strong&gt;: cold-start latency on local tools is real and often invisible during dev. Set timeouts based on worst-case, not happy-path.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bug #7 — Templates listing had the same filter bug as globals (Bug #4)
&lt;/h2&gt;

&lt;p&gt;I'd written the templates listing code right after the globals code, and copy-pasted the same broken meta filter pattern. Caught it when I tested &lt;code&gt;list_elementor_templates&lt;/code&gt; with &lt;code&gt;type='widget'&lt;/code&gt; and got back the Default Kit again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;: same client-side filter pattern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson&lt;/strong&gt;: a bug in one place is a bug in N places where the same pattern is reused. Search the codebase for the smell before declaring the bug fixed.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd take from this for any MCP build
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;End-to-end test against a live target before publishing — always.&lt;/strong&gt; Unit tests + lint + typecheck told me v0.1 was "ready." End-to-end told me 7 things were broken.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Don't trust REST silent acceptance.&lt;/strong&gt; 200 OK means "I received your request," not "I persisted your change." Verify post-write that the value actually exists.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Filter at the boundary, not in the middle.&lt;/strong&gt; Type mismatches between transports (REST vs CLI, JSON vs YAML, etc.) are easier to handle at the read/write edge than to track through your domain code.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;stderr ≠ error.&lt;/strong&gt; Don't conflate them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;PATH is not what you think it is in non-interactive shells.&lt;/strong&gt; Probe or accept explicit paths.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cold starts are real.&lt;/strong&gt; Set timeouts based on worst case.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Update the skill / playbook that taught you the safe patterns.&lt;/strong&gt; I patched the upstream WordPress-Elementor skill on GitHub with the three REST quirks I'd surfaced, so the next person doesn't have to learn them the hard way (&lt;a href="https://github.com/jezweb/claude-skills/pull/92" rel="noopener noreferrer"&gt;PR #92 on jezweb/claude-skills&lt;/a&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;-y&lt;/span&gt; elementor-mcp-agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A WordPress site with Elementor installed&lt;/li&gt;
&lt;li&gt;An &lt;a href="https://wordpress.org/documentation/article/application-passwords/" rel="noopener noreferrer"&gt;Application Password&lt;/a&gt; for an admin user&lt;/li&gt;
&lt;li&gt;Optionally SSH access for WP-CLI tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Config snippet for Claude Desktop / Code in the &lt;a href="https://github.com/Mogacode-ma/elementor-mcp-agent#configure" rel="noopener noreferrer"&gt;README&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The MCP is on the official registry: search "elementor-mcp-agent" in your favourite MCP-aware client.&lt;/p&gt;

&lt;p&gt;If you hit a bug, open an issue with the exact tool call + response (sanitize tokens). I'll usually patch within a day. Battle-tested against an agency fleet — but every WordPress install is its own snowflake, so PRs and bug reports welcome.&lt;/p&gt;

&lt;p&gt;⭐ if it saved you time.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by &lt;a href="https://mogacode.ma" rel="noopener noreferrer"&gt;MogaCode&lt;/a&gt; in Essaouira.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>wordpress</category>
      <category>typescript</category>
      <category>claude</category>
    </item>
    <item>
      <title>Building an MCP server for a Swiss hosting provider (and what reverse-engineering its manager taught me)</title>
      <dc:creator>Patrick Rary</dc:creator>
      <pubDate>Thu, 21 May 2026 16:46:58 +0000</pubDate>
      <link>https://forem.com/mogacode/building-an-mcp-server-for-a-swiss-hosting-provider-and-what-reverse-engineering-its-manager-2fg9</link>
      <guid>https://forem.com/mogacode/building-an-mcp-server-for-a-swiss-hosting-provider-and-what-reverse-engineering-its-manager-2fg9</guid>
      <description>&lt;p&gt;I spent the last six weeks building an unofficial MCP server for Infomaniak — the Swiss hosting provider — that lets Claude (and any MCP client) drive web hosting, mail, kDrive, DNS, SSL certificates and AI tools from natural language. It's MIT, on npm as &lt;code&gt;infomaniak-mcp-agent&lt;/code&gt;, runs locally over stdio. This post walks through what I learned, what's surprisingly hard, and what I'd do differently.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/Mogacode-ma/infomaniak-mcp-agent" rel="noopener noreferrer"&gt;https://github.com/Mogacode-ma/infomaniak-mcp-agent&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Why an MCP for Infomaniak specifically
&lt;/h2&gt;

&lt;p&gt;I run 200+ websites for clients across Belgium, Luxembourg, France and Morocco. Most live on Infomaniak (managed cloud, shared hosting, mail, DNS). Day-to-day operations are: provision a new site, swap a DNS record, add a mailbox, request an SSL cert, audit which domains expire in the next 60 days.&lt;/p&gt;

&lt;p&gt;These tasks are all doable from the manager UI, all doable via the public API — but only one or two clicks/calls each, and they don't compose. Claude is good at composition: &lt;em&gt;"audit all my DNS zones for missing DNSSEC, list every domain whose certificate expires in the next 30 days, and create a redirect from &lt;code&gt;www.legacy-site.be&lt;/code&gt; to &lt;code&gt;legacy-site.be&lt;/code&gt; on the production hosting."&lt;/em&gt; That's three API calls minimum, and the cognitive overhead of remembering the right endpoint each time is the friction I wanted to remove.&lt;/p&gt;

&lt;p&gt;MCP is the right shape for this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tools are typed (Zod → JSON Schema → MCP)&lt;/li&gt;
&lt;li&gt;Side effects are explicit (idempotent? destructive? confirmation required?)&lt;/li&gt;
&lt;li&gt;The LLM doesn't need to know HTTP — it sees a catalogue of named operations&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The architecture in one paragraph
&lt;/h2&gt;

&lt;p&gt;A single Node 18+ binary, ESM, stdio transport. 54 tools registered with the MCP SDK, each backed by a thin function calling &lt;code&gt;api.infomaniak.com&lt;/code&gt; (Bearer token) or &lt;code&gt;manager.infomaniak.com/proxy/...&lt;/code&gt; (cookie-authenticated). A token-bucket throttles to 60 req/min (Infomaniak's hard cap). Confirmation tokens for destructive operations (TTL 60s by default). Per-tool tests, ESLint, Prettier, gitleaks, CodeQL, vitest with 35% coverage and climbing.&lt;/p&gt;

&lt;p&gt;Install: &lt;code&gt;npx -y infomaniak-mcp-agent&lt;/code&gt;. Config: one env var (&lt;code&gt;INFOMANIAK_API_TOKEN&lt;/code&gt;), generated at &lt;a href="https://manager.infomaniak.com/v3/api-token" rel="noopener noreferrer"&gt;https://manager.infomaniak.com/v3/api-token&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The first surprise: the public API is missing half of what the manager does
&lt;/h2&gt;

&lt;p&gt;I started with the public Infomaniak API. Documented at &lt;a href="https://developer.infomaniak.com" rel="noopener noreferrer"&gt;https://developer.infomaniak.com&lt;/a&gt;, neat OpenAPI-ish spec, Bearer auth. Within a week I'd wrapped most read operations: list sites, list databases, list domains, list mailboxes.&lt;/p&gt;

&lt;p&gt;Then I tried to &lt;strong&gt;create a site&lt;/strong&gt;. The public POST endpoint returned 200 OK with a site ID. The site never appeared in the manager. No error. Just... nothing.&lt;/p&gt;

&lt;p&gt;I diffed the network tab of the manager's "Create site" wizard against what I was sending. The manager wasn't calling the public API at all. It was calling &lt;code&gt;manager.infomaniak.com/proxy/&amp;lt;int&amp;gt;/v3/api/proxypass_2/1/...&lt;/code&gt; with a different payload shape, and with two cookies (&lt;code&gt;SASESSION&lt;/code&gt; + &lt;code&gt;MANAGER-XSRF-TOKEN&lt;/code&gt;) instead of Bearer auth.&lt;/p&gt;

&lt;p&gt;The "public API" silently ignores the operation. The "manager-private API" actually creates the site.&lt;/p&gt;

&lt;p&gt;The same pattern holds for: database creation, FTP/SSH user creation, mailbox creation, redirection creation, password rotation. The public API is &lt;strong&gt;read-mostly&lt;/strong&gt;. Real automation requires the manager-private surface.&lt;/p&gt;

&lt;p&gt;This is documented honestly in the repo's &lt;a href="https://github.com/Mogacode-ma/infomaniak-mcp-agent/blob/main/REVERSE-ENGINEERING.md" rel="noopener noreferrer"&gt;REVERSE-ENGINEERING.md&lt;/a&gt;. The cookie extraction is done with &lt;code&gt;chrome-cookies-secure&lt;/code&gt; in memory only — nothing is written to disk.&lt;/p&gt;




&lt;h2&gt;
  
  
  The second surprise: Infomaniak's rate limit is shared per token
&lt;/h2&gt;

&lt;p&gt;60 req/min sounds generous until you write a workflow that iterates over 50 domains and makes 3 calls each. You hit the limit in 30 seconds and Infomaniak starts returning 429 with a 60-second cool-off.&lt;/p&gt;

&lt;p&gt;I implemented a token-bucket in &lt;code&gt;src/throttle/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TokenBucket&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;refillPerMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;lastRefill&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;capacityPerMinute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;capacity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;capacityPerMinute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;capacityPerMinute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refillPerMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;capacityPerMinute&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;acquire&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;refill&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&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;await&lt;/span&gt; &lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;refill&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastRefill&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refillPerMs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastRefill&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wrapped around every HTTP call. Workflows like &lt;code&gt;audit_dns_zones&lt;/code&gt; now run reliably across 50+ domains, just slower (1 second per call instead of 100 ms — but they finish).&lt;/p&gt;




&lt;h2&gt;
  
  
  The third surprise: destructive operations need a confirmation dance
&lt;/h2&gt;

&lt;p&gt;Claude is enthusiastic. Give it a tool called &lt;code&gt;delete_site&lt;/code&gt; and a thread of context saying "let's clean up old test sites", and it will happily delete production.&lt;/p&gt;

&lt;p&gt;The MCP spec has tool annotations (&lt;code&gt;destructiveHint&lt;/code&gt;, &lt;code&gt;idempotentHint&lt;/code&gt;) but they're hints — they don't enforce anything. I added a &lt;code&gt;requireConfirmation&lt;/code&gt; wrapper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// First call: returns a confirmation token, no destructive action yet.&lt;/span&gt;
&lt;span class="nf"&gt;delete_site&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;host_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12345&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="c1"&gt;// → { confirmation_token: "abc...", expires_in_seconds: 60, "what_will_happen": "Site 'legacy-corp.be' (123 files, 2 databases) will be deleted." }&lt;/span&gt;

&lt;span class="c1"&gt;// Second call (within 60s): actually deletes.&lt;/span&gt;
&lt;span class="nf"&gt;delete_site&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;host_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12345&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;confirmation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;abc...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="c1"&gt;// → { deleted: true, host_id: 12345 }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first call describes what's going to happen and &lt;strong&gt;returns&lt;/strong&gt;. The LLM has to ask the human (or itself) "are you sure?" before the second call. The token expires after 60s. Multiple in-flight tokens per resource are allowed.&lt;/p&gt;

&lt;p&gt;This pattern saved me from production accidents twice already during dogfooding.&lt;/p&gt;




&lt;h2&gt;
  
  
  The fourth surprise: MCP JSON Schema strictness varies across clients
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;zod-to-json-schema&lt;/code&gt; produces JSON Schema Draft 7. Anthropic API and Claude Desktop are happy with that. The MCP Inspector tool? Stricter. Some clients use Draft 2020-12 and reject &lt;code&gt;exclusiveMinimum: true&lt;/code&gt; (Draft 4 syntax) — they want &lt;code&gt;exclusiveMinimum: &amp;lt;number&amp;gt;&lt;/code&gt; (Draft 6+).&lt;/p&gt;

&lt;p&gt;A community contributor (@ruffzy) sent a PR fixing this by targeting &lt;code&gt;jsonSchema7&lt;/code&gt; explicitly in &lt;code&gt;zodToJsonSchema&lt;/code&gt; config. I merged it and shipped 0.8.2 within a day. Open source working as intended.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's hard about a hosting-provider MCP that isn't obvious
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Idempotency is the LLM's responsibility, but the tool author has to surface enough information&lt;/strong&gt;. The &lt;code&gt;list_hostings&lt;/code&gt; tool returns &lt;code&gt;is_locked: bool&lt;/code&gt; — if I hid that, the LLM would happily try operations on locked hostings and fail. Verbose output is fine; surprise failures aren't.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pagination has to be invisible&lt;/strong&gt;. Some Infomaniak endpoints page at 25 items, others at 50. The MCP tool always pages through everything and returns the merged list. Letting the LLM do pagination = it forgets, gets the first page only, and reasons over incomplete data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Error shapes must be normalized&lt;/strong&gt;. Infomaniak's public API returns &lt;code&gt;{error: {code, description}}&lt;/code&gt;. The manager-private API returns either that or &lt;code&gt;{"errors": [{"code", "description"}]}&lt;/code&gt; or raw HTML on auth failure. I wrote &lt;code&gt;InfomaniakError&lt;/code&gt; to flatten everything into a consistent &lt;code&gt;{kind, code, message, raw}&lt;/code&gt; so tools can handle errors uniformly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Logs go to stderr, not stdout&lt;/strong&gt;. stdio transport mixes JSON-RPC and arbitrary writes on stdout, so any &lt;code&gt;console.log&lt;/code&gt; corrupts the protocol. I use &lt;code&gt;pino&lt;/code&gt; with stderr destination. If you build an MCP server, do this from day one.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;npx -y&lt;/code&gt; requires &lt;code&gt;bin&lt;/code&gt; field + shebang in your built JS&lt;/strong&gt;. tsup config:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;banner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;js&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#!/usr/bin/env node&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in &lt;code&gt;package.json&lt;/code&gt;:&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="nl"&gt;"bin"&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;"infomaniak-mcp-agent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist/server.js"&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;Missing either and &lt;code&gt;npx -y&lt;/code&gt; either fails silently or runs the wrong entry point.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd do differently next time
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cookie-based manager auth is a maintenance debt&lt;/strong&gt;. The session cookies expire every few hours. Users have to re-open the manager in Chrome to refresh them. A long-lived service account would be cleaner if Infomaniak ever ships one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reverse-engineering needs a version pinning strategy&lt;/strong&gt;. The manager-private endpoints change without notice. I'd add a smoke-test workflow that hits a known set of endpoints daily and opens an issue when something 404s.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start with tests, not tools&lt;/strong&gt;. I built the tools first and added tests later. Inverted, I'd have caught the rate-limit issue 3 weeks earlier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make the README the install path&lt;/strong&gt;. Anyone who lands on the npm page should be able to copy 3 lines and have it running in Claude Desktop. That's the win condition.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;-y&lt;/span&gt; infomaniak-mcp-agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll need an Infomaniak API token (&lt;a href="https://manager.infomaniak.com/v3/api-token" rel="noopener noreferrer"&gt;https://manager.infomaniak.com/v3/api-token&lt;/a&gt;) and to wire it into your MCP client. Full Claude Desktop / Claude Code config snippets in the &lt;a href="https://github.com/Mogacode-ma/infomaniak-mcp-agent#install" rel="noopener noreferrer"&gt;repo README&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you're on Infomaniak and you hit a bug, open an issue with the exact tool call + response (sanitize tokens). I'll usually patch within a day.&lt;/p&gt;

&lt;p&gt;If you're building an MCP server for &lt;em&gt;your&lt;/em&gt; niche provider, the patterns above (token bucket, confirmation dance, error normalization, stderr-only logging) are reusable. The repo is MIT, fork it as a starting point.&lt;/p&gt;

&lt;p&gt;⭐ if it saved you time. PRs welcome.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>claude</category>
      <category>typescript</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
