v2.2.0 release notes
Release Date: 2026-08-31
Upstream Base: CyberChef v11.4.0 (unchanged)
Licence: GPL-3.0-or-later
Node: >=24 <27
Highlights
Section titled “Highlights”v2.1.0 made 524 tools usable. This release is what happened when the server was connected to a real MCP client and pointed at real work.
Two things produced almost everything below. The first was a battery of 74 cases drawn from the published CyberChef recipe corpus — malware triage, IOC extraction, DFIR, red-team crypto, CTF ciphers, steganography, flow control — run against the published v2.1.0 container rather than against the source tree. The second was registering that container as an MCP server in a client and reading the schema it actually loaded.
Those two found different things, and neither found the other’s:
| v2.1.0 | v2.2.0 | |
|---|---|---|
Generate QR Code |
"" — the PNG deleted |
the image |
Play Media |
23 characters of player chrome | the audio |
Generate all hashes |
error:0308010C |
44 hashes |
| A misspelled argument | silently ignored, wrong answer | rejected, with the valid names |
cyberchef_bake argument schema |
positional only, contradicting the implementation | both forms |
| Tools carrying annotations | 0 of 527 | 527 of 527 |
| Prompts / resources | none | 5 / saved recipes |
| MCP tests | 872 | 955 |
Multi-modal results
Section titled “Multi-modal results”Generate QR Code returned an empty string, and always had. Its output type is html and the
payload rides in <img src="data:image/png;base64,...">; the html-to-text conversion removed the
tag carrying the entire result. Not a v2.1.0 regression — v2.0.0 returned {} for the same call,
because it JSON.stringify’d the Dish. The operation has never worked over MCP. Only the failure
mode changed.
Play Media lost its audio identically, leaving 23 characters of player chrome where a
recording had been.
MCP has carried an image block since its first revision and an audio block alongside it, so one
extractor now covers <img>, <audio> and <video>, and the block type is chosen from the
payload’s MIME type. Magic-number sniffing (PNG/JPEG/GIF/BMP/WebP, WAV/MP3/OGG) catches the binary
path, where an operation such as From Base64 of a PNG produces no markup at all.
RIFF needed care rather than another table row: RIFF....WAVE is audio and RIFF....WEBP is an
image, and the first four bytes are identical, so the form type is read separately and a buffer
truncated before it decides nothing.
Video is the honest exception. MCP has no video content block, so the data URI is returned as
text — unreadable, but recoverable, which is the entire difference between that and stripping it.
Returning an image block for a video would be worse than returning nothing.
Binary is deliberately unchanged by default. It looks like mojibake, but that was measured
rather than assumed: the latin1 mapping is byte-for-byte reversible (str.charCodeAt(i) equals
bytes[i] for every byte of a gzip payload), so nothing is lost. It is a readability problem, not a
correctness one, and altering output for 76 operations to fix something that is not broken is a
worse trade than an opt-in. CYBERCHEF_BINARY_OUTPUT=base64 opts in.
The hash failure the last release said it had fixed
Section titled “The hash failure the last release said it had fixed”Generate all hashes failed in the shipped container:
Error [INVALID_INPUT]: Generate all hashes - error:0308010C:digital envelope routines::unsupportedThe v2.1.0 notes say --openssl-legacy-provider fixed this. The variable was set — podman inspect confirms it in the image’s NODE_OPTIONS — and it did nothing, because the runtime image
has no legacy provider module to load:
$ podman run --rm --entrypoint /usr/bin/node <image> -e "require('crypto').createHash('md4')"Unable to load legacy provider.node v26.8.1 openssl 3.6.4A filesystem walk of the whole image finds no *legacy*.so. This is the same shape as the SafeRegex
incident: a mitigation documented as active that the shipped artefact never carried.
The blast radius was one operation, not the twenty it looked like. Running all 21 members
individually in the container, 20 pass — MD4, NT Hash and Whirlpool included, because
CyberChef implements those in JavaScript and OpenSSL never enters. Only LM Hash reached it, via
ntlm@0.1.3’s crypto.createCipheriv("DES-ECB", ...), and single DES is legacy-only under
OpenSSL 3.
Fixed by removing the dependency rather than satisfying it: LM Hash is computed with node-forge,
already a direct dependency, in pure JavaScript. Both canonical vectors match
(LM("password") = E52CAC67419A9A224A3B108F3FA6CB6D). The flag is gone from the Dockerfile and both
npm scripts, and nothing needs it — verified by running all 2,289 operation tests on a host that
also lacks the provider.
Separately, and worth keeping on its own merits: one failing algorithm no longer discards the other twenty. Every algorithm ran unguarded, so a single throw destroyed every digest that had computed correctly.
Two silent-wrong-answer defects
Section titled “Two silent-wrong-answer defects”Both return data, report success, and are wrong. For a server used in forensic and malware work that is the worst failure mode available — worse than an error, because nothing prompts a second look.
A misspelled argument was silently dropped. Running a three-round base64 decode via Label and
Jump returned one round, with no error. The recipe passed {label: "top", maximum_jumps: 2};
the real names are label_name and maximum_jumps_if_jumping_backwards, so both keys matched
nothing and the defaults were used:
{label: "top", maximum_jumps: 2} -> ["", 10] // empty label: no jump ever happensCyberChef’s UI labels sanitise into forms nobody would guess, so this is a mistake a caller will actually make. Unknown keys are now rejected, naming both the offending ones and the accepted ones.
cyberchef_bake advertised the wrong argument shape. Its schema declared args as
type: "array" — positional only — while the implementation has accepted named arguments since
DEP005, which is the form the entire v2.1.0 usability effort rests on. A client that validates
outbound arguments against inputSchema could not send the supported form at all, and
cyberchef_recipe_create declared the same concept as an object two tools away. Now
anyOf: [object, array].
That second one was found only by connecting a real MCP client and reading the schema it loaded. Neither the test suite nor the container harness had ever compared the advertised argument shape against the accepted one — the same blind spot, in a new place, that let 524 empty schemas ship for three releases.
Tool annotations
Section titled “Tool annotations”Every one of the 527 tools now carries readOnlyHint, destructiveHint, idempotentHint and
openWorldHint, plus a human-readable title — “AES Encrypt” rather than cyberchef_aes_encrypt.
With no annotations a careful client must assume the worst of every tool, so a session that decodes
base64, extracts URLs and hashes the result asks for approval three times, for three operations that
read their input and return a value.
The exceptions were measured rather than guessed, because an annotation is worth exactly as much as its accuracy:
- Network reach by grepping
src/core/operations/forfetch/XMLHttpRequest/axios. Exactly two:HTTP requestandDNS over HTTPS. They are kept independent ofreadOnlyHint, because they are: a DNS lookup reaches the network and changes nothing. - Non-idempotence by running each candidate twice on the same input and comparing. That is
why
BcryptandDerive PBKDF2 keyare marked (both generate a random salt, which reading the names would not reveal) and whyArgon2andScryptare not (fixed defaults, identical output both runs). Over-marking would cost real cacheability.
cyberchef_bake is deliberately marked neither read-only nor non-destructive. It runs
caller-supplied recipes, which may contain HTTP request with a POST or a DELETE. Marking it read-only would be convenient — it is the primary
tool, so a prompting client now prompts often — and false. A hint that is convenient and wrong
teaches a client to ignore the whole set. Callers wanting the cheap path have one: call the
operation tool directly, where all 504 are annotated from their own behaviour.
Prompts and resources
Section titled “Prompts and resources”Two MCP surfaces this server had never served.
Prompts answer the question the tool list does not. A 24-tool navigation index is the right shape for a model that already knows it wants to base64-decode something, and the wrong shape for someone holding a suspicious blob — the case this server is most useful for and worst at advertising. Five, each encoding a real procedure from the upstream recipe corpus rather than restating the tool list:
| Prompt | What it encodes |
|---|---|
analyse-unknown-data |
Magic first, because guessing before identifying wastes calls |
extract-iocs |
extract, decode-then-extract, and defang before reporting |
deobfuscate-script |
unwrap layer by layer; the four common chains, in order of frequency |
identify-hash |
Analyse hash, then what actually distinguishes the candidates |
decode-chain |
compose one recipe rather than one call per layer |
Resources expose saved recipes at recipe://<id>, with a recipe://{id} template. A saved
recipe is reference material — browsed and attached far more often than executed — and MCP separates
that from actions deliberately. Reading one previously cost a tools/call a cautious client might
prompt for.
Keyed by id, not name, and that is load-bearing: recipe names are user-supplied and not unique, so a name-keyed URI would make one of two same-named recipes unreachable and silently return the other.
Adding these caught a drift worth recording: there are two places a server is constructed — the
module singleton backing stdio and the per-session factory backing HTTP — and the first version
updated only the factory. Every stdio client, which is most of them, would have been told this
server has no prompts while the handlers sat there answering. Both now read one
SERVER_CAPABILITIES.
The coverage gate was not a gate
Section titled “The coverage gate was not a gate”MCP Server CI had been failing on master since the v2.1.0 merge, with all four thresholds under
and every one of the 805 tests passing. Two causes:
core-ci.ymlhas nopull_requesttrigger, and its path filter omittedtests/**. So the MCP suite and the coverage gate were never a merge requirement, and v2.1.0 was reviewed, merged and tagged with the gate already red — it only turned visible on the push tomaster. A gate that cannot fail before a merge is not a gate.handler-dispatch.test.mjsdoes not dispatch handlers. Its header claims it tests “all handler branches in the CallTool request handler”; it asserts on re-exported helpers, andcreateMcpServer()was called by no test in the suite. 263 of 309 statements counted as dead code. Meanwhile the modules covered only through a spawned server measured near zero (tool-catalog.mjs: 2.17%), because v8 attributes nothing a child process does to the parent. “Tested but not measurably tested” is indistinguishable from “untested” to a threshold.
Fixed by testing the handlers, not by lowering the bar — a real MCP Client connected to
createMcpServer() over InMemoryTransport:
statements 74.11 -> 93.91 mcp-server.mjs 14.51 -> 87.70branches 67.36 -> 84.40 tool-catalog.mjs 2.17 -> 97.83lines 74.44 -> 94.58 tests 805 (26 files) -> 937 (32 files)Pull requests now run both.
- A draft recipe can be validated.
cyberchef_recipe_validateand_testrequiredidandversion— both server-assigned — so they could only check a recipe already saved, which is when checking is least useful. - Release notes get their relative links rewritten at publish time. A release body renders on
the Releases page, not in the tree, so
[text](https://github.com/doublegate/CyberChef-MCP/blob/master/docs/security/foo.md)404s for every reader of the release. v2.0.0 shipped 8 such links and v2.1.0 shipped 3, all broken. Two mistakes were made writing that fix and both are now pinned by tests: asedversion resolved../guides/xtoguides/xinstead ofdocs/guides/x, and an inline heredoc inside a YAML block scalar would have broken every release (bash -n: “unexpected end of file”).
Packaging, and one thing deliberately not shipped
Section titled “Packaging, and one thing deliberately not shipped”cyberchef-migrate is a real command at last. docs/v2.0.0-breaking-changes.md has told readers to
run it since v1.8.0 and it never existed – only the two MCP tools were ever built, and those
are reachable only from inside a session, which is no use to someone holding a directory of recipe
files. It shares its analysis with those tools rather than reimplementing it, so they cannot
disagree about what a v1 recipe means. It will not rewrite a file without --write, keeps a .bak,
and refuses to overwrite that .bak without --force – a second run would otherwise replace the
backup with the already-migrated file and destroy the only copy of the original.
Two navigation tools now declare an outputSchema and return structuredContent, so a
caller gets a typed object rather than parsing JSON out of a text block. Only those two: the 504
operations return whatever CyberChef returns, undocumented and varying per operation, and declaring
a schema for that would be a claim rather than a contract – and a wrong one makes the SDK reject
results that are perfectly valid.
npm publishing is prepared and not shipped. The package is cyberchef-mcp (the cyberchef name
belongs to upstream), version carries the product version with the upstream base moved to
cyberchefUpstreamVersion, and there is a files allowlist, a prepack that generates the two
gitignored files the server cannot start without, two bin entries and a server.json. npm pack
produces a correct 12 MB tarball.
Then installing that tarball and running it showed it would not work. npm 12 blocks dependency
install scripts by default (npm help install-scripts: “Dependency install scripts are blocked by
default”), and this project needs one – crypto-api ships extensionless imports that Node’s ESM
resolver rejects, so the installed server dies immediately on
Cannot find module '.../node_modules/crypto-api/src/hasher/has160'Confirmed with a minimal probe package whose only content was a postinstall echoing a marker: it
did not run either, so this is npm’s behaviour rather than something about this package.
Publishing anyway would ship something that fails on install for anyone on npm 12+ – the same shape
as the --openssl-legacy-provider claim this very release had to correct. Docker and GHCR remain
the supported channels and both work. The fix is to stop needing the patch at all, and is v2.3.0
work rather than something to rush into a release.
Three defects found on the way are fixed regardless, and each would have bitten later:
postinstallshelled out to grunt – a devDependency a consumer never installs – and tosed. It is pure Node now, which also deletes the macOS/Linux branching that existed only because BSD and GNUsed -idisagree about an argument.- The patcher looked in the wrong
node_modules: npm runs a dependency’spostinstallinside its own directory while dependencies are hoisted to the consumer root, so every patch reported “target absent” and silently did nothing. src/node/mcp-server.mjshad no shebang, so as abinentry the shell tried to execute JavaScript as a shell script:cyberchef-mcp: line 1: /bin: Is a directory.
Upgrading
Section titled “Upgrading”docker pull ghcr.io/doublegate/cyberchef-mcp_v2:latestNo configuration change is required, and no tool is renamed or removed. Three behaviour changes are worth knowing:
- Image and audio operations now return
image/audiocontent blocks rather than an empty string. A client that expectedcontent[0].textfromGenerate QR Codewas reading""; it now getscontent[0].data. - A misspelled argument is now an error rather than a silently-defaulted value. This surfaces latent bugs in existing recipes — which is the point, since those recipes were returning wrong answers.
cyberchef_bakeis annotated as not read-only, so a client that prompts on non-read-only tools will now prompt for it. Call operation tools directly to avoid that; they are annotated individually.
Verification
Section titled “Verification”npm run lint 0 errorsnpx vitest run tests/mcp/ 955 passed (33 files)npm run test:coverage 93.91 / 84.40 / 94.37 / 94.58npm pack + install + run tarball correct; npx blocked by npm 12 (see above)tests/operations 2289 passedtests/node 241 passed74-case discipline battery 62/62 simple, 12/12 complex, against the built imagecontainer verification QR image, 44 hashes, argument rejection, draft validation