CLAUDE.md@bestax-mcp · git:20261010.618e483 · 2026-10-10 · sha256 5ca352e5d67d23e7

CLAUDE.md@bestax-mcp git:20261010.618e483A

Immutable. This exact content never changes and is served at /api/v1/blob/5ca352e5d67d23e7.

# 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. The copy itself is
  `scripts/lib/sync-skills.mjs` at the repo root, shared with create-bestax.
  Keep both properties if you touch either file, and see the shared file's
  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. Finding no install is said once, on the first
  answer, since it is also what a server started outside the project looks like;
  `BESTAX_MCP_PROJECT_DIR` points the probe at the project for a client that
  does that. `BESTAX_MCP_NO_VERSION_CHECK=1` disables the check.
- **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` (or `BESTAX_MCP_PROJECT_DIR`), 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

Released by semantic-release, keyed off the `bestax-mcp` commit scope (`release.config.js`, tag
`bestax-mcp@x.y.z`), and patched with every bulma-ui release so the index ships with the
library (VERSIONING.md, "bestax-mcp releases with bestax-bulma"). 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`.

The release job regenerates `data/` after bulma-ui's release and before this package's, and
the publish packs that working tree, so the index a release ships is stamped with the
`bestax-bulma` version released in the same run (#932). The release commit does not take the
change; the job's last step commits it to main. `ci.yml` has the details, at "Regenerate the MCP
index".

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".

The `bestax` plugin in allxsmith/bestax-skills builds its launch config from `server.json`,
read and checked by `scripts/mcp-registry-publish.mjs`: the one npm package over stdio becomes
`npx -y <identifier>@<version>`, with the version from `package.json`, each time
`skills-publish.yml` regenerates it, which it does after every release. Its README lists the
package's `environmentVariables` and the skills in `data/skills.json`, so both stay in step with
this package. The generator fails if `server.json` stops describing an npm package over stdio, or
adds launch arguments or a required variable the plugin cannot carry. The README's prose
(`plugin/README.md`) says what the server does over the network and what npx downloads, so a
change to either updates it in the same PR.

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 MCP index restamp in `ci.yml`, whose diff check
and commit are 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.