Skip to content

v4.0.0 release notes

This is the first major since v3.0.0, and it exists for a reason the plan did not model.

Why a major, when the trigger watch says none is due

Section titled “Why a major, when the trigger watch says none is due”

npm run check:v4-triggers was run first, as the ritual requires, and reported what it has reported for four releases:

draft changelog EMPTY ("Changes since the most recent release will accumulate here.")
current revision 2026-07-28, no successor
SDK @modelcontextprotocol/{server,node,client} all 2.0.0
conformance 0.2.0-alpha.11
server.json schema 2025-12-11 still newest

Nothing fired, and this release cuts a major anyway — because the plan was wrong, not the measurement.

v4.0.0-plan.md defined four triggers, and every one is an external MCP specification event. The principle was sound: SemVer says MAJOR means something broke, and inventing a break to reach a round number is worse than staying on v3 — which is exactly why the v4.1.0, v4.2.0 and v4.3.0 charters correctly shipped as v3.9.0, v3.10.0 and v3.11.0.

The error is that the trigger set was exhaustively external, leaving no internal path to a major at all. So v4.0.0 waited on an event that might never arrive, removable surface accumulated with no mechanism able to remove it, and the version number stopped carrying information.

A major version is not only a response to someone else’s breaking change. It is also how a project retires its own past.


cyberchef_migration_preview and cyberchef_deprecation_stats

Section titled “cyberchef_migration_preview and cyberchef_deprecation_stats”

Two tools advertised on every surface to help a caller migrate to v2.0.0 — nine minors ago.

The state of the machinery behind them is the argument. Calling cyberchef_bake normally produced exactly one deprecation record:

{
"code": "DEP007",
"description": "WITHDRAWN: cyberchef_bake is NOT being renamed. It keeps its name.",
"alternative": "No action required.",
"withdrawn": true
}

A warning about a change that was cancelled, telling the caller no action is required. Of the eight DEP codes, three were withdrawn in v2.0.0 and the rest described work that had shipped (Zod validation, structured errors, unified config) or been decided against — DEP005 and DEP006 announced that positional recipe arguments would be removed, while AGENTS.md and the FAQ correctly document that both argument forms work and are tested.

cost: 995 bytes of EVERY tools/list, on every surface
522 lines in src/node/deprecation.mjs, 580 lines of tests

For scale, cyberchef_pqc_identify — the entire new tool v3.11.0 shipped — cost 1,470 bytes.

A published bin, so removing it is a genuine break. Its premise is false: it converts v1-format recipes to v2, and both formats work. Verified through a real client rather than assumed:

v1 positional args [{op:"To Base64", args:["A-Za-z0-9+/="]}] -> "SGk="
v1 bare string op ["To Base64"] -> "SGk="
v2 named args [{op:"To Base64", args:{alphabet:"..."}}] -> "SGk="

All three identical. It converted a working format into another working format.

Documented as a historical alias since v2.1.0, and it silently outranked CYBERCHEF_TOOL_SURFACE in both directions — so a value set once and forgotten kept serving all 544 tools regardless of what the newer variable said. A silent alias for the most expensive configuration change this server has is worth being explicit about. Use CYBERCHEF_TOOL_SURFACE=all.

It is inert, not merely unsupported, and a test pins both directions: an alias that half-works is worse than one that is gone.

suppressDeprecations and v2CompatibilityMode went with the system they configured, and tools.exposeAllOps went with the inert alias it set – leaving that mapping would have let a config file set a variable nothing reads. Settings: 64 in 15 sections → 61 in 14. All three fail closed: the server refuses to start and names the key.

The floor moves to >=26 <27. CI has tested both ends of the range since v2.8.1; the image has shipped Node 26 throughout.


surface before (v3.11.0) after (v4.0.0) delta
index 43 / 45,963 bytes 41 / 44,968 -2 tools, -995 bytes
curated 121 / 109,209 bytes 119 / 108,214 -2 tools, -995 bytes
all 546 / 426,367 bytes 544 / 425,372 -2 tools, -995 bytes

