Added
Dockerfile(multi-stage, non-root, stdio entrypoint) and.dockerignore,
so registries that build and introspect the server in a container no longer
have to guess a build.- Multi-arch container images (amd64/arm64) at
ghcr.io/ni-c/hetzner-dns-mcp, published from CI with an SBOM and
max-mode build provenance and scanned with Trivy on every push and pull
request.server.jsonlists the image as an OCI package, so the MCP
registry offers it alongside the npm package. HETZNER_READ_ONLY=trueregisters only the seven read-only tools. The write
tools are not registered at all rather than rejected when called, so there is
no code path from a write request to the API.- Documentation site at hetzner-dns-mcp.ni-c.de
with a guide, the full tool reference and the security model.
Changed
- A missing
HETZNER_API_TOKENno longer exits at startup. The server
completes the MCP handshake and lists its tools without credentials; the
token is required when a tool actually calls the API, which then fails with
the same setup instructions as before. Base URL validation still exits,
since a bad base URL can leak the token. - Breaking: the
confirmboolean is gone. Destructive tools now take an
optionalconfirmTokeninstead — see below. Callers that passed
confirm: truewill be refused and handed a token to call again with. engines.noderaised to>=22; Node 20 is end-of-life and is no longer in
the CI matrix. The container has been on Node 24 all along.- Published source maps embed their sources, since only
dist/is shipped and
the maps previously pointed at asrc/that is not in the tarball.
Security
-
Destructive tools require a server-issued confirmation token. Every
irreversible tool refuses its first call and returns a random, single-use
token with a five-minute lifetime; a second call must repeat the identical
arguments and pass it. Forset_records,remove_records,
import_zonefileandchange_primary_nameserversthe token is bound to a
SHA-256 fingerprint of the payload, so a confirmation for one record list
cannot write a different one.The previous
confirmboolean was a value the model set itself, while the
refusal messages pasted the current RRSet contents back as raw API JSON.
Together that was a self-approving loop: an instruction hidden in a TXT
record value or a zone-file comment arrived verbatim in the very message
asking for confirmation. A token cannot be produced that way, because it
only ever exists in a previous result from this server. -
Removing protection now counts as destructive.
change_zone_protectionand
change_rrset_protectionhad no guard at all, so unprotecting a zone and
deleting it was two uninterrupted calls; disabling protection is now gated
exactly like the deletion it enables, while enabling it stays immediate. -
Confirmation messages no longer quote anything read back from the API. They
report record counts and TTLs only. -
API responses are wrapped in an
<untrusted-data>envelope, keys matching
tsig_key,token,secret,passwordorcredentialare redacted,
single values are truncated at 4 000 characters and whole results at
200 000. -
Upstream error bodies are truncated at 2 000 characters and HTML error pages
— a reverse proxy or WAF in front of the API — are dropped entirely instead
of being pasted into the model's context. -
HETZNER_API_TOKENandHETZNER_API_BASE_URLare deleted from the
environment once read, so a later crash report or diagnostic dump cannot
expose them. An unparseable base URL is no longer echoed back, since it can
contain auser:token@part. -
mcp-publisheris pinned to a release and verified against its SHA-256
before it runs. It was fetched from/releases/latestunverified, in a job
holdingid-token: write. -
The runtime image no longer ships npm, npx or corepack; they are never
invoked there, but their vendored dependencies kept appearing in scans. -
CI additionally runs CodeQL and a Trivy scan of the image for both
architectures.