CLAUDE.md@bestax-mcp · git:20261013.d4be1a8 · 2026-10-13 · sha256 84057470e4be326d
CLAUDE.md@bestax-mcp git:20261013.d4be1a8A
Immutable. This exact content never changes and is served at /api/v1/blob/84057470e4be326d.
# 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. A component that renders another component's element (`RENDERS_ELEMENT_OF` in `scripts/lib/api-sources.mjs`) names that component's `get_css_variables` call too, since "none" alone reads as "can't be themed". ## 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.