Skip to content

Release v4.2.0

Latest

Choose a tag to compare

@github-actions github-actions released this 12 Sep 05:29
· 22 commits to master since this release
27e58a2

v4.2.0 — One place to add a meta-tool, for the eight that are uniform

Released: 2026-09-12
Upstream base: CyberChef v11.4.0

What changed, in one paragraph

A meta-tool that declares a run handler in the META_TOOLS literal is now dispatched from that
declaration, so its name, description, schema and handler live in one entry instead of a literal and
a dispatch branch that nothing checked agreed with each other. Eight of the ten recipe tools moved;
the dispatch chain went from 24 if occurrences to 16. No behaviour changed, and that is the
claim this release spent most of its effort proving rather than asserting.

Both of the charter's claims were wrong

The charter is one page and it makes two load-bearing claims. Neither survived.

"The dispatch table has never had its version"

It has, since v3.7.0. tests/mcp/meta-tool-parity.test.mjs asserts exactly the two directions this
release set out to gate, and its own header cites the same v3.0.0 carried-forward item as the
reason. So the headline deliverable had shipped three releases earlier, and a release about removing
duplication began by building a second copy of a check.

It is still worth having done, because of how it was already solved. The v3.7.0 gate is
syntactic — it greps for if (name === "cyberchef_…") and compares the set against the literal
— and F-01 predicted before any code that a syntactic check
would be fragile here. The prediction landed harder than expected: the refactor this very charter
asks for breaks it, because eight tools dispatched from a table have no branch to grep. A gate
that forbids the change it was written to protect is not a gate, it is a lock.

So it is superseded and removed rather than patched. Patching it to also read the table would leave
two overlapping checks with the more fragile one authoritative, which is the two-sources problem
this release exists to remove. Its one assertion behaviour cannot reach — a meta-tool shadowing a
registry tool, which ToolRegistry.register catches only in the other direction — is carried into
tests/mcp/dispatch-table.test.mjs and strengthened to derive names from ToolRegistry.exposedName
rather than from a filename regex.

"They differ only in the method called"

True for eight of ten. The two exceptions differ in ways the shape does not show:

Tool What was hidden
cyberchef_recipe_execute Calls validateInputSize(args.input) first, and returns the recipe's own string result rather than a JSON envelope
cyberchef_recipe_export exportRecipe already returns a string; the table's JSON.stringify encoded it twice, so callers got "{\n \"id\": …}" instead of the document — and it round-trips through recipe_import, so this broke a pair rather than only cosmetics

Folding them in silently dropped an input-size guard and changed two output shapes. The charter's
own kill criterion reads "a tidier dispatch that answers differently is a regression with good
intentions"
, and the regression happened on the first attempt. What stopped it shipping was the
pre-existing suite — in-process-handlers.test.mjs and an executable example — not the new
gate, which asks only whether a tool is reachable, and both remained perfectly reachable while
answering differently.

Both keep their branches, with the reason written where the next person will try to remove them.
The table holds 8 of 10; a table with two special cases inside it is a table of exceptions.

The gate is behavioural, and its second direction was circular

tests/mcp/dispatch-table.test.mjs calls every advertised tool through a real client on all three
surfaces and reads the answer, distinguishing "the handler rejected your arguments" (a pass — the
tool is reachable) from "no such tool" (a failure). The distinction is keyed to the error code,
because the word "unknown" appears in both answers and means opposite things.

The second direction — every callable meta-tool is advertised — was circular on the first
attempt: it read the META_TOOLS literal and compared it against tools/list, so deleting a
declaration removed the name from both sides at once and the check still passed. Proven by deleting
cyberchef_quota_info's entry while leaving its dispatch branch, the exact "callable but invisible"
defect, and watching all three tests stay green. It is rebuilt as syntax proposes, behaviour
decides
: the dispatch chain is scraped only for candidates, and each unlisted candidate is then
called to settle whether it really dispatches.

Measured

                          before        after
dispatch `if` occurrences     24           16
tools dispatched by table      0            8
mcp-server.mjs statements  85.40%       85.56%   (357/418 -> 338/395)
mcp-server.mjs branches    68.04%       70.39%   (164/241 -> 164/233)
mcp-server.mjs functions   91.48%       92.85%
global statements          95.30%       95.50%
global branches            89.31%       89.88%

Coverage moved the right way and for the right reason: 25 fewer statements and 10 fewer branches
to cover
, with the covered count essentially unchanged.

Two of mcp-server.mjs's uncovered branches are there on purpose. The table dispatch guards against
a handler returning a value JSON.stringify cannot serialise, which would otherwise surface as a
bare TypeError from Buffer.byteLength naming neither the tool nor the cause. No handler does
that today — all eight were called and checked — so the guard is unreachable by construction and
therefore uncovered
, and it costs about 0.03% of global statement coverage. The alternative
offered in review was JSON.stringify(value ?? {}), which is covered, cheaper, and wrong: it serves
{} to the caller and turns a handler bug into a silent empty answer. Verified to fire, by making a
handler return undefined:

Error [INVALID_INPUT]: Tool 'cyberchef_recipe_list' returned no serialisable value
  - tool: "cyberchef_recipe_list"
  - returned: "undefined"
``` The sprint asks whether it moved on the
theory that a consolidation which does not move it has probably moved complexity rather than removed
it.

## Compatibility

**No behaviour changes, and no configuration changes.** Every meta-tool answers exactly as it did in
v4.1.0. Two instruments in `docs/internal/measurements/` exist to make that checkable rather than
believable, and both were run across the consolidation:

- `list-contract-snapshot.mjs` — the whole `tools/list` response: descriptions, `inputSchema`,
  annotations, titles, and the order the 2026-07-28 deterministic-ordering SHOULD rests on.
  **Byte-identical** on all three surfaces: 21,185 / 145,016 / 583,253 capture bytes. Those are
  pretty-printed snapshots of the tools array and are deliberately **not** the canonical surface
  figures (`npm run measure:surfaces`: 15,620 / 109,548 / 426,706, the compact wire size of the
  whole result), which this release does not move. Identical captures are the stronger statement of
  the two, since they compare every field rather than a total.
- `meta-tool-answers.mjs` — 27 meta-tool calls through a real client, successes and rejections
  alike, with ids and timestamps normalised. **Byte-identical.**

`handleListTools`, including the `visibleTools()` call that filters the list by scope when auth is
on, is unchanged line for line — so the auth-filtered listing is identical by composition over an
identical input.

Both instruments carry the same warning, earned: **compare sizes before believing a clean diff.**
The first run of the first one reported all three surfaces identical when both captures were empty,
because the script had been placed outside the repository and could not resolve the SDK.

## Verification

```text
npm run lint            zero errors
npm run check:versions  all version references agree
npm run test:mcp        1,728 passed (75 files)
npm run test:coverage   96.44 lines / 89.88 branches / 96.84 functions / 95.50 statements

Every figure above is measured for this release, not carried forward. One that was carried forward
did not survive the check: this project's AGENTS.md recorded 77 MCP test files for a release,
and git ls-tree v4.1.0 tests/mcp returns 75.