v3.11.0 release notes
This release executes the v4.2.0 charter and ships as v3.11.0, for the reason v3.10.0
established: the v4 charter numbers are a reading order, not a schedule, and a v4 charter executed
before v4.0.0 lands as a v3 minor. The trigger watch was run first and nothing fired.
draft changelog EMPTY ("Changes since the most recent release will accumulate here.")current revision 2026-07-28, no successorSDK @modelcontextprotocol/{server,node,client} all 2.0.0conformance 0.2.0-alpha.11server.json schema 2025-12-11 still newest; every 2026-* date 404scyberchef_pqc_identify — the nineteenth registry tool
Section titled “cyberchef_pqc_identify — the nineteenth registry tool”Zero of the 504 operations touch ML-KEM, ML-DSA or SLH-DSA. That was measured against
OperationConfig.json rather than assumed: no operation matches kyber, dilithium, sphincs,
ml-kem, ml-dsa, slh-dsa, falcon, lattice or post-quantum. A toolkit that parses RSA,
ECDSA, DSA and EdDSA and not the NIST standards has a gap a user will meet.
The new tool names the parameter set behind a key, signature or ciphertext — ML-KEM (FIPS 203), ML-DSA (FIPS 204), SLH-DSA (FIPS 205), all eighteen sets.
It identifies; it does not implement
Section titled “It identifies; it does not implement”Node’s own crypto generates every one of the eighteen parameter sets from Node 24. This project
has no business shipping its own lattice arithmetic, and the RE-MEASURE ritual’s “search the
ecosystem before you build” step is what surfaced that. It changed the build twice over:
- Every fixture that is supposed to be real is generated by Node at test time, and the OID/size table is asserted against all eighteen parameter sets rather than a sample of six. A hand-written blob tests the parser against its author’s understanding of the format; a generated one tests it against an implementation that has to interoperate. The malformed structures are necessarily hand-assembled — no implementation emits an OID body of half a megabyte — and exist to drive the rejection paths.
- The OID and size table was extracted, not cited. Every value came out of a key Node produced, and then agreed with the published FIPS figures — which is the cross-check, not the source.
What it refuses to claim
Section titled “What it refuses to claim”Given DER — PEM, hex or base64, SPKI or PKCS#8 — it reads the algorithm OID, answers definite, and
reports the OID so the answer can be checked rather than trusted. Given raw bytes it has only a
length, and a length is evidence, not an identification. So it lists every candidate and
declines to choose:
- 1568 bytes is both an ML-KEM-1024 encapsulation key and an ML-KEM-1024 ciphertext. A tool that answered one would be right half the time and confident always.
- A 32-byte SLH-DSA public key is indistinguishable from a SHA-256 digest or an Ed25519 key, and 48 and 64 collide the same way.
- The hash family is not recoverable from a raw signature at all.
SHA2-128sandSHAKE-128ssignatures are both 7856 bytes, and that holds across every SLH-DSA size.
A non-match is reported as a non-match rather than as absence: 999 bytes matching nothing does not mean the input is not post-quantum.
The first extraction was wrong and looked right
Section titled “The first extraction was wrong and looked right”The OID table was first pulled by scanning for the 0x06 tag byte near the start of the SPKI. It
gave correct answers for five of six algorithms, and this for the sixth:
ml-kem-1024 1.8.11.6.9.96.840.1.101.3.4.4.3.3.262.33.0.2348.414852...A 0x06 inside a length field, read as a tag. Five plausible results and one absurd one is a
lucky failure mode — had the sixth also looked plausible, a wrong OID would have shipped.
Replaced with a real DER walk, which agrees with the published values for all eighteen, and pinned
by a test that fails if the byte scan comes back.
The coverage margin v3.10.0 warned about lasted one release
Section titled “The coverage margin v3.10.0 warned about lasted one release”v3.10.0 measured statements at 95.06% against a threshold of 95, wrote down that the margin was thin, and said what to do when it broke: cover the code, do not move the bar.
before 94.99 statements FAILS the 95 thresholdafter 95.15 statements exit 0The deficit was the new tool, and the shape of it is the interesting part: pqc-identify.mjs
measured 91.39% statements against 98.64% lines. Nearly every line ran. What did not run were
the parser’s rejection paths — a truncated TLV, a PKCS#8 version INTEGER with nothing after it,
an AlgorithmIdentifier whose first element is not an OID, an OBJECT IDENTIFIER with an empty body
— and every input_format other than Auto.
Those are precisely the paths where a parser invents an answer, and this parser’s predecessor did
invent one. The tests were owed regardless of the threshold, and writing them found a real defect:
the tool threw a bare Error where every other registry tool throws createInputError, so the
same class of bad input reached a client untyped, without the INVALID_INPUT code or the offending
field. Fixed, and pinned by a test asserting the code rather than the message.
Four gates caught this release, and the one I had not anticipated was the strongest
Section titled “Four gates caught this release, and the one I had not anticipated was the strongest”Adding one tool moved five documented figures — the registry-tool count, the three surface tuples, and coverage. Four gates fired between them, and each named its own fix:
registry-tool-docs 19 expected, 18 found, across 7 documents; 4 inventories missing the nametool-surface-figures table says 42 / 44,493, server serves 43 / 45,963stdio-client-contract a registry tool with no client-driven call fixturetest:coverage statements 94.99% against a threshold of 95The third entry is the one worth recording, because it was the only one not already on my list of
things to update. stdio-client-contract.test.mjs derives its expected fixture
list from the live registry rather than from the fixture table beside it — its own comment
explains that an earlier version compared the table against itself, which is an identity that cannot
fail. That is the only reason a new tool with no client-driven call surfaced at all.
registry-tool-docs earned its keep the same way: it discovers which documents behave as
inventories instead of listing them, and so named docs/guides/commands.md, which no part of the
plan mentioned.
What review found, which is the part worth reading
Section titled “What review found, which is the part worth reading”Both reviewers independently found the same two defects, and both are the failure this tool was written to avoid — a parser that invents an answer — arriving by routes I had not closed.
An OID outside the structure that declares it still produced definite. readTlv bounded every
read against the whole buffer rather than against the parent’s declared end, so an
AlgorithmIdentifier declared empty, with a real OID as its sibling, was identified with full
confidence:
before 300d3000 0609608648016503040401 -> definite / ML-KEM-512after -> noneA partially-decodable input was partially identified. Buffer.from stops at the first thing it
cannot parse and returns the prefix with no error, so 1,312 valid hex bytes followed by garbage
decoded to 1,312 bytes and was reported as a probable ML-DSA-44 public key; "zz" as declared hex
decoded to zero. Hex, base64 and PEM are now validated in full before decoding, and PEM requires
its BEGIN and END labels to match — spliced labels parsed happily before.
Copilot found a third that no fixture could have caught: decodeOid assumed the first
subidentifier was one byte. It is base-128 like every other, and X.690 splits it by range rather
than by division. Every NIST PQC OID happens to encode in one byte, so the tests were blind to it —
but the tool reports the OID for algorithms it does not know, which is exactly where an unusual
arc reaches a user:
2.40 -> "3.0" now 2.402.100.3 -> "3.9.52.3" now 2.100.3And one that was already wrong before this release: the user guide said the round-trip was
9.9x cheaper while tool-catalog.mjs said 9.2x and the truth was 8.9x. The surface gate checks
that multiplier in tool-catalog.mjs and nowhere else, so it had been stale across releases. The
gate now checks the guide too, verified by reintroducing 9.9x and watching it fail — a gate that
covers one occurrence of a claim reads as covering the claim, which is this repository’s
most-repeated defect and the fourth time it has been found inside a check written to prevent it.
Finally, Copilot caught that the chart’s own version should not have moved. Chart.yaml
carries a paragraph beginning “WHY THIS IS NOT BUMPED EVERY RELEASE, since a reviewer has now raised
it twice” — and it was bumped anyway. Reverted to 0.5.8, where v3.9.0 and v3.10.0 also left it.
Verification
Section titled “Verification”npm run lint zero errorsnpm run check:versions every location agreesnpm run check:server-json validates against 2025-12-11npm run check:v4-triggers nothing firednpm test 241/241 Node API, 2289/2289 operationsnpm run test:mcp 1804/1804 across 77 filesnpm run test:coverage exit 0 — 96.11 lines / 89.44 branches / 96.64 functions / 95.15 statementsSurfaces measured, not recorded:
surface tools bytes KBindex 43 45,963 45curated 121 109,209 107all 546 426,367 416index + one operation schema: 47,741 bytes (8.9x cheaper than `all`)The tool costs 1,470 bytes on the index — 43 tools where v3.10.0 served 42. A registry tool has no navigation path, so one that is not listed cannot be called at all; that is why it is in every surface, and why the index grows with each one.
Upgrading
Section titled “Upgrading”Nothing breaks. One new tool is advertised on every surface, so a client that pins an expected tool
count will see 43 rather than 42 on index, 121 on curated and 546 on all.