# Building an MCP server for a Swiss hosting provider (and what reverse-engineering its manager taught me)

> Source: <https://dev.to/mogacode/building-an-mcp-server-for-a-swiss-hosting-provider-and-what-reverse-engineering-its-manager-2fg9>
> Published: 2026-05-21 16:46:58+00:00

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 `infomaniak-mcp-agent`

, runs locally over stdio. This post walks through what I learned, what's surprisingly hard, and what I'd do differently.

Repo: [https://github.com/Mogacode-ma/infomaniak-mcp-agent](https://github.com/Mogacode-ma/infomaniak-mcp-agent)

## Why an MCP for Infomaniak specifically

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.

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: *"audit all my DNS zones for missing DNSSEC, list every domain whose certificate expires in the next 30 days, and create a redirect from www.legacy-site.be to legacy-site.be on the production hosting."* That's three API calls minimum, and the cognitive overhead of remembering the right endpoint each time is the friction I wanted to remove.

MCP is the right shape for this:

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

## The architecture in one paragraph

A single Node 18+ binary, ESM, stdio transport. 54 tools registered with the MCP SDK, each backed by a thin function calling `api.infomaniak.com`

(Bearer token) or `manager.infomaniak.com/proxy/...`

(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.

Install: `npx -y infomaniak-mcp-agent`

. Config: one env var (`INFOMANIAK_API_TOKEN`

), generated at [https://manager.infomaniak.com/v3/api-token](https://manager.infomaniak.com/v3/api-token).

## The first surprise: the public API is missing half of what the manager does

I started with the public Infomaniak API. Documented at [https://developer.infomaniak.com](https://developer.infomaniak.com), neat OpenAPI-ish spec, Bearer auth. Within a week I'd wrapped most read operations: list sites, list databases, list domains, list mailboxes.

Then I tried to **create a site**. The public POST endpoint returned 200 OK with a site ID. The site never appeared in the manager. No error. Just... nothing.

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 `manager.infomaniak.com/proxy/<int>/v3/api/proxypass_2/1/...`

with a different payload shape, and with two cookies (`SASESSION`

+ `MANAGER-XSRF-TOKEN`

) instead of Bearer auth.

The "public API" silently ignores the operation. The "manager-private API" actually creates the site.

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

This is documented honestly in the repo's [REVERSE-ENGINEERING.md](https://github.com/Mogacode-ma/infomaniak-mcp-agent/blob/main/REVERSE-ENGINEERING.md). The cookie extraction is done with `chrome-cookies-secure`

in memory only — nothing is written to disk.

## The second surprise: Infomaniak's rate limit is shared per token

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.

I implemented a token-bucket in `src/throttle/`

:

```
class TokenBucket {
  private tokens: number;
  private readonly capacity: number;
  private readonly refillPerMs: number;
  private lastRefill = Date.now();

  constructor(capacityPerMinute: number) {
    this.capacity = capacityPerMinute;
    this.tokens = capacityPerMinute;
    this.refillPerMs = capacityPerMinute / 60_000;
  }

  async acquire(): Promise<void> {
    while (this.tokens < 1) {
      this.refill();
      if (this.tokens < 1) await sleep(50);
    }
    this.tokens -= 1;
  }

  private refill(): void {
    const now = Date.now();
    this.tokens = Math.min(this.capacity, this.tokens + (now - this.lastRefill) * this.refillPerMs);
    this.lastRefill = now;
  }
}
```

Wrapped around every HTTP call. Workflows like `audit_dns_zones`

now run reliably across 50+ domains, just slower (1 second per call instead of 100 ms — but they finish).

## The third surprise: destructive operations need a confirmation dance

Claude is enthusiastic. Give it a tool called `delete_site`

and a thread of context saying "let's clean up old test sites", and it will happily delete production.

The MCP spec has tool annotations (`destructiveHint`

, `idempotentHint`

) but they're hints — they don't enforce anything. I added a `requireConfirmation`

wrapper:

```
// First call: returns a confirmation token, no destructive action yet.
delete_site({ host_id: 12345 })
// → { confirmation_token: "abc...", expires_in_seconds: 60, "what_will_happen": "Site 'legacy-corp.be' (123 files, 2 databases) will be deleted." }

// Second call (within 60s): actually deletes.
delete_site({ host_id: 12345, confirmation: "abc..." })
// → { deleted: true, host_id: 12345 }
```

The first call describes what's going to happen and **returns**. 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.

This pattern saved me from production accidents twice already during dogfooding.

## The fourth surprise: MCP JSON Schema strictness varies across clients

`zod-to-json-schema`

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 `exclusiveMinimum: true`

(Draft 4 syntax) — they want `exclusiveMinimum: <number>`

(Draft 6+).

A community contributor (@ruffzy) sent a PR fixing this by targeting `jsonSchema7`

explicitly in `zodToJsonSchema`

config. I merged it and shipped 0.8.2 within a day. Open source working as intended.

## What's hard about a hosting-provider MCP that isn't obvious**Idempotency is the LLM's responsibility, but the tool author has to surface enough information**. The`list_hostings`

tool returns`is_locked: bool`

— if I hid that, the LLM would happily try operations on locked hostings and fail. Verbose output is fine; surprise failures aren't.**Pagination has to be invisible**. 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.**Error shapes must be normalized**. Infomaniak's public API returns`{error: {code, description}}`

. The manager-private API returns either that or`{"errors": [{"code", "description"}]}`

or raw HTML on auth failure. I wrote`InfomaniakError`

to flatten everything into a consistent`{kind, code, message, raw}`

so tools can handle errors uniformly.**Logs go to stderr, not stdout**. stdio transport mixes JSON-RPC and arbitrary writes on stdout, so any`console.log`

corrupts the protocol. I use`pino`

with stderr destination. If you build an MCP server, do this from day one.. tsup config:`npx -y`

requires`bin`

field + shebang in your built JS

```
banner: { js: "#!/usr/bin/env node" }
```

And in `package.json`

:

```
"bin": { "infomaniak-mcp-agent": "dist/server.js" }
```

Missing either and `npx -y`

either fails silently or runs the wrong entry point.

## What I'd do differently next time

-**Cookie-based manager auth is a maintenance debt**. 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. -**Reverse-engineering needs a version pinning strategy**. 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. -**Start with tests, not tools**. I built the tools first and added tests later. Inverted, I'd have caught the rate-limit issue 3 weeks earlier. -** Make the README the install path**. 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.

## Try it

```
npx -y infomaniak-mcp-agent
```

You'll need an Infomaniak API token ([https://manager.infomaniak.com/v3/api-token](https://manager.infomaniak.com/v3/api-token)) and to wire it into your MCP client. Full Claude Desktop / Claude Code config snippets in the [repo README](https://github.com/Mogacode-ma/infomaniak-mcp-agent#install).

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.

If you're building an MCP server for *your* 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.

⭐ if it saved you time. PRs welcome.
