v10.4.1 — Registry ownership
V10.4 added the awareness read. This release is about ownership — proving it, and never again having to remember to claim it.
BrainLLM is now in the MCP Registry as io.github.miisodev/brainllm, and every release from here publishes itself.
The registry entry, and why it took four releases
server.json had been maintained and version-bumped since v10.1 — correct name, correct package, correct environment variables. It had never been published. Nothing was broken; the file simply looked like a completed integration, and a local artifact that looks like a shipped thing is invisible until someone queries the outside world.
Publishing it required proving ownership of the npm package. The registry does not take the claim on faith: it fetches registry.npmjs.org/brainllm/<version> and reads an mcpName field off the published package.json, which must equal the server name.
That has an ordering consequence worth stating, because it is the kind that costs a release to learn: npm versions are immutable, so the proof has to be baked into a version that npm actually serves. v10.4.0 could never have been registered, no matter what was done to the working tree. Hence 10.4.1.
Releases now publish themselves
.github/workflows/publish.yml runs on any v* tag and does everything downstream of it — gates, build, tests, npm, the registry, then verification of both from outside.
No secrets. Both publishes authenticate by OIDC from the workflow's own identity: npm trusted publishing, and mcp-publisher login github-oidc for the registry, which proves the io.github.miisodev/* namespace from the repository owner. npm also attaches SLSA provenance automatically.
The alternative was a granular npm token, and it is worse in a specific way. The default expiry is 30 days, and an expired token fails npm publish with a 404 on PUT rather than a 401 — npm answers unauthenticated writes to an existing package that way so it does not leak whether the package exists. Read literally, it says the package does not exist. That exact message cost time on the day this was set up. Worse, npm publish --dry-run succeeds against a dead token, because it never authenticates.
This closes a failure that had happened twice. Six commits shipped between v10.1.0 and v10.2.0 with npm still serving 10.1.0. Then v10.3.0 shipped complete — docs, landing site, live deploy — while npm, the tags, the releases and the registry all stayed exactly where they were. Both times every local signal was green: clean tree, passing CI, a successful deploy. A push satisfies all of them while leaving untouched every registry a user installs from.
The bug that would have shipped
server.json was invalid against its own declared schema, and had been for four releases: a 259-character description against a 100-character limit. Nothing validated it, so the first signal would have been the registry rejecting the publish — after npm had already published immutably, forcing a 10.4.2 to fix a typo-scale problem.
scripts/validate-release.ts now runs in CI on every push, not at release time. It checks four things, each one a failure that has actually occurred here:
server.jsonagainst the schema it declares — not a pinned one, because the file names its own contract and validating against anything else lets the two drift while still reporting greenpackage.jsonmcpNameequalsserver.jsonname- All four version manifests agree
- The namespace matches the identity the publish will authenticate as
The publish workflow adds a fifth: the tag must equal every manifest. Six version sites across five files have drifted independently more than once.
$schema also moves to 2025-12-11. The two schemas were diffed rather than bumped on faith — no properties added or removed at any level, and packages[].version relaxed from required to optional. Strictly more permissive.
The OAuth pages look like the product now
The consent screen was a generic light/dark card with a purple button, bearing no relationship to the application it authorizes access to. That is not cosmetic: a consent screen is the one page where someone must be certain what they are looking at.
It now carries the landing site's tokens — the #0a0a0f ground and its amber radial glow, Space Grotesk headings, the raised card on a hairline border, a monospace host chip, and the brand's own 3×3 node grid. The error page shares the same shell; it was previously inline styles on a bare body, which made the unbranded page the one users hit when something had gone wrong.
It makes no external requests, deliberately. The landing site pulls its fonts from Google Fonts; this page does not, and that is a decision rather than an omission. A third-party request on a consent screen tells that host someone is authorizing access to their memory, adds a dependency the authorization flow cannot function without, and blocks on exactly the networks most likely to restrict it. The font stacks name the same faces first, so a machine that has them renders identically, and the brand mark is CSS rather than an image for the same reason. A test asserts the page contains no external src or href at all.
Five new tests also pin the security property that was previously only a comment: the screen names the host — the one part of a client's identity DNS and TLS vouch for — and never client_name, which is self-asserted text in a document the requester controls and could just as easily read "Anthropic".
Also
CI discovers test suites by glob instead of naming them. The explicit list meant a new suite sat outside CI until someone remembered to add it, and nothing failed when they didn't — it happened to oauth.test.ts for a full release, and to kinds.test.ts on the day it was written. Both times the omission looked exactly like success: green.
ajv was promoted from a transitive dependency of the MCP SDK to an explicit one. A release gate reached through someone else's dependency tree fails open the moment they reorganise it.
181 tests, tsc --noEmit clean, build clean. 42 core tools, 75 with full mode.