CSS is Awesome

MCP server

cia ships a Model Context Protocol stdio server at mcp/server.cjs. Any MCP-aware client — Claude Code, Cursor, Aider, Gemini, Copilot — can connect to it and discover cia’s entire surface without grep-walking the repo. Pure filesystem scan; no database, no build step.

Setup

Recommended — zero install. Use the dedicated css-is-awesome-mcp package. It depends on css-is-awesome and always resolves your installed version’s real source, and the MCP SDK ships as a real dependency — no separate install step:

{
  "mcpServers": {
    "css-is-awesome": {
      "command": "npx",
      "args": ["css-is-awesome-mcp"]
    }
  }
}

No install command needed — npx fetches and caches the package the first time your MCP client runs it. Prefer a pinned version in your own lockfile instead? Install it like any other dependency:

npm install css-is-awesome-mcp

npx then runs the locally installed copy instead of fetching one.

Alternative — the copy already in your node_modules. cia’s own package ships mcp/server.cjs too, for anyone who’d rather not add a second package. This copy needs its SDK installed manually first, since it’s an optional peer dependency (so a plain CSS-only install never pulls JS in):

npm install -D @modelcontextprotocol/sdk zod
{
  "mcpServers": {
    "css-is-awesome": {
      "command": "node",
      "args": ["node_modules/css-is-awesome/mcp/server.cjs"]
    }
  }
}

Tools — 35 total: 28 across 8 families + 7 specialty tools

Every tool returns structured JSON. list_* tools return catalogs; get_* tools return a single record; search_* tools run keyword search across the catalog and return ranked matches.

FamilyToolsWhat it surfaces
Themeslist_themes, get_theme, search_themesAll 24 shipped themes (eight families × auto / light / dark) with full token assignments + raw SCSS
Mixinslist_mixins, get_mixin, search_mixins162 public mixins (core, layout, animation, icons, generator + per-component) with signature, doc, body, line range
Functionslist_functions, get_function, search_functions26 public @functions (color, space, radius, shadow, font-size, z, …)
Tokenslist_tokens, get_token, search_tokens127 required + 49 optional contract tokens. get_token returns sample values across themes plus the list of mixins/functions that reference it
Animationslist_animations, get_animation12-slug vocabulary, 3 speeds, 4 hover effects, the animate / animate-on mixin records
Componentslist_components, get_component, search_components10 component files (accordion, buttons, copy-button, data, feedback, forms, navigation, overlay, stepper, tabs) with all their mixins
Recipeslist_recipes, get_recipeRecipes under scss/recipes/ — both markdown pattern recipes (dialog, combobox, print-to-pdf, print-spec, letterhead, mobile-nav, bottom-nav) and opt-in SCSS recipes (e.g. bare-tags, consumed via @use)
Docsread_llm_txt, read_changelog, read_migration, read_theming, read_agents, read_contract, read_three_tiers, read_readme, read_versioningFull markdown bodies of every top-level doc

assemble_prompt

The headline tool. Pass an intent string and get back a ready-to-paste context block tailored for the next AI generation call. Bundles the relevant mixins, tokens, and docs into one prompt — no need to call N separate get_* tools.

Recognized intents:

resolve_size

Snap a design px value to cia's 4px geometric grid. The other AI-facing tool besides assemble_prompt. Returns the step number, the SCSS call to emit (cia.grid(n) when exactly on-grid; cia.px(value) when off-grid), the equivalent rem, and a human-readable note.

Contract for AI agents: call this whenever you receive a px value from a design tool (Figma, mockup, screenshot) and need to express it in cia code. NEVER write raw rem/px literals when a cia function applies. See the composition decision tree for the full rules.

// AI receives "24px button height" from a Figma design
resolve_size({ px: 24 })
// →  { step: 6, exact: true, rem: 1.5, scssCall: "cia.grid(6)", ... }
//
// AI emits in generated SCSS:
//   .my-btn { height: cia.grid(6); }

// Off-grid case: AI receives "17px hero margin"
resolve_size({ px: 17 })
// →  { step: 4, exact: false, rem: 1, scssCall: "cia.px(17)",
//      alternative: "cia.grid(4)  // snaps to 16px (1rem)", ... }
//
// AI follows the note: prefer the snapped grid value unless the
// design intent specifically requires the off-grid value.

validate_theme

Validate ANY theme CSS against cia’s real token contract and WCAG contrast audit — the same check npm run validate-themes runs, exposed as a tool call instead of a shell command. Not scoped to cia’s own shipped themes: pass a fully custom theme you (or another agent) just built — for example, the output of the derive-theme:<base> assemble_prompt intent above — and get back a real pass/fail before you write it anywhere.

Pass compiled CSS (a :root or [data-theme="..."] block) — this does not compile Sass, so give it the output, not .scss source.

validate_theme({ css: theCssYouJustBuilt, label: "boilerplatev2" })
// →  { ok: false, mode: "consolidated",
//      themes: [{ name: "boilerplatev2", ok: false,
//        missing: ["--space-unit", "--space-0"], a11y: [...] }] }
//
// ok: false means required tokens are missing, or an a11y pair failed —
// fix and re-validate before writing the file anywhere.

theme_from_tokens

Turn a design-tokens file into a complete, validated cia theme in one call. Accepts DTCG v2025.10 ($value / $type leaves, {aliases} resolved), Tokens Studio for Figma exports (value / type leaves, single set or multi-set with $metadata.tokenSetOrder), or a flat { "--token": value } map. The format is auto-detected.

