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
Highlights
Section titled “Highlights”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-Idheader 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.
Startup: ~1300 ms → ~185 ms
Section titled “Startup: ~1300 ms → ~185 ms”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 mssrc/core/Chef.mjs 20 ms <- what tool calls actually usesrc/node/lib/tool-catalog.mjs 12 ms <- what tools/list actually uses88% 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
bakeOnCore → Chef.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 mslazy + background warm 1300 mseager (before) 1300 msModule 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=200during drain ready=503 live=200after exit connection refusedexit code 143 (128+15)Deployment
Section titled “Deployment”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.enabledwithoutauth.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.enabledwithoutauth.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 complainton disk now: [ 'saved-by-B' ] <- A's recipe is goneA 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.
Environment variables
Section titled “Environment variables”| 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.
Upgrading
Section titled “Upgrading”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_STORAGEpath.
Verification
Section titled “Verification”npm run lint cleannpm run test:mcp 1246 passed (46 files)helm lint clean; every optional path rendersdocker compose config validThree 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.