Components agents can
use without drift.
Tokens are the substrate. MCP is the query surface. Components are where it all gets used — and where most systems leak. The seven-item contract for a component shape that survives a 50-message Cursor session without the agent inventing illegal props, drifting from docs, or copying hex literals into the next file.
01 — The drift problem
Sloppy prop APIs are how agents go off the rails.
In a 50-message Cursor or Claude Code session, the agent reads dozens of files and writes dozens more. Every component it touches becomes a sample for the next generation. If Button accepts variant: string and you used variant="primary" in one file, the agent might decide variant="subtle" is fine in the next. By the end of the session, you have four made-up variants in production code.
The fix isn't docs. Docs help humans; agents prove out via the type system. A component whose prop signature is precisely what it accepts — discriminated unions, required-with-context constraints, JSDoc on every prop — gives the agent a wall it can't accidentally walk through. The compiler becomes the governance layer.
Seven items make up the contract. Most are TypeScript hygiene that you should be doing anyway. Two are AI-specific (MDX docs that are type-checked, registry endpoints for distribution). All seven compound — getting six of them right still leaves the one gap as the failure mode the agent will find.
02 — The seven-item contract
Seven things every AI-ready component does.
Most lists like this are aspirational. This one is the bar: if your Button doesn't do all seven, an agent editing your codebase will find the gap within a single session.
- 01
Variants as discriminated unions
size?: string lets the agent invent 'huge'. size: 'sm' | 'md' | 'lg' rejects it at type-check. Every variant prop should be a union of literal types, never a bare string.
- 02
Required props that are actually required
If a Button needs children OR an aria-label (when icon-only), enforce that with a discriminated union. Don't leave it to docs. The compiler is your contract; the docs are a courtesy.
- 03
Discoverable from the import path
import { Button } from '@your-system/ui'. One entry point. Not '@your-system/ui/button', not '@your-system/ui/dist/components/button/index'. Agents follow conventions; reward that.
- 04
Co-located JSDoc on every prop
The TS LSP surfaces JSDoc to the agent at edit time. /** When to use 'destructive': irreversible actions only — delete, remove, leave. */ tells the agent more than a docs page can.
- 05
Tokens, not literals, in defaults
background: 'var(--color-action-primary)' not background: '#3B82F6'. The default value in your TSX should reference the token. Otherwise the agent sees a literal and treats it as the source of truth.
- 06
An MDX doc with type-checked samples
Agents read MDX. They can't read Storybook screenshots. Every component needs a Component.mdx with working TSX snippets — ideally type-checked so they don't drift from the source.
- 07
A registry endpoint for distribution
shadcn-style: GET /r/button.json returns the component source as JSON. Agents (via MCP or direct fetch) can pull the canonical version into a project without npm-installing your whole library.
03 — Before / after
A real Button refactored against the contract.
The same component. The before version is technically valid TypeScript that passes type-check. The after version is the same component with the seven-item contract applied. Agent perspective: night and day.
Before · open-ended
type ButtonProps = {
variant?: string
size?: string
color?: string
loading?: boolean
children?: React.ReactNode
onClick?: () => void
}
export function Button({
variant = 'primary',
size = 'md',
color = '#3B82F6',
loading,
children,
onClick,
}: ButtonProps) {
return (
<button
style={{ background: color }}
data-variant={variant}
data-size={size}
onClick={onClick}
>
{loading ? '...' : children}
</button>
)
}Agent perspective: "variant is a string, so I can pass anything. color defaults to a hex, so hex values are accepted. children is optional, so an icon-only button might just have an icon and no label." Three failure modes in one signature.
After · contracted
/** Visual treatment. Use 'destructive' for irreversible actions only. */
type Variant = 'primary' | 'secondary' | 'destructive' | 'ghost'
/** Density. 'sm' = compact tables, 'md' = default, 'lg' = hero CTAs. */
type Size = 'sm' | 'md' | 'lg'
type ButtonProps =
// Either has visible children...
| { variant?: Variant; size?: Size; loading?: boolean;
children: React.ReactNode; 'aria-label'?: string }
// ...or is icon-only with an aria-label.
| { variant?: Variant; size?: Size; loading?: boolean;
children?: never; 'aria-label': string; icon: React.ReactNode }
export function Button(props: ButtonProps) {
return (
<button
className={`btn btn-${props.variant ?? 'primary'} size-${props.size ?? 'md'}`}
aria-label={'aria-label' in props ? props['aria-label'] : undefined}
aria-busy={props.loading}
>
{props.loading ? <Spinner /> : 'children' in props
? props.children
: props.icon}
</button>
)
}
/* tokens.css drives the visuals */
.btn-primary { background: var(--color-action-primary); }
.btn-destructive { background: var(--color-action-danger); }Agent perspective: "variant is one of four. size is one of three. I can't pass color — it comes from the variant. If I want an icon-only button, I must pass aria-label and icon — the type-checker rejects anything else. Zero room to invent."
04 — The registry endpoint
Ship components as JSON, not as an npm dependency.
shadcn/ui popularized the pattern: components live at predictable URLs as JSON, consumers fetch them on demand. The agent doesn't need to npm-install your whole library — it can pull the one component it needs, with its dependencies, into the current project. This is the distribution shape that won 2024-25.
The registry shape
// GET /r/button.json
{
"name": "button",
"type": "registry:ui",
"description": "Triggers an action. Variants: primary, secondary, destructive, ghost.",
"dependencies": ["react"],
"registryDependencies": ["spinner"],
"tailwind": {
"config": { /* if extending Tailwind config */ }
},
"files": [
{
"name": "button.tsx",
"content": "/* the full TSX source, escaped */",
"type": "registry:ui"
},
{
"name": "button.mdx",
"content": "/* the type-checked docs, escaped */",
"type": "registry:doc"
}
]
}The endpoints
Consumers (humans or agents) install via npx shadcn add https://your-system.com/r/button.json. The component source lands in their repo — modifiable, type-checked, with no version-pinning headaches. This is the inverse of MUI/Chakra and it's why shadcn won.
05 — Anti-patterns
Four component shapes agents reliably break.
Patterns that look reasonable to a human reviewer but provide no guardrails for an agent. Audit your component library for these — most teams find one or two in every popular component.
Open-ended variant props
Avoid
variant?: stringPrefer
variant: 'primary' | 'secondary' | 'destructive' | 'ghost'The first signature lets the agent invent 'fancy', 'big-red', 'subtle-2'. None of them work; the agent doesn't find out until runtime, by which point three more wrong calls have already happened.
Polymorphic props without constraints
Avoid
as?: keyof JSX.IntrinsicElementsPrefer
as?: 'button' | 'a' | 'span' (or use a Slot pattern)The first lets the agent render a Button as <table>. The second restricts to the three elements that make semantic sense. Polymorphism is fine; unbounded polymorphism is an attractive nuisance.
Docs that drift from source
Avoid
Manually-maintained code blocks in a docs sitePrefer
MDX with imported components and type-checked TSX in <code> blocksHand-maintained docs lag the source by weeks. Agents reading the docs site get stale prop signatures, suggest deprecated variants, and confuse their next 10 generations. Type-checked MDX fails CI when the source changes; docs stay in sync by construction.
Hex literals in component defaults
Avoid
const Button = ({ color = '#3B82F6' }) => ...Prefer
const Button = ({ color = 'var(--color-action-primary)' }) => ...When the agent reads the Button source to mimic it, the default value becomes the example. A literal default teaches the agent to use literals; a token default teaches it to use tokens.
The full stack
You now have the three pillars. Ship them together.
Tokens give the agent answers. MCP serves those answers as a query surface. Components are where the answers get applied — and where bad shapes leak. The three pillars compound: a registry endpoint serving constrained-prop components that reference machine-readable tokens, all queryable via MCP, is the design system shape that owns 2026.