There is no minimum content. Every required contract token the file does not supply is inherited from a shipped base theme (default boilerplate) and listed in report.inherited, so the output is always contract-complete; paths that are not cia tokens are emitted verbatim and listed in report.unmapped, never dropped. Pass dark (same format), or a single file with paired color-light / color-dark groups, and the differing colours become light-dark(). The result carries the same validation object validate_theme returns, run on the CSS before you write it anywhere. Same function as npx cia theme from-tokens, and reachable in-process as handlers.theme_from_tokens.

theme_from_tokens({
  name: "acme",
  tokens: {                                   // DTCG v2025.10
    color: { brand: { primary: { $value: "#3a5fcd" } },
             text:  { primary: { $value: "#0f172a" } } },
    space: { "4": { $type: "dimension", $value: { value: 16, unit: "px" } } }
  }
})
// →  { css: ':root, :root[data-theme="acme"] { color-scheme: light; --action-primary-active: …',
//      report: { format: "dtcg", base: "boilerplate", darkMode: false,
//                fromTokens: ["--brand-primary", "--space-4", "--text-primary"],
//                inherited: [ …the other 124 required tokens… ], unmapped: [] },
//      validation: { ok: true, mode: "consolidated", themes: [ … ], a11ySummary: { fail: 0, … } } }

Path rules: an explicit table covers Figma-style names that do not spell the cia token (typography.font.body → --font-sans); otherwise a leading color. is stripped, common group names are rewritten (spacing. → space., border-radius. → radius., zIndex. → z.), and the segments are joined with - — so space.4 → --space-4 and brand.primary → --brand-primary. Full contract in the theme authoring guide.

get_token_map

The mapping theme_from_tokens applies, as data, so a second implementation (a boilerplate registry, an inventory builder, another agent) maps the same source token the same way without re-deriving the rules. Without path it returns the whole map: the explicit table, the prefix rewrites (as regex sources), the generic rule in one paragraph, and the contract’s required and optional token lists. With path it returns how that one name resolves, plus the contract’s view of the target. Same data as npx cia theme map --json; reachable in-process as handlers.get_token_map.

get_token_map({ path: "spacing.4" })
// →  { path: "spacing.4", token: "--space-4", mapped: true, via: "generic",
//      status: "required", required: true, feature: null, category: "space" }

get_token_map({ path: "typography.size.lg" })
// →  { token: "--typography-size-lg", mapped: false, via: "passthrough", status: null, … }
//     (cia's type scale is a Sass map, not tokens — the path is carried verbatim)

get_token_map()
// →  { generatorVersion, contractVersion: "1.2",
//      explicit: { "typography.font.body": "--font-sans", …115 entries },
//      aliases: [{ pattern: "^colors?\.", replaceWith: "" }, …20],
//      genericRule: "If a path is not in explicit …",
//      targets: { required: [ …127 ], optional: [ …41 ] } }

fix_theme

Upgrade a theme that still uses a deprecated token. cia deprecates rather than deletes, so the old declaration keeps resolving — but there is a better name now, and this rewrites it. Only the property moves: values, comments, ordering and whitespace survive byte-for-byte, so applying it cannot change how the theme renders. A block that already declares the replacement is reported as a conflict and left alone, because choosing between two values is a judgement call.

It never writes to disk. It returns text and the caller decides — the same contract as theme_from_tokens. The result carries applied: false precisely so an agent cannot report “I updated your theme” when nothing was saved. Same function as npx cia fix-theme; reachable in-process as handlers.fix_theme.

fix_theme({ css: ":root { --background-hero: #eee; }" })
// →  { css: ":root { --page-hero-bg: #eee; }",
//      changes: [{ line: 1, from: "--background-hero", to: "--page-hero-bg",
//                  kind: "rewrite", reason: "…deprecated since contract 1.3…" }],
//      unchanged: false, applied: false,
//      summary: "1 declaration(s) renamed. Write the returned css yourself…" }

fix_theme({ css: ":root { --page-hero-bg: #eee; }" })
// →  { unchanged: true, changes: [], summary: "No deprecated tokens found …" }

status

The cheapest call on the server, about forty tokens: which version answered, how big the public surface is, and a hash over every public name. Call it first. If the hash matches the one you saw last time, nothing you cached from list_* has changed, so there is no reason to page through those catalogues again. Only names go into the hash, not bodies — a doc-comment edit is not a surface change.

store is always "disk": this server reads cia's files and nothing else, so there is no database to be unavailable. The same field sits on every list_* and search_* envelope, for callers that treat several MCP servers alike. Reachable in-process as handlers.status.

status()
// →  { version: "1.21.0", cia: "1.21.0", contractVersion: "1.3",
//      counts: { mixins: 161, functions: 26, tokens: 176, themes: 24,
//                components: 10, recipes: 33, animations: 12 },
//      hash: "af41168486d63069", store: "disk",
//      takenAt: "2026-10-16T19:57:03.639Z" }

Security + portability

Discovery is pure filesystem scan. The server reads files inside the cia package directory and never writes anything. Safe to run from any clone or installed node_modules/css-is-awesome/. No network, no telemetry, no eval. The MCP transport is stdio — your client connects over a pipe, not a port.

Versioning

Two ways to run this server, two versioning stories. The in-repo copy (mcp/server.cjs) ships inside the cia package itself, so its serverInfo.version is cia’s own version — when cia’s mixin API changes, it reports the new API on the next read, no separate semver, no client config to update.

The dedicated css-is-awesome-mcp package tracks its own, independent release cadence instead (packaging fixes don’t need a cia release, and vice versa) — its serverInfo.version reports its own version, not cia’s. It always resolves whatever version of css-is-awesome you actually have installed as a real dependency, so the mixin data it serves is never stale even though the version number reported is a different one.

Theme