git:20261003.882497d to git:20261003.1437727

2 added, 2 removed. Audit A to A.

# bestax-mcp
The first-party MCP server for `@allxsmith/bestax-bulma`. Published unscoped as
`bestax-mcp`, alongside `create-bestax` and `bestax-migrate`.
It answers questions about the library — components, props, examples, CSS
variables — and ships the library's _opinions_ as MCP prompts, one per Agent
Skill. Run it with `npx bestax-mcp`.
## Everything in `data/` is generated
`pnpm gen:mcp` (root) runs `scripts/gen-mcp-index.mjs`, which reuses the same
extraction the API docs use — `scripts/lib/props-extract.mjs` in
`markdown: false` mode, `scss-vars.mjs`, `api-sources.mjs` — plus the
hand-written `## Usage` / `## Accessibility` sections of the API pages.
- **Never hand-edit `data/`.** CI fails on staleness (`pnpm gen:mcp:check`).
- `data/catalog.json` and `data/components/*.json` are **committed**.
- `data/bulma-classes.json` is **committed** too, and comes from somewhere else:
bestax-migrate's `src/sources/bulma-classes/class-map.ts`, which the generator
imports directly (node strips its types). `lookup_bulma_classes` answers from
it by following that source's planner class by class, and
`src/__tests__/bulma-classes-agree.test.ts` imports the planner itself to hold
the two together, so a change to either side of that table shows up here.
- `data/skills/` is **gitignored** and synced from `/skills` by
`scripts/sync-skills.mjs`, which runs on `build`, `test`, `test:coverage`,
`test:watch` and `prepack`. Only the manifest (`data/skills.json`) is
committed — enough for the staleness gate to catch a new or renamed skill,
without putting a second copy of ~390 KB of markdown in every skill diff.
Several Turbo tasks can invoke that script at the same time, so it takes a lock
and no-ops when the tree already matches the source; keep both properties if
you touch it, and see its trailing "Concurrency" note for why the freshness
check is required rather than merely an optimisation.
- `gen:mcp:check` runs `git add --intent-to-add` before diffing. The output is a
directory, and `git diff --exit-code` cannot see an untracked file — a newly
added component would otherwise pass the gate.
A library change that moves props means `pnpm gen` in the same PR, exactly like
the API pages and the skill catalog.
## Conventions
- **Responses are markdown, not JSON.** The consumer is a language model: a prop
table costs fewer tokens than the same data as nested objects, and it does not
tempt the model into echoing JSON structure into the code it writes.
- **Progressive disclosure.** `search_bestax` and `list_components` are cheap and
every result names the tool to call next. Full prop tables, usage examples and
skill references are separate calls. A tool that returns everything up front
spends the context the client needs for the actual task.
- **No hardcoded roster.** Prompts, resources and skill lookups are all derived
from `data/skills.json`, which the generator builds by reading the `skills/`
directory. The rule repo-wide is derive, don't list (#540); the prose copies
that cannot be derived are held to the directory by the `skills-roster`
conformance check (`pnpm check:conformance --only=skills-roster`). Do not add
a hand-maintained copy here for that check to chase.
- **Nothing on stdout but JSON-RPC.** stdout _is_ the transport. Diagnostics go
to stderr — a stray `console.log` surfaces to the user as an unexplained
disconnect.
- **Version mismatch is reported, not hidden.** The index is pinned to the
`bestax-bulma` release it was generated from; the server resolves the version
actually installed in the user's project and appends a warning on minor or
major drift. Patch drift stays silent on purpose — a note on every response is
a note a model learns to skip. `BESTAX_MCP_NO_VERSION_CHECK=1` disables it.
- **Unhelpful answers are bugs.** A missing component suggests near misses by
edit distance; a bad dot-path lists the real ones; a component with no CSS
variables says so rather than returning blank. An empty response reads as a
failed lookup and costs a retry.
## Dependencies
`@modelcontextprotocol/sdk` and `zod` are the ones that are imported (the
SDK's schema types are zod, and pnpm's isolated linker means anything imported
must be declared). The SDK pulls a large tree of transitive packages, most of them
for HTTP transports this server does not use. That footprint was weighed against
what the server needs before adopting it, and `pnpm audit --audit-level=high`
comes back clean on the result. Re-check both if that ever changes.
`@allxsmith/bestax-bulma` is declared, not imported (#644): the server is built
for the library and its manifest says so, the way bulma-ui declares `bulma`.
It is spelled `workspace:^`, which `pnpm publish` rewrites to the release
current at pack time (bulma-ui releases first in the same job), and the sibling
rule in `check:conformance` allows it only because `SIBLING_RUNTIME_DEPS`
declares this exact pair. Two things follow. `npx bestax-mcp` installs the
library, `bulma`, and — npm's automatic peer install — `react`/`react-dom`
alongside the server. And the version probe in `src/version.ts` walks up from
`cwd`, so under `npx` the server's own copy in the npx cache is never on that
path; installed as a project devDependency instead, npm may hoist this
dependency's copy to the project's top-level `node_modules`, and when the
project has no copy of its own the probe reports that one.
## Tests
`pnpm --filter bestax-mcp test`. Thresholds 95% / 78% branches, matching
`create-bestax`.
Tests drive the real protocol through the SDK's `InMemoryTransport` rather than
calling handlers directly: the contract that matters is what a client sees, and
that includes schema validation and the tool/resource/prompt listings. A handler
test would pass while the server advertised nothing.
They run against the committed index, not fixtures. The index is the product; a
test that stubs it proves nothing about what ships.
## Releases
Independent semantic-release keyed off the `bestax-mcp` commit scope
(`release.config.js`, tag `bestax-mcp@x.y.z`). It publishes with
`pnpm publish`, like every package here (#532) — the command, its flags, and the
ways that publish fails quietly are documented in `VERSIONING.md` and
`scripts/lib/pnpm-publish.mjs`.
MCP directories and lists describe what the server offers and give `npx -y bestax-mcp@1` as
- its command. A change to either, to how it starts, or a new major means updating the listings
- that `docs/docs/guides/distribution.md` names under "What goes stale".
+ its command. A change to either, or a new major, means updating the Manual listings in
+ `docs/docs/guides/distribution.md` ("What goes stale").
Its `prepack` runs the guard and then `scripts/sync-skills.mjs`, which fills
`data/skills/` at pack time. That directory is gitignored while the manifest
`data/skills.json` is committed, so packing locally does not dirty the tree —
which also keeps it clear of the post-release restamp step in `ci.yml`, whose
diff check is scoped to `bestax-mcp/data`.
`files` carries a `"!data/.sync-skills"` negation, and it is not cosmetic.
`files` ships all of `data/`, and `sync-skills.mjs` keeps its fingerprint and
lock state in `data/.sync-skills/`, so without the negation that build state
ships as product.
**This is not a `pnpm publish` behaviour, and an earlier version of this note
said it was.** npm and pnpm both include dot-directories under a `files`
entry; a synthetic package with `files: ["data"]` and `data/.state/x` packs
identically under either. The published 1.0.0 tarball has no `.sync-skills`
only because the state directory did not exist yet — it arrived with #520 on
2026-08-14, two days after 1.0.0 shipped. So this was a latent bug that the
next release would have hit whoever packed it, surfaced by diffing a
`pnpm -C bestax-mcp pack` against the published tarball while moving
publishers (#532). That diff is the check worth repeating whenever `files` or
the sync script changes, because nothing asserts tarball contents.
### MCP Registry
Each release is also published to the official MCP Registry as
`io.github.allxsmith/bestax-mcp`, by `.github/workflows/mcp-registry.yml` (on
`release: published`, or by dispatch with a tag). The listing is `server.json`
in this directory. The workflow reads it from `main`, writes the release
version in, and publishes it with `mcp-publisher` over GitHub OIDC; the logic
is `scripts/mcp-registry-publish.mjs`.
- **`mcpName` in `package.json` is load-bearing.** The registry proves
ownership by reading it from the published version on npm, and requires it
to equal `server.json`'s `name`. Drop or rename it and every later release
becomes unlistable.
- **Both `version` fields in `server.json` stay `0.0.0-set-from-release-tag`.**
semantic-release bumps `package.json` and nothing else, so a real version
here would go stale; the workflow writes the release version into the
runner's copy only. `scripts/mcp-registry-publish.test.mjs` fails CI on any
other value, and holds the rest of the file to `package.json` and the
schema's description limit.
- **`server.json` does not ship.** `files` does not name it, and the registry
never looks in the tarball.