Skip to content

v2.6.0 release notes

Release Date: 2026-09-02 Upstream Base: CyberChef v11.4.0 (unchanged) Licence: GPL-3.0-or-later Node: >=24 <27

The plan for this release was “Distributed Architecture”: externalise session state to Redis, add session affinity and sticky sessions, and pre-warm instances to hide a slow start.

Almost none of that turned out to be the right work, and finding out why is most of what this release is. The protocol removed sessions. The slow start had one cause and deleting it beat the plan’s target fivefold. The state that actually blocks running replicas was mislabelled in the plan as living in SQLite when it is a JSON file.

What shipped: the server starts seven times faster, can be deployed and rolled without dropping requests, and no longer amplifies an outage at its authorization server.

The plan’s centrepiece solves a problem the protocol deleted

Section titled “The plan’s centrepiece solves a problem the protocol deleted”

MCP protocol revision 2026-07-28 — which this server has implemented since v2.3.0 — removed protocol-level sessions:

“Revision 2026-07-28 changed the behavior of Streamable HTTP. Changes included: Removal of the GET stream endpoint. Removal of protocol-level sessions.

“An Mcp-Session-Id header on a request: ignore it, and do not mint or echo session IDs.

There is no session to externalise, no affinity to configure, no store to run. Verified against this server rather than assumed: the modern path is createMcpHandler(..., {legacy: "reject"}), so session state exists only on the legacy 2025-era route.

Had this been built from the plan, the release would have added a database dependency to coordinate state the protocol no longer has.

Cold start was ~1300 ms. Profiling rather than guessing:

src/node/index.mjs 1159 ms <- imports all 505 operation implementations
src/core/operations/index 1132 ms
src/core/Chef.mjs 20 ms <- what tool calls actually use
src/node/lib/tool-catalog.mjs 12 ms <- what tools/list actually uses

88% of startup was one import that nothing on the hot path needs. tools/list is built from OperationConfig.json; every operation call, registry tool and streaming path goes through bakeOnCoreChef.mjs. The Node API was reached from exactly three places: help for cyberchef_search, help for the batch search branch, and bake for recipe execution.

Deferred behind a memoised loader:

before after
launch → first tools/list ~1300 ms 185 ms (mean of 8)
first cyberchef_search 1119 ms, once
subsequent 3 ms

The cost moved from every launch — on stdio, which is how every editor starts this server — to first use of one of three tools. The common case never pays it.

The warm pool made it worse. The plan’s target was “<1 s cold start (with warm pools)”. A background warm-up after connect was implemented and measured:

lazy, no warm-up 186 ms
lazy + background warm 1300 ms
eager (before) 1300 ms

Module loading blocks the event loop, so “in the background” is not something it can be — the warm-up does the same 1.1 s of work in front of the request already queued behind it. Removed. The target was beaten fivefold by deleting the cost rather than hiding it.

Health probes and a drain that loses no requests

Section titled “Health probes and a drain that loses no requests”

Three endpoints on the HTTP transport, unauthenticated because a kubelet probe carries no bearer token, and deliberately uninformative — a status string and nothing else, because an unauthenticated endpoint that reports internal state is a reconnaissance surface.

path 200 when 503 when
/health/startup listener bound still starting
/health/ready serving starting or draining
/health/live always, until exit never

Liveness stays healthy while draining, and that is the point. A liveness failure means restart me; during a drain the server is refusing new traffic while finishing in-flight work, so a liveness probe that tracks readiness gets the pod killed mid-drain. Verified by mutation: making liveness follow readiness fails two tests.

Draining exists because Kubernetes sends SIGTERM and removes the pod from Service endpoints at the same time, and endpoint removal has to propagate first. Closing on SIGTERM drops the requests routed during that window — the deploy looks clean and a fraction of requests fail.

Verified end to end against a real process:

before SIGTERM ready=200 live=200
during drain ready=503 live=200
after exit connection refused
exit code 143 (128+15)

A Helm chart and a Compose file. The chart refuses to render three configurations the server would reject at run time, so they fail at helm template with an explanation rather than as a crashloop:

  • a shared recipe volume across replicas;
  • auth.enabled without auth.resource — the value the token audience is checked against (RFC 8707), so a mismatch rejects every otherwise-valid token and looks like “auth is broken”;
  • tenancy.enabled without auth.enabled.

Two replicas were silently eating each other’s recipes

Section titled “Two replicas were silently eating each other’s recipes”

Saved recipes are a JSON file, so they are per-process. Two replicas sharing one both load, both modify, and both save — and the second commit discarded the first:

A saved. A sees: [ 'saved-by-A' ]
B saved WITHOUT complaint
on disk now: [ 'saved-by-B' ] <- A's recipe is gone

A user saves a recipe, it is accepted, and it is gone. No error, no log line.

The file now carries a generation, checked immediately before the commit, so a stale writer is refused with an error saying what to change. It is a conflict detector, not a lock — there is a window between the check and the commit, and Node has no portable advisory locking. Adding a database to coordinate one JSON document is the wrong trade, which is the same judgement that kept Redis out of this release. A test pins that limitation so it stays part of the contract.

The constraint is documented rather than engineered away: a volume per replica, or a single replica. If you do not use saved recipes, scale freely.

The authorization server could take this one down with it

Section titled “The authorization server could take this one down with it”

fetchJwks cached successes for five minutes and failures not at all, and discoverJwksUri tries two metadata URLs — so an issuer outage turned every incoming request into two outbound ones. None had a deadline, because Node’s fetch has no default timeout: against a black-holed host, each hung until the OS gave up.

20 verifications against a down issuer
before: 40 outbound attempts (2 per request, growing with load)
after: 10 outbound attempts (then the circuit opens; the rest make none)

The shape matters more than the ratio: before, the cost grew with traffic — a stampede against a service already unhealthy, exactly when it can least afford one.

This also wires up CircuitBreaker, which had sat in retry.mjs since v1.5.0 with a full test suite and no caller anywhere in src/. The first disposition here was to leave it alone; measuring the JWKS path is what changed the answer.

Variable Default Meaning
CYBERCHEF_DRAIN_DELAY_MS 5000 After SIGTERM: keep serving this long while readiness fails
CYBERCHEF_DRAIN_TIMEOUT_MS 20000 Then wait this long for in-flight requests

0 is meaningful, not missing: set the delay to 0 where there is no load balancer.

Nothing changes unless you deploy over HTTP. stdio is untouched — it simply starts faster.

Two things to know:

  • First use of cyberchef_search, batch search, or a saved recipe now takes ~1.1 s, once per process. That is the startup cost moved, not added. Everything else is faster.
  • A recipe store shared between replicas will now refuse writes rather than silently losing them. If saves start failing after upgrading, that is this check, and it means the writes were already being lost — give each replica its own CYBERCHEF_RECIPE_STORAGE path.
npm run lint clean
npm run test:mcp 1246 passed (46 files)
helm lint clean; every optional path renders
docker compose config valid

Three properties verified by mutation rather than assumed: removing the tenant-scoping predicates, restoring the eager import, and making liveness track readiness each fail the tests that cover them.