Box Kite's MCP server runs your AI assistant's props through the real styling engine mid-task, and names each value that would render nothing. Below: the mistakes it catches, a worked session with every answer pasted from the server, and what it can't know about your project. It documents @box-kite/mcp 2.2.12.
The examples here are screenshots; they run live in the original article.
The problem
Three props an assistant wrote for a settings row, handed to the server:
check_styles { "props": { "jc": "between", "fontSize": 4, "lineHeight": 1.5 } } Some of these write no CSS, or CSS they almost certainly did not mean. - ❌ `jc` does not accept "between" — no rule and no class name were written. It takes start, end, flex-start, flex-end, center, left, right, space-between, space-around, space-evenly, stretch, safe center, … (17 in all — call `get_props`). - ⚠️ `fontSize` → `.fontSize-4{font-size:0.25rem}` — fontSize={4} is 4px text: fontSize's divider is 16, not the spacing scale's 4, so the number is the pixel size. For 16px write fontSize={16}. - ⚠️ `lineHeight` → `.lineHeight-1\.5{line-height:1.5px}` — lineHeight={1.5} is a 1.5px line: lineHeight is direct pixels, not the unitless multiple CSS also takes. Write the pixels (lineHeight={24} is 24px), lineHeight="font-size" for a multiple of 1, or css={{ lineHeight: 1.5 }} for any other multiple. … And here is what those props draw:
The two numbers typecheck, the build passes, and nothing fails loudly. In development the engine logs a [box-kite] warning once the component renders. A production build says nothing.
Why a server and not a docs file
Box Kite's prop names look like the utility classes you already type, so an assistant brings its habits along. Three of them fail:
- Each prop takes a closed set of values. Anything else writes no rule and no class name, so a typo can't become a broken declaration.
- Numbers mean different units per prop.
p={4}is 1rem (÷4),fontSize={14}is 14px (÷16),b={1}andlineHeight={24}are direct pixels. - HTML attributes go in
props. A top-levelhrefordata-*typechecks and is then dropped.
The package ships a rules file, but an agent reads it once, at the start of a task. It can ask a server at the moment the question comes up. No document can answer "does this exact value work?" either. The server bundles the engine of its own release, and check_styles runs each prop on a fresh engine of its own.
Install
Node 22 or newer. No key, no network. In Claude Code:
claude mcp add box-kite -- npx -y @box-kite/mcp Cursor, VS Code, Codex, Zed and any other stdio client:
{ "mcpServers": { "box-kite": { "command": "npx", "args": ["-y", "@box-kite/mcp"] } } } On connect, the server also hands the model the library's four core facts (the dividers, no style, the element comes from the component, attributes in props), plus "before believing a value, call check_styles".
A settings panel, start to finish
1. Find the component.
search_docs { "query": "toggle switch" } - **Switch** (component) — The APG switch: an on/off control drawn as a track and a thumb. … 2. Read its contract. get_component returns props, parts, keyboard map and ARIA. Two rows change the code:
get_component { "name": "Switch", "include": ["props", "a11y"] } … | `label` | `React.ReactNode` | — | The text beside the switch, rendered inside a label that wraps the input. | | `labelProps` | `BoxProps<'label'>` | — | Styles for the wrapping `<label>`: the row's layout, not the track's own appearance. | … So the row needs no Flex and Span of its own. The label names the switch and makes the whole row clickable.
3. The first draft, written from habit:
import Flex from '@box-kite/react/components/flex'; import { H2, Span } from '@box-kite/react/components/semantics'; import Switch from '@box-kite/react/components/switch'; export function NotificationSettings() { return ( <Flex d="column" p={6} rounded="xl" shadow="md" bgColor="white" color="gray-900" borderColor="gray-150"> <H2 fontSize={5} fontWeight="semibold"> Notifications </H2> <Flex jc="between" ai="center" py={3} borderBottom={1} data-setting="email"> <Span lineHeight={1.5}>Email me about new comments</Span> <Switch name="comments" defaultChecked /> </Flex> </Flex> ); } 4. Check it, one check_styles call per element. The verdicts, summarized:
| Draft | Verdict | Fix |
|---|---|---|
rounded="xl" | ❌ no prop of that name | borderRadius={3} |
borderColor="gray-150" | ❌ not a palette step | "gray-200" |
fontSize={5} | ⚠️ 5px text | fontSize={20} |
fontWeight="semibold" | ❌ takes 100 to 900 | fontWeight={600} |
jc="between" | ❌ not accepted | jc="space-between" |
borderBottom={1} | ❌ no prop; suggests borderColor | bb={1} |
data-setting="email" | ⚠️ HTML attribute, dropped | props={{ 'data-setting': 'email' }} |
lineHeight={1.5} | ⚠️ a 1.5px line | lineHeight={20} |
One of those answers, as the agent sees it:
check_styles { "props": { "jc": "between", "ai": "center", "py": 3, "borderBottom": 1, "data-setting": "email" } } Some of these write no CSS, or CSS they almost certainly did not mean. - ❌ `jc` does not accept "between" — no rule and no class name were written. It takes start, end, flex-start, flex-end, center, left, right, space-between, space-around, space-evenly, stretch, safe center, … (17 in all — call `get_props`). - ✅ `ai` → `.ai-center{align-items:center}` - ✅ `py` → `.py-3{padding-block:0.75rem}` - ❌ `borderBottom` no prop of that name. Try: `borderColor`. - ⚠️ `data-setting` an HTML attribute, not a style prop. Written at the top level it typechecks and is then dropped — it goes in `props={{ "data-setting": … }}`. … TypeScript catches the ❌ rows, one error per element, so it takes a few rounds. It never catches the ⚠️ rows. Two fixes came from elsewhere: search_docs for "rounded corners" returns borderRadius first, but nothing led to bb. Once you know the name, get_props confirms that bb={1} writes border-bottom-width:1px.
5. The second draft. Every call now answers "Every prop writes CSS.":
import Flex from '@box-kite/react/components/flex'; import { H2 } from '@box-kite/react/components/semantics'; import Switch from '@box-kite/react/components/switch'; const row = { width: 'fit', d: 'row-reverse', jc: 'space-between', gap: 3, py: 3, bb: 1, borderColor: 'gray-200', fontSize: 14, lineHeight: 20, } as const; export function NotificationSettings() { return ( <Flex d="column" p={6} borderRadius={3} shadow="md" bgColor="white" color="gray-900" b={1} borderColor="gray-200"> <H2 fontSize={20} fontWeight={600} mb={2}> Notifications </H2> <Switch name="comments" defaultChecked label="Email me about new comments" labelProps={row} props={{ 'data-setting': 'email' }} /> <Switch name="digest" label="Weekly digest" labelProps={row} props={{ 'data-setting': 'digest' }} /> </Flex> ); } d: 'row-reverse' puts the text first, because the label renders the input before it.
To skip the typing altogether, get_blocks has a ready settings panel: get_blocks { "name": "settings-form" } answers with npx shadcn@latest add https://www.box-kite.dev/r/settings-form.json.
Field notes from a production app
From a team running the server on a real app, each checked against the source.
Pin the server next to the library. Each release builds @box-kite/mcp from the same commit, at the same version, with that release's engine inlined. npx -y @box-kite/mcp runs whatever version npx resolves, not the one in your lockfile. So the team installed it as a dev dependency at the library's version (npm install -D @box-kite/mcp@2.2.12) and pointed the project's .mcp.json at its bin:
{ "mcpServers": { "box-kite": { "command": "npx", "args": ["box-kite-mcp"] } } } Your own tokens look wrong to it. The app's colours came from Box.extend():
check_styles { "props": { "bgColor": "ui-primary/20", "borderColor": "ui-primary" } } Some of these write no CSS, or CSS they almost certainly did not mean. - ❌ `bgColor` does not accept "ui-primary/20" — no rule and no class name were written. It takes currentColor, transparent, green, red, blue, gray, black, white, vi, slate-50, slate-100, slate-200, … (326 in all — call `get_props`). … The server can't load your configuration. It reads no config file, environment variable or argument, so read its rejection of your own token as "unknown", not "wrong". What still works:
- Every other prop in the bag is judged on its own, so those verdicts stand.
- Your app's engine knows your tokens. With
Box.extend({ 'ui-primary': '#3355ff' }, {}, {}),bgColor="ui-primary/20"renders with no warning, and the typoui-primry/20gets a[box-kite] … wrote no CSSwarning in development. For your own tokens, the console is the check. - A design that only recolours the components can use
Box.palette(). Itsaccent-*andbase-*tokens are built in, andcheck_stylesaccepts them.
get_blocks goes through the shadcn CLI. The server installs nothing. The CLI copies a block's files into your project as code you own. That suits starting a new page, not editing an existing one. It needs a components.json (setup on the registry page), and installed blocks don't update.
The honest limits
- Values, not layouts.
jc="space-between"passes whether or not the element is a flex container. - Not your project. Your
Box.extend()tokens and props and yourBox.components()designs are invisible to it. - Keyword search. "on/off toggle" didn't find
Switch, and "border bottom" never foundbb. Searching by the CSS property name works best. - Gaps in coverage.
VisuallyHiddenand the SVG elements have noget_componententry, and the tool says so. - It can't make the agent ask. Ask for
check_stylesin your prompt or your rules file.
Try it
Built for AI runs check_styles in your browser on the same engine, before you install anything. Then:
claude mcp add box-kite -- npx -y @box-kite/mcp Originally published on box-kite.dev. Box Kite is open source under MIT — if it is useful to you, a star on GitHub helps other people find it. Install it with npm install @box-kite/react.




