@cssints/mcp

An MCP server for cssints: it lets a coding agent check its CSS the way the build checks it, without running the app, and ask a running app's dev server what its classes are. It runs over stdio (cssints-mcp) and is built on effect/ai McpServer. Logs go to stderr; stdout carries the protocol only.

Tools

Tool Answers Example
check_declaration ok, or the exact message the build gives: the property's grammar, value ranges, the prose rules of the specs { "property": "padding", "value": "-1rem" } → padding: unexpected "-1rem" in "-1rem", expected <length-percentage [0,∞]>
check_support the targets that lack a property or a feature of its value, with the BCD key, by the rule of the build's support warnings (partial_implementation counts as supported); what LightningCSS lowers for them (loweredOf of cssints/check) is supported, under lowered with the value it writes { "property": "textWrap", "value": "balance", "query": "chrome 109" } → css.properties.text-wrap, lacking chrome 109
check_contrast the WCAG 2.x ratio of css.check.contrast, from the engine (contrast of cssints/check; cut, not rounded, at two places), and whether it meets min { "foreground": "#777", "background": "white", "min": 4.5 } → contrast 4.47 is below 4.5
compile a module through the real plugin: its class strings, the CSS it adds to the sheet, the transformed code, errors and warnings, each at its call (file:line:col; a warning is said by every compile) { "file": "src/Button.tsx" }, or { "file": "src/Card.ts", "source": "..." } for text that is not saved
search_docs the matching sections of docs/API.md, README.md and the plugin READMEs: file, line, heading trail, an excerpt { "query": "firstThatWorks fallback" }
list_plugins the plugin packages, their members, what each does, the README {}

On a running dev server

Tool Answers Example
explain_class per class: its declarations, conditions (@media, @container, :hover, ...), layer, plugin or kernel, the tokens it reads with their values, the calls that made it (file:line:col) { "classes": "_1d608he" } → color: #dc2626, :hover, _.b, src/components/Inspect.tsx:25:14
module_styles the css.* calls of one file as the dev server compiled them, each with its classes and their CSS { "file": "src/components/Button.tsx" }
dev_errors the build errors Vite sends to its clients now (message, file:line:col, plugin, code frame); what is not sent (warnings, the page's runtime errors) is named {} → padding: unexpected "-1rem" at src/bad.ts:4:20

They take url, the app's dev server (default http://localhost:5173), which must be running. explain_class and module_styles need cssints({ devtools: true }) in the app's vite.config (else they say so): they ask for the table the devtools overlay reads (cssints:devtools:get over Vite's HMR websocket, one socket per url for the session) and keep it until the server says it is stale. The server knows only the modules it has served, so the first call fetches every module the page reaches (dynamic imports too), and again after an edit: no browser has to be open. dev_errors works on any Vite dev server: it fetches the modules again and collects Vite's error payloads.

In a browser

Tool Answers Example
inspect_element what the devtools overlay shows: the element's classes by the call that made them, per property the winner, what it overrides (and why), what does not apply now, and the check against the computed style { "selector": "[data-case=hover-md]", "hover": true } → color: #dc2626 from ._1d608he:hover beats #2563eb by specificity, ✓
why_property one property: the winner and its call, its conditions and whether each holds, what it beats and why, what does not apply, ✓/✗ with the reason { "selector": "[data-case=unlayered]", "property": "color" } → ✗, .mine { … } in src/components/inspect.css beats it

They open url (default http://localhost:5173) in a headless Chromium of playwright-core, one page per url kept for the session, read the table from the dev server as the live tools do (so cssints({ devtools: true }) again; server when the server is not the page's origin), add the devtools core to the page (@cssints/devtools/inject, bundled by Vite on the first call) and run the overlay's cascade on the first element the selector matches. hover and focus put the element in that state first (else the pointer is moved away and the focus dropped); width and height size the viewport. They need a Chromium: npx playwright-core install chromium, else an installed Chrome is used.

check_support without query reads the project's browserslist (package.json "browserslist" or .browserslistrc, found from root, else the working directory), as the plugin reads it from its Vite root; compile's warnings are those of the root's targets. compile starts a Vite dev server in middleware mode for the project root (the root argument, else the working directory) on its first call and reuses it: the project's vite.config is loaded when it has one (its aliases, its cssints() options), else the plugin alone; the project's own vite and cssints are used when installed. Each call reads the module and what it imports again.

Installed in a project, search_docs reads node_modules/cssints/docs/API.md (the published cssints holds a copy, made by its prepack), the README of cssints and those of @cssints/*, and list_plugins those READMEs; run from this repository, they read docs/API.md, README.md and every package README.

Install

bun add -d @cssints/mcp   # or: npm i -D @cssints/mcp

The client starts the server in the project's directory, so the working directory is the project root.

opencode

opencode.json:

{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "cssints": {
      "type": "local",
      "command": ["npx", "cssints-mcp"],
      "enabled": true
    }
  }
}

Claude Code

claude mcp add cssints -- npx cssints-mcp

(--scope project writes it to .mcp.json for the whole team.)

Cursor

.cursor/mcp.json:

{
  "mcpServers": {
    "cssints": {
      "command": "npx",
      "args": ["cssints-mcp"]
    }
  }
}

In this repository, use node packages/mcp/src/bin.ts as the command (Node 22.18 or later runs the TypeScript sources).

Credits

The server speaks the Model Context Protocol and is built on effect/ai. The checks are those of the build: mdn-data and browser-compat-data (both CC0-1.0), through @cssints/css-grammar.

Development

cd packages/mcp
bun run test        # test/tools.mjs: every tool through an in-process MCP client; test/stdio.mjs: the bin over stdio;
                    # test/live.mjs: the live tools on apps/demo's dev server and on test/fixtures/app
bun run test:browser  # test/browser.mjs: inspect_element and why_property on the demo in Chromium (needs one)
bun run typecheck