css-sota-mcp
Tells agents what CSS you can actually ship today — Baseline status, per-browser support, stylesheet audits and UX reviews — from live webstatus.dev and MDN browser-compat-data.
Tài liệu
css-sota-mcp
An MCP server that answers what CSS you can actually ship today — from live Baseline data and MDN browser-compat-data, not from a model's training set.
Agents are confidently wrong about browser support. They will tell you anchor-name is fine, or
that :has() needs a polyfill, depending on when their weights were frozen. This server replaces
the guess with the current answer.
- Endpoint —
https://css-sota-mcp.lusrodri.workers.dev/mcp(Streamable HTTP, no auth) - Docs — css-sota-mcp.pages.dev
- Registry —
io.github.LuSrodri/css-sota-mcp, listed in the official MCP Registry
Tools
| Tool | Answers | Source |
|---|---|---|
search_css_features | "Which features exist for this, and are they safe yet?" | webstatus.dev |
whats_new | "What can I start using that I couldn't before?" | webstatus.dev |
get_feature | "Tell me everything about this one feature." | webstatus.dev + mdn/content |
check_support | "Which browser versions support this exactly?" | bundled browser-compat-data |
audit_css | "Does this stylesheet work for my users?" | bundled browser-compat-data |
dont_make_me_think | "How should this UI be designed — and is this page any good?" | bundled UX guidelines |
check_support and audit_css answer with no network call at all — the data they need is compiled
into the Worker.
dont_make_me_think
Named after Steve Krug's rule: a page should be self-evident. Two modes.
mode: "guidelines" returns the principles to design against — Nielsen's 10 heuristics, Hick's
and Fitts's laws, WCAG 2.2, neurodiversity-inclusive design, motion and microinteractions
(including when Lottie or Rive earn their bundle cost), SVG craft and animation, light-first
theming, lightness, responsiveness. Filter with topic. The knowledge base is
mcp/src/data/ux-guidelines.json; every principle carries its
rationale, actionable rules and a source.
mode: "review" checks HTML and CSS — or a fetched url — and reports what violates which
principle, with the line and the evidence.
It reads source; it does not render it. A Worker has no layout engine, so the review cannot
measure computed contrast, real target sizes, or where focus actually lands. It catches what is
visible in the markup: missing alt, blocked zoom, animation with no reduced-motion path, a
removed focus ring, a dark-only palette, vague link text, a nav past Hick's range. A clean result
is a floor, not a pass, and the tool says so in its own output.
audit_css targets
Two target styles, because they answer different questions:
- A Baseline level —
baseline-widely,baseline-newly. Asks "is this interoperable enough to ship?", judged againstweb-features' Baseline status. - An explicit browser list —
chrome 120, safari 17.4, firefox 128. Asks "does this work for my users?", judged against per-browser versions.
Browserslist queries (last 2 versions, >0.5%) are not accepted. Resolving them needs usage
data this server does not carry, and approximating them would produce confidently wrong audits —
exactly the failure mode the server exists to fix. The tool says so rather than guessing.
Connect
claude mcp add --scope user --transport http css-sota https://css-sota-mcp.lusrodri.workers.dev/mcp
--scope user registers it once for every project on the machine. Leave it out and the server is
added to the current project only.
Claude Desktop
Remote servers go in through Connectors, not through claude_desktop_config.json — that file only
takes local stdio servers. Open Settings → Connectors → Add custom connector and paste:
https://css-sota-mcp.lusrodri.workers.dev/mcp
The endpoint is unauthenticated, so the connector asks for no client id and no secret.
Cloudflare AI Playground
Open playground.ai.cloudflare.com, paste the endpoint into the MCP server field, and connect. The six tools appear immediately.
MCP Inspector
npx @modelcontextprotocol/inspector@latest
Set transport to Streamable HTTP and connect to the endpoint.
Limits on the hosted endpoint
The endpoint is public and unauthenticated on purpose: every tool is read-only over public datasets, so there is nothing to protect from disclosure. What is worth protecting is the account's request budget and the server's standing with the upstreams it proxies.
| Limit | Value | On exceeding |
|---|---|---|
| Requests per client IP | 120 / minute, per Cloudflare location | 429 with Retry-After: 60 |
| Request body | 1 MB | 413 |
audit_css source | 400 000 characters | schema validation error |
120/minute is sized against real usage rather than a round number: an agent working through a task calls a handful of tools per turn, so a burst of twenty is unremarkable and 120 leaves room for a shared address running several clients.
Cloudflare's own guidance prefers keying rate limits on a user or tenant id rather than an IP, since an IP can be shared behind NAT or a privacy relay. This endpoint has no authentication and so no such id; the limit is set generously enough that the trade is a fair one.
If you expect sustained traffic above this, run your own instance — the whole thing is one Worker and deploys in a minute.
Layout
mcp/ The MCP server — a Cloudflare Worker
landing/ Documentation site — Vite, on Cloudflare Pages
How the data is put together
@mdn/browser-compat-data unpacks to ~20 MB, far past a Worker's bundle budget. At build time
mcp/scripts/build-data.js extracts the CSS slice of it plus the web-features catalog, drops
every field the server never reads, and encodes per-browser support positionally. The result is
about 1 MB of JSON — 120 KB gzipped — which ships inside the Worker.
The generated files are gitignored. Every build, test and deploy regenerates them, so the data always matches whatever version npm resolved.
Two details worth knowing, both found the hard way:
web-featuresencodes Baseline as"high"/"low"/false, while api.webstatus.dev and all Baseline documentation saywidely/newly/limited. The build normalises to the latter so the two halves of the server never disagree.- MDN reorganised its CSS reference under
Web/CSS/Reference/…. Compat data records the slug a page had when the entry was written, so building a raw GitHub path frommdn_url404s.get_featureresolves the canonical slug through MDN first, then reads the source.
Development
npm install
npm run dev --workspace mcp # wrangler dev on :8787
npm test --workspace mcp # vitest
npm run typecheck # both workspaces
node mcp/scripts/smoke.js # real MCP protocol call against :8787
node mcp/scripts/smoke.js <url> # ...or against a deployment
smoke.js speaks the 2025-era Streamable HTTP flow — the same one the AI Playground and MCP
Inspector use — so a passing run means those clients will work too.
Deploy
Pushing to main deploys both. Cloudflare builds from this repo directly — no API token is
stored in GitHub, and Cloudflare issues its own build credential.
| Target | Product | Root | Build | Deploy |
|---|---|---|---|---|
| Worker | Workers Builds | mcp | npm run build:data | npx wrangler deploy |
| Landing | Pages Git integration | landing | npm run build | output dist |
The Worker's build command is not optional: mcp/src/data/generated/ is gitignored, and
src/data/index.ts imports it statically, so a build that skips it fails to bundle.
Because the two are independent products, neither waits for the other. .github/workflows/verify.yml
covers that gap — it smoke-tests the live endpoint on a schedule and on demand.
Publishing to the registry
server.json is the registry's record of this server. Because the server is remote,
it carries a remotes entry pointing at the Worker rather than a packages one — there is no
artifact to install, and so no package-ownership marker to place anywhere.
.github/workflows/publish-mcp.yml republishes it on a v* tag:
git tag v0.2.0 && git push origin v0.2.0
The tag sets the version, so server.json's own value is only a fallback for a manual
workflow_dispatch run. The job authenticates with OIDC — proving it runs in this repository is
what grants the io.github.LuSrodri/* namespace — so there is no token stored in GitHub, matching
how the rest of this repo deploys.
It deliberately does not run on every push. The registry record points at a URL, not at a build, so it stays correct across deploys; only a metadata change needs a new version.
Previewing a pull request
Every push to a PR branch uploads a Worker version and builds the landing site, each reachable before merge:
| URL | |
|---|---|
| Worker, by branch | https://<branch-with-dashes>-css-sota-mcp.lusrodri.workers.dev/mcp |
| Worker, by version | https://<version-prefix>-css-sota-mcp.lusrodri.workers.dev/mcp |
| Landing | https://<deployment-id>.css-sota-mcp.pages.dev |
The branch alias is the useful one — it stays put as you push. A branch named
fix/thing becomes fix-thing-css-sota-mcp.lusrodri.workers.dev. Point the
AI Playground or MCP Inspector at it to try a PR's server
for real; node mcp/scripts/smoke.js <url>/mcp works against it too.
A landing preview calls its own branch's Worker, not production. landing/vite.config.ts
derives the alias from CF_PAGES_BRANCH at build time, so a PR touching both halves is previewed
as a matched pair. Without this the preview would show a new front end against the old server —
green preview, broken on merge. An explicit VITE_MCP_ORIGIN still wins, and production builds
fall through to the default.
The alias is derived rather than looked up, so a mismatch points the demo at a URL that 404s. That fails visibly: the endpoint is printed on the page and the hero reports it could not reach the server. The Pages build log prints the wiring on every preview build.
One caveat remains: no automatic PR comment. Cloudflare normally posts the preview links on the
pull request; this account cannot enable that (12044: This account does not have access to Workers Previews). The URLs work — you construct them from the branch name.
To deploy by hand instead:
npm run deploy --workspace mcp # Worker
npm run deploy --workspace landing # Pages
Both need Cloudflare credentials — either wrangler login, or CLOUDFLARE_API_TOKEN and
CLOUDFLARE_ACCOUNT_ID in the environment. Note that wrangler login needs a real terminal; in a
non-interactive shell it refuses and asks for the token variable instead.
Account prerequisites
Cloudflare gates Workers behind these, and the errors only surface at deploy time:
- Workers enabled on the account. Until the Workers & Pages dashboard has been opened once,
every Workers API call fails with
10034: You need to verify your email address to use Workers— which is misleading, since a verified email does not clear it. Opening the page does. - A
workers.devsubdomain, if you want a*.workers.devURL. Absent one, the API answers10007. - The Cloudflare GitHub App installed, for Git-based deploys. Without it the repository
connection API answers
8000008, regardless of account permissions.
Built with
MCP TypeScript SDK v2 · Cloudflare Workers · webstatus.dev · @mdn/browser-compat-data · web-features
The server uses createMcpHandler, which returns a web-standard { fetch } object and serves
requests statelessly — so there is no Durable Object, no KV, and no session affinity. Any isolate
can answer any request.
License
MIT