The round-trip multiplier improved for the first time since v3.2.0 — index plus one operation schema is now 9.1x cheaper than all, up from 8.9x. That figure has fallen at every release that added a registry tool (18.2x → 9.5x → 9.2x → 9.0x → 8.9x), because a registry tool has no navigation path and must sit in the index. Removing two tools that navigate to nothing moved it back. The mechanism working in the direction it was designed for.


T-13 — accumulated removals, added to scripts/check-v4-triggers.mjs so it is executed rather than remembered, like its four neighbours:

A major is due when surface still advertised to callers describes a migration that has completed, been withdrawn, or been decided against.

It is the only offline check in that script — the question is about this repository, not the ecosystem. It reads none today, and that zero is worth nothing on its own, so it was verified by planting the real defect (the removed tool declaration, put back) and watching it fire and exit 1.

The original kill criterion is not repealed. “A major exists because the protocol forced one” remains true of triggers A–D; it is simply no longer the only reason one may exist.


The image is renamed on a major, and I had not done it. bump-version.mjs moved the version and left cyberchef-mcp_v3:4.0.0 — a tag that will never be pushed, since mcp-release.yml derives the GHCR package from the tag’s major. check:versions failed on five locations at once. This is exactly the v3.0.0 defect that made the check assert the major and not only the version, and this is the first major since — a gate built for a once-in-years event worked the first time that event recurred.

A test that mutates a source file is a race. Verifying T-13 meant planting the defect somewhere. The first attempt planted it in src/node/mcp-server.mjs with a restore in finally. It passed alone, passed under coverage, and failed the full suite in a file it never touched:

FAIL tool-surface-figures > serves exactly the tool counts and byte sizes the table records
expected 'curated tools: table says 119, server serves 120' to be ''

Vitest runs test files in parallel, and that suite boots a real server from the file being planted. Fixed by copying the script and its inputs into a temp directory. The restore was never the problem; the window was, and a window is invisible to a single-file run.


npm run lint zero errors
npm run check:versions every location agrees, including the _v4 image rename
npm run check:server-json validates against 2025-12-11
npm run check:v4-triggers nothing fired, T-13 included
npm test 241/241 Node API, 2289/2289 operations
npm run test:mcp 1713/1713 across 74 files, twice
npm run test:coverage exit 0 — 96.08 lines / 89.43 branches / 96.54 functions / 95.1 statements

Node 26 is now the floor. >=26 <27.

The image is renamed. v4.x publishes to ghcr.io/doublegate/cyberchef-mcp_v4; v3.x stays at _v3 and is not superseded in place. Docker Hub remains the single un-suffixed parobek/cyberchef-mcp.

If you call cyberchef_migration_preview or cyberchef_deprecation_stats, they are gone. There is no replacement, because there is no migration left to preview: v2.0.0’s recipe and error formats have been the only formats for nine minors, and both recipe argument forms are supported.

If you run cyberchef-migrate, it is gone. Your v1 recipes already work unchanged.

If you set CYBERCHEF_EXPOSE_ALL_OPS, it is now ignored, and the server says so at startup rather than leaving you to wonder why the tool count collapsed:

WARN CYBERCHEF_EXPOSE_ALL_OPS=true is set and is IGNORED: it was removed in v4.0.0.
The tool surface is "index" -- set CYBERCHEF_TOOL_SURFACE=all|curated|index to choose one.

It reports rather than restores: honouring the variable again would undo the removal, and guessing what the operator meant is exactly what the silent alias did wrong. Check which surface you were actually getting, because the old variable outranked the new one.

If you set compatibility.suppressDeprecations or compatibility.v2CompatibilityMode in cyberchef.config.json, remove them: configuration parsing fails closed on unknown keys, so the server will refuse to start rather than ignore them.