Skip to content

Releases: lazyants/hetzner-mcp-server

v2.5.0

Choose a tag to compare

@lazyants lazyants released this 20 Aug 19:41
31cd92c

Added

  • The publish workflow fails before npm publish when the GitHub Release tag
    and package.json version disagree. check-versions.mjs proved that
    package.json and server.json agreed with each other — it does not read
    package-lock.json in this repo — but nothing tied that version to the tag
    the release was cut from — so tagging
    v1.9.0 on a commit reading 2.0.0 would have published 2.0.0 to npm and the
    MCP Registry while the GitHub Release — the artifact humans read — claimed
    otherwise, silently and on the irreversible side of the publish. Ported from
    lexware-mcp-server 5.2.0 (lexware #103).

Changed

  • The publish job runs on Node 24 (Active LTS) instead of Node 20. npm's Trusted
    Publishing prerequisite is two-part — npm 11.5.1+ and Node 22.14+ — and
    the job satisfied only the npm half, leaving the irreversible npm publish
    step one npm patch away from breaking if that floor starts being enforced. The
    CI test matrix stays on Node 20 + 22: it tracks engines.node, which is
    unchanged. Ported from lexware-mcp-server 5.2.0 (lexware #102).
  • Raised the @modelcontextprotocol/sdk floor to ^1.30.0, which declares
    @hono/node-server: ^1.19.9 || ^2.0.5 where 1.29.0 declared only ^1.19.9.
    A correctness alignment, not a security fix: the resolved tree already carried
    a patched @hono/node-server, and the production audit gate was green before
    and after. What it buys is that a resolver cannot fall back to an SDK whose
    range predates the widening. Ported from lexware-mcp-server 5.2.0 (lexware
    #81).

Security

  • Refreshed the overrides floors. Three had drifted inside live advisory
    ranges and two packages that reach the production audit gate were not pinned
    at all — verified against the GitHub advisory API on 2026-08-20:

    Package Was Now Advisory range
    fast-uri ^3.1.2 ^3.1.5 GHSA-7p8r-x3mc-p8w7, >=3.0.0 <3.1.5
    brace-expansion ^5.0.6 ^5.0.9 GHSA-rgw5-rvv9-x895, >=4.0.0 <5.0.9
    hono ^4.12.25 ^4.12.34 GHSA-8j4g-w8fx-2239 et al., <4.12.34
    ip-address unpinned ^10.3.1 GHSA-mwp4-54f8-5fhr, <=10.3.0
    body-parser unpinned ^2.3.0 GHSA-v422-hmwv-36x6, >=2.0.0 <2.3.0

    package-lock.json is unchanged: every resolved version was already above the
    new floors, so this closes a latent gap rather than a live one, and the audit
    gate was green before and after. The unpinned entries are the dangerous ones —
    an overrides entry is the only thing that pulls a sticky lockfile forward on
    install, so an unpinned transitive dep is the one that drifts into a range
    while pinned ones re-resolve themselves.

  • The override guard test now covers every pin. It asserted only qs, hono
    and form-data, leaving fast-uri and brace-expansion declared but
    unchecked — a pin nothing checks is indistinguishable from no pin once it
    rots. The six hand-rolled assertions are replaced by a PINS table plus a
    completeness assertion that fails when the overrides block and the table
    disagree in either direction, so a pin cannot be added without a guard. Both
    directions were vacuity-probed rather than assumed.

v2.4.0

Choose a tag to compare

@lazyants lazyants released this 20 Aug 16:55
b0eebc9

Added

  • hetzner_create_server now accepts a volumes array, so volumes can be attached
    at creation time instead of requiring a separate attach call — the automount
    flag already referenced volumes but there was no way to supply them (#64).
  • hetzner_list_server_actions gains sort and status filters, bringing it in
    line with the other ten list_<resource>_actions tools (#64).
  • hetzner_create_primary_ip gains an optional assignee_id, so a Primary IP can
    be created and assigned in one call (#63).
  • PathSegmentSchema in schemas/common.ts: a second guard layer alongside
    pathSeg() that rejects ., .., empty and embedded-slash values before a URL
    is built. encodeURIComponent never escapes ., so pathSeg('..') was a no-op.
    Applied to IdOrNameSchema and the nine path-interpolated zone RRSet name
    fields (#66).

Changed

  • hetzner_create_primary_ip: the datacenter parameter is replaced by
    location.
    Hetzner removed datacenter from POST /primary_ips
    (additionalProperties: false), so calls passing it were already being rejected
    upstream. location matches the convention used by servers, volumes and load
    balancers (#63).
  • assignee_type is now pinned explicitly whenever assignee_id is supplied
    without it. Hetzner's default flips from server to unassigned on 2026-08-01,
    which would have silently stopped create-and-assign calls from assigning (#63).
  • index.ts and the eight entry-*.ts binaries iterate a shared SPLITS /
    ALL_REGISTRARS map from the new src/splits.ts instead of hand-listing
    register*Tools calls, so the entry-point partition can no longer drift between
    runtime wiring and the tests asserting tool counts (#67).
  • Removed the unused src/types/ tree (15 files); ZONE_RRSET_TYPES moved to its
    sole consumer, and MAX_PER_PAGE is now the single source of the per_page
    ceiling in PaginationParams (#66).
  • Extracted an rrsetPath() helper so the zone/name/type path is built and
    encoded in exactly one place (#66).

Fixed

  • 429 backoff was defeated by malformed headers. Retry-After and
    ratelimit-reset were parsed with a bare parseInt, which returns NaN for an
    HTTP-date Retry-After; Math.max(0, NaN) is NaN and setTimeout(NaN) fires
    immediately, so a rate-limited request retried in a tight loop. Ported lexware's
    parseRetryAfterMs (delta-seconds plus strict IMF-fixdate parsing, clamped to
    the setTimeout ceiling) and guarded the ratelimit-reset fallback with a strict
    /^\d+$/ parse so a malformed value falls back to exponential backoff (#61, #65).

Security

  • Request-body secrets are now scrubbed from chained error causes.
    scrubConfig left config.data — the serialized request body — intact when an
    AxiosError was chained as { cause: err }, so a certificate private_key, a
    Storage Box password, or a DNS TSIG key could reach a logger walking the cause
    via util.inspect(err, { depth: null }) or AxiosError.toJSON() (#51, #65).
  • Regenerated package-lock.json to clear the npm audit gate, which had gone red
    as the advisory database moved: 6 production vulnerabilities, 3 high
    (fast-uri, axios, ip-address high; hono, @hono/node-server moderate;
    body-parser low). No manifest change was needed — every package resolved past
    its advisory range within the ranges already declared (#71, #73).

Removed

  • dist/types/** is no longer published (45 files). The src/types/ tree was
    dead code — 14 of its 15 modules exported only TypeScript interface
    declarations, so their compiled .js files were empty stubs and no runtime value
    can break. The single runtime export, ZONE_RRSET_TYPES, moved into
    src/tools/zones.ts and is no longer exported from a published path.

    This package declares no exports map, so deep imports such as
    @lazyants/hetzner-mcp-server/dist/types/servers.js were resolvable, and type
    imports of those interfaces will stop resolving.
    No compatibility shim is
    provided: the package's product is its nine stdio bin entries, and no library
    API is documented. If you were importing these types, copy the interfaces you
    need — they were never part of a supported surface.

Internal

  • Test-coverage gaps closed by the splits.ts refactor: the Zod-4
    required[]/describe enumeration test was building its "full server" from 15 of
    16 registrars (156 of 185 tools, omitting Storage Boxes) and now covers all 185;
    the annotation test swept 3 registrars and now sweeps every registered tool for
    boolean-hint presence and the read-only/destructive exclusivity invariant. The
    get_action-exclusion GET scan was widened to also catch storageBoxRequest
    calls (#53, #67).

v2.3.1

Choose a tag to compare

@lazyants lazyants released this 22 Jun 08:01
5f54c40

Security

  • Redact the bearer token (HETZNER_API_TOKEN / HETZNER_STORAGE_API_TOKEN) from rethrown axios error cause chains (defense-in-depth, #44). API errors are wrapped as new Error(msg, { cause: err }); the chained AxiosError previously retained the token in config.headers.Authorization and Node's raw request._header block, so a logger walking the cause via util.inspect(err, { depth: null }) or AxiosError.toJSON() could surface it. A central wrapHetznerError now sanitizes the error in place — scrubbing Authorization/proxy-authorization/cookie headers (case-insensitively) on both config refs, and dropping config.auth/proxy.auth, the request/response.request objects, and any object cause. The single request() chokepoint routes both the Cloud (api.hetzner.cloud) and Storage Box (api.hetzner.com) call paths through it, so both token families are covered. Thrown messages are unchanged. Locked with a regression test asserting no token survives util.inspect(depth:null) or toJSON(), through both hetznerRequest and storageBoxRequest.

Fleet lockstep with lexware v3.2.1.

Full Changelog: v2.3.0...v2.3.1

v2.3.0

Choose a tag to compare

@lazyants lazyants released this 20 Jun 13:02
f22b0de

Minor release.

Added — Storage Boxes domain (29 tools, 156 → 185)

New hetzner-mcp-storage-boxes entry point covering Storage Box CRUD + folders + actions (protection/type/password/access-settings, snapshot plan enable/disable + rollback), snapshots CRUD, subaccounts CRUD + actions, and Storage Box types. Storage Boxes call the separate https://api.hetzner.com/v1 host via a second cached client that reuses the same retry, 429 backoff, and error normalization. Token resolves as HETZNER_STORAGE_API_TOKEN || HETZNER_API_TOKEN (an empty storage token falls back to the Cloud token), so a single existing token keeps working. pathSeg URL-encoder promoted to schemas/common.ts and unit-tested.

Deprecated — datacenters

hetzner_list_datacenters / hetzner_get_datacenter now flag Hetzner's removal of /datacenters (after 2026-10-01, HTTP 410) and point to hetzner_list_server_types (locations[].available/recommended) and hetzner_list_locations. Tools remain functional until the cutover.

See CHANGELOG.md for details.

v2.2.1

Choose a tag to compare

@lazyants lazyants released this 20 Jun 11:47
e9ba1a2

Security patch — clears two HIGH npm-audit advisories that were failing the npm audit --audit-level=moderate --omit=dev CI gate:

  • form-data (GHSA-hmw2-7cc7-3qxx) — CRLF injection via unescaped multipart field/file names; reaches the prod tree via axios. Pinned ^4.0.6 (lockfile resolves 4.0.6).
  • hono (<= 4.12.24) — serve-static path traversal et al.; reaches prod via @modelcontextprotocol/sdk@hono/node-server. Override ^4.12.25.

Dependency-only change — no runtime or API behaviour changes. See CHANGELOG.md for full details.

v2.2.0

Choose a tag to compare

@lazyants lazyants released this 13 Jun 19:13
5560ceb

Wave-2 minor release.

Added

  • 9 new Cloud API tools (147 → 156): hetzner_attach_server_to_network, hetzner_detach_server_from_network, hetzner_add_server_to_placement_group, hetzner_remove_server_from_placement_group, hetzner_reset_server_password, hetzner_enable_lb_public_interface, hetzner_disable_lb_public_interface, hetzner_change_lb_dns_ptr, and hetzner_get_pricing.
  • Read-only API-reference Resource reference://hetzner/api (text/markdown) on the main binary and all 7 split entry points.

Fixed

  • hetzner_attach_lb_to_network now forwards the optional ip_range field.

See PR #37 (feature) and #38 (release).

v2.1.1

Choose a tag to compare

@lazyants lazyants released this 13 Jun 17:15
0746931

Patch release.

  • Security (#35): pin a qs override (^6.15.2) to clear the transitive npm-audit DoS advisory (GHSA-q8mj-m7cp-5q26) and reinstate a clean npm audit gate. No runtime behavior change.
  • Deps (#34): grouped minor+patch Dependabot bump (lockfile-only refresh of 5 transitive packages). No behavior changes.

Published to npm with provenance + submitted to the MCP Registry via publish-registry.yml.

v2.1.0

Choose a tag to compare

@lazyants lazyants released this 20 May 18:45
f0a6ab9

Added

  • DNS Zones module (src/tools/zones.ts) + new entry binary
    hetzner-mcp-dns (src/entry-dns.ts) covering 22 tools across
    the Hetzner DNS Zones API (zones CRUD, RRSets CRUD,
    primary-nameserver changes, protection, TTL changes, zonefile
    import/export). Wraps the GA Nov 2025 surface on the existing
    api.hetzner.cloud baseURL + token (PR #24).
  • change_*_protection across 7 resources: servers, load
    balancers, volumes, networks, floating IPs, primary IPs, images.
    Adds the data-loss guard rails the API has supported for years
    but were never wrapped (PR #21).
  • Per-resource list_*_actions for 8 resources: load balancers,
    volumes, floating IPs, primary IPs, networks, firewalls,
    certificates, images (PR #23). Replaces the global /actions
    endpoint Hetzner deprecated in January 2025.
  • Server actions: request_console, enable_backup,
    disable_backup, change_alias_ips, change_dns_ptr (servers);
    change_ip_range (networks). Plus must_be_unassigned
    precondition notes on delete_floating_ip / delete_primary_ip
    descriptions (PR #25).
  • Per-tool axios-mock test coverage across all 14 tool modules
    (PRs #26, #28, #29, #30). Every tool now has at least one test
    asserting path/method/body/query shape against a mocked
    hetznerRequest().
  • README tool counts updated to reflect the 147-tool surface
    (PR #27).

Changed

  • Migrated to Zod 4 (zod ^4.4.3). Brings hetzner into line
    with @lazyants/lexware-mcp-server and
    @lazyants/transkribus-mcp-server (both already on Zod 4 since
    their 2.0.1). All z.record(z.unknown()) call sites updated to
    the Zod-4 two-argument form z.record(z.string(), z.unknown()).
    Runtime-verified .describe() propagation and tools/list
    required[] correctness on the full schema surface (PR #22).
  • Bumped grouped minor+patch deps (PR #18): see the Dependabot PR
    for the exact diff. No behavior changes.

Fixed

  • formatResponse no longer sets structuredContent for array
    payloads. The MCP SDK rejects arrays in structuredContent;
    previously this would crash on any tool returning a top-level
    array (e.g. paginated list_* shortcuts). Now array payloads
    go through the JSON-stringified content[] path only (PR #31).

Note on tool count

Total registered tools grew from 104 to 147 (+43): +22 DNS Zones
(PR #24), +8 list_*_actions (PR #23), +6 server/network actions
(PR #25), +7 change_*_protection (PR #21). Smoke test asserts
147 across full server + per-entry splits (28/21/25/20/17/14/22).

v2.0.1 — Clear transitive CVE backlog

Choose a tag to compare

@lazyants lazyants released this 20 May 10:59
1d7023d

Catch-up release matching lexware-mcp-server v2.0.1 and transkribus-mcp-server v2.0.1.

What's new

This is a security-hygiene release. No code changes; no tool additions.

All overrides target dev-transitive deps — no runtime impact on the published package surface.

Verification

```
npm view @lazyants/hetzner-mcp-server version # → 2.0.1
```

v2.0.0 — FSL-1.1-MIT relicense + hygiene baseline

Choose a tag to compare

@lazyants lazyants released this 06 May 16:50

First major release. Two themes: (1) license switch to FSL-1.1-MIT, and
(2) project-hygiene baseline shared across the Lazy Ants MCP server
fleet (npm provenance, Trusted Publishing, ESLint 9, Dependabot,
multi-Node CI, version-sync guard).

Versions ≤ 1.1.1 remain under the original MIT license.

Changed

  • License → FSL-1.1-MIT. Auto-reverts to MIT after two
    years. The license switch is what makes this a semver-major bump;
    the rest would be minor/patch on their own.
  • Dependencies: typescript^6.0.0, vitest^4.1.0,
    actions/checkoutv6, actions/setup-nodev6,
    axios^1.16.0 (CVE coverage), @modelcontextprotocol/sdk
    ^1.29.0. zod stays on ^3.25.0 — the MCP SDK has known
    .describe() propagation bugs with zod 4.
  • Releasing: GitHub Releases auto-publish to npm with provenance
    (--provenance --access public) before pushing to the MCP Registry.
    Authentication via npm Trusted Publishing — no NPM_TOKEN secret
    in the repo. mcp-publisher is pinned by version + SHA-256 and
    smoke-tested before the irreversible npm publish. Cutover/recovery
    guard skips npm publish cleanly if the version is already on npm.
  • CI: test workflow runs on Node 20 and 22 with npm cache, lint,
    version-sync check, and npm audit --audit-level=moderate --omit=dev.

Added

  • ESLint 9 flat config (eslint.config.mjs) with
    tseslint.configs.recommended + globals.node. New npm run lint
    script, gated in CI.
  • Dependabot for weekly npm + GitHub Actions updates, grouped
    minor+patch, ignoring @types/node major bumps.
  • scripts/check-versions.mjs enforces
    package.json#version == server.json#/packages[0].version
    (hard fail) and warns on registry-version regressions. Implements
    full semver §10/§11 (build metadata stripped, prerelease ordering).
  • SECURITY.md vulnerability disclosure path.
  • CHANGELOG.md (Keep a Changelog format).
  • engines.node >= 20 in package.json.
  • Hetzner API field additions from the upstream changelog:
    • hetzner_rebuild_server accepts optional user_data (2026-01-16).
    • LB HTTP service schemas accept optional timeout_idle (2026-04-30).
    • hetzner_create_primary_ip no longer requires assignee_type
      (2026-04-27).

Verification

  • npm: npm view @lazyants/hetzner-mcp-server@2.0.0 will show a
    provenance attestation block once the publish workflow completes.
  • MCP Registry: listing at
    https://registry.modelcontextprotocol.io/ updates to 2.0.0.

Full Changelog: v1.1.1...v2.0.0