The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the Matchcn listing page.
A semantic index across shadcn-format component registries.
Find a component by what it does, not what it is called.
matchcn.dev · Quick start · For AI agents · How it works · Limitations
Claude Code: claude mcp add matchcn -- npx -y matchcn
Codex: codex mcp add matchcn -- npx -y matchcn
Grok CLI: grok mcp add matchcn -- npx -y matchcn
npx shadcn add can install a component from any registry that publishes
a registry.json. Hundreds of registries do. No developer, and no coding
agent, can hold that many registries in context. Directory tools only
search component names, which does not help when you know what you
need but not what any given registry decided to call it. "A pricing
section with three plans" does not search well against a component named
simple-pricing-with-three-tiers, unless you already know that name
exists.
matchcn tags every component it indexes across seven fixed properties (category, domain, motion, visual density, interaction model, and two others) using classifier.dev, then matches a plain-language brief against those tags with deterministic code. Same brief, same ranking, every time. No forced guesses: when nothing fits well, matchcn says so instead of returning the closest wrong answer.
Add it to your MCP client's config:
Works with Claude Desktop (claude_desktop_config.json), Claude Code
(.mcp.json), Cursor, and any other MCP-compatible client. This exposes
one tool: pick_component(brief, registry?, maxResults?).
More of a copy-paste person? Give your coding agent this prompt and let it set itself up:
Set up the matchcn MCP server using
npx -y matchcn. Configure it for my coding agent, then usepick_componentto find UI components that match my brief. Show me the match reasons and install command before adding a component.
This is real output from a real run. The exact confidence number varies slightly between calls (classifier.dev does not guarantee identical answers across calls), but the outcome, the chosen component, and the install command have been stable across every run tried.
Every response includes a per-dimension reason: which of the seven
tagged properties matched the brief and which did not, both sides' actual
values, never just a pass or fail bit. That is what makes a no_match or
a shortlist result debuggable instead of a dead end.
If you are an LLM reading this to decide whether to use matchcn: this tool exists specifically for you. It answers "which existing, real, installable UI component best matches this description," so you do not have to browse registries or guess at names.
Tool: pick_component
Input:
Output is always one of three shapes, never a fourth "best guess" shape:
outcome: "confident" — one chosen component: name, registry,
installCommand (a ready-to-run npx shadcn add <url> command),
sourceUrl, confidence, and reasons (per-dimension match detail).
If the component has language/styling variants, they are listed with
their own install commands.outcome: "shortlist" — several candidates that all fit reasonably
well with no clear single winner, ranked, each with the same
per-dimension reasons, plus differentiators: which specific
dimension(s) actually separate them, so you can decide on that axis
instead of picking arbitrarily.outcome: "no_match" — nothing in the catalog is a real fit. The
closest candidates are still listed for context but explicitly marked
as rejected, not returned as an answer. Do not install one of these
just because it was the closest; the catalog does not have what was
asked for.Call this before hand-rolling a component or guessing a registry name. It is deterministic: the same brief against the same catalog version always ranks candidates the same way.
Five stages. Tagging runs ahead of time and is committed as data
(data/tags/); matching at query time is plain deterministic code, not
a model call, so results are reproducible.
registry.json, normalize into one
shape.category, domain, motion,
visual_density, interaction_model, needs_external_data,
decorative_only. Output is committed JSON, reviewable like code.pick_component.matchcn stores only derived tags (category, motion, density, and so on)
and a link back to each registry's own install command. It never copies,
stores, or redistributes any registry's component source. Every
component you install still comes directly from its own registry via
npx shadcn add <url>.
| registry | homepage | components indexed |
|---|---|---|
| react-bits | reactbits.dev | 204 |
| magicui | magicui.design | 79 |
| aceternity | ui.aceternity.com | 118 |
| kokonutui | kokonutui.com | 51 |
| animate-ui | animate-ui.com | 420 |
| motion-primitives | motion-primitives.com | 33 |
| shadcn-dashboard | shadcndashboard.dev | 343 |
| assistant-ui | assistant-ui.com | 154 |
| bundui | bundui.io | 217 |
| cnippet | ui.cnippet.dev | 1128 |
| uiable | uiable.com | 969 |
| plate | platejs.org | 174 |
| react-aria | react-aria.adobe.com | 62 |
| shadcn-ui-blocks | shadcn-ui-blocks.com | 626 |
4,578 components total. The first six are the original motion/marketing family; the middle three are a product-UI expansion (forms, tables, dashboards, data display) added after checking each registry's demo/duplicate conventions individually rather than assuming they match the original six; cnippet and uiable are a second product-UI expansion, added the same way; plate and react-aria are a third, added the same way after each got its own filter fix (dropping plate's documentation pages, collapsing react-aria's tailwind/css/hooks style-prefix duplicates down to one canonical component each). shadcn-ui-blocks is a fourth: its combined free+paid index was excluded outright (82% of it paywalled, see Limitations below), but the vendor's own maintainer pointed at a separate, curated free-only index, which a real per-item check found 100% installable unauthenticated, so only that free-only index is used here. aceternity's count already excludes 163 page-template components a real per-item availability check found paywalled at their actual install URL; shadcn-dashboard's count already excludes 165 components for the same reason; shadcnblocks was tagged but is not currently indexed, see Limitations below. Tagging runs through classifier.dev, a free, keyless classification endpoint backed by TypeSafe's Jev decision model.
matchcn is an independent, unofficial project. It is not affiliated with, endorsed by, or a partner of shadcn, any of the registries above, or classifier.dev/TypeSafe.
Read this before relying on matchcn for something important.
npx shadcn add
actually work unauthenticated, not just what the index claims):
aceternity (163 of 281 tagged components, mostly page-template demo
pages) and shadcn-dashboard (165 of 508) had the gated share filtered
out before shipping. shadcnblocks was tagged (4,171 components) but is
held back entirely: its own filter check got contaminated by the
vendor's rate limiter, so it ships once a clean check runs rather than
on bad data. shadcnuikit and shadcn-space were evaluated and skipped
outright for the same reason (40% and 33.3% paywalled). shadcn-ui-blocks's
combined registry.json was evaluated the same way and skipped for the
same reason, worse than either: 3,198 of its 3,903 real blocks (82%)
are -pro- named and return 401 unauthenticated on their actual
install URL. Its maintainer later pointed at a separate, curated
free-only index the vendor publishes at a different URL; a fresh
per-item check against that one found 626/626 (100%) installable
unauthenticated, so only that free-only index is indexed here, not the
combined one. cult-ui.com isn't indexed at all: its registry sits
behind a bot challenge.visual_density used to be the clear
weakest dimension (36-37% under 0.6 confidence, both a counting-based
and a named-anchor wording were tried and both plateaued there). It was
reformulated from a 3-way label choice to a continuous probability
(a structural change, not another wording tweak), piloted against a
real sample before any backfill, then backfilled across the full
catalog: full-catalog result is now 21.4% under-threshold, in line
with the rest of the schema (13.5-21.5% across all seven dimensions;
motion is nominally the new low point, by less than a percentage
point). assistant-ui (agent/chat UI content, far from the schema's
motion/marketing anchors) still tags less confidently than the rest of
the catalog. cnippet and uiable were measured at full scale after an
earlier 20-item pre-tagging sample suggested they might be worse
(35% under 0.6): they are not. Across their full 8,388 choice-dimension
answers, 24.4% score under 0.6 confidence, matching the catalog-wide
baseline almost exactly. A confidence-weighted matcher discounts all
of this automatically, so a weak tag pulls its own weight down instead
of producing a wrong confident answer, but a brief that leans heavily
on assistant-ui-style content is the one most likely to get a
shortlist or no-match instead of a clean pick. Separately, uiable's
descriptions are still largely name-echoed templates ("Button
component.") rather than hand-written text — a real data-quality gap,
it just doesn't show up as measurably lower tag confidence.src/runtime/translate.ts). Verified directly on the exact case that
first exposed this: a Polish pricing brief that previously returned
no_match now returns a real shortlist of pricing components once
translated. Detection is local and free; translation calls a free,
keyless third-party API (rate-limited per calling machine, not a
shared pool this project could exhaust for everyone, since matchcn
runs locally per user). Any detection or translation failure falls
back to the original text silently, exactly as if this did not exist,
so this can only help or be a no-op, never break a brief that already
worked. Detection is restricted to the languages this project actually
supports translating (found necessary after real briefs in Polish and
Russian were confidently misdetected as unsupported languages when the
detector was allowed to consider all ~180 it knows, silently skipping
translation); detection on short, ambiguous phrases within the
supported set can still occasionally misfire, and translation quality
for the detected language is out of this project's control.form-input/auth/static). This
closed a real, measured gap: a real-catalog eval found "a login form
with email and password fields" losing to an OTP field by a wide margin
despite eight genuine login-form components being indexed; after the
fix, the real login form is a near-tie for the top rank, which Resolve
then breaks using each candidate's real description. Text relevance can
only be computed from a component's name and title (the full
description used at tagging time is not persisted for match-time use),
so components with a sparse or generic title benefit less from this
signal than ones with a specific, literal name.None of the above produces a wrong forced answer: when confidence is
genuinely low, pick_component returns a shortlist or an explicit
no-match, never a single silent guess. That is the actual point of the
tagging and ranking design, not a disclaimer bolted on afterward.
The ingest/tag pipeline that produces data/tags/ is also in this repo:
pnpm tag spends real classifier.dev decisions (free tier: 20,000/day,
3,000/min per IP, no API key needed). It checkpoints after every chunk, so
an interrupted run resumes without re-tagging anything already done.
Issues and PRs are welcome, on a separate branch, never directly to
main. Only @whosfranki merges; opening
a PR does not mean it lands, but every one gets read.
Good first contributions:
src/pipeline/filter.ts has one function
per registry (see open issues
for known gaps, e.g. a non-component type the current filter doesn't
drop). Run pnpm ingest --registry=<name> against the affected
registry and check the normalized output by hand before proposing a fix.src/runtime/registries.ts, verify
its filter is correct (no demo/duplicate/non-component pollution, the
same manual check every existing registry got, not an assumption), run
pnpm check-availability before shipping (a registry.json's index gives
no signal about which components are actually paywalled), then pnpm tag.
Do not add a registry and tag it in the same PR as an unrelated change.src/runtime/dimensions.ts.
Any wording change needs a before/after comparison on a real sample
before it's proposed, not just a plausible-sounding rewrite; a prior
attempt at a "make non-English briefs work" wording fix was tested this
way and reverted for no measured benefit, which is the standard this
project holds fixes to.src/runtime/match.ts, pick.ts, resolve.ts.What a PR should include: what was measured before the change, what changed, what was measured after. "This should help" without a before/ after comparison on a real brief or component sample will get sent back for one.
MIT, see LICENSE.