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
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
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.
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.
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.
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.
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.
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.
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.
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
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
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
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.
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.
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.
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.
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
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.