Skip to main content
Glama
DarCas
by DarCas
README.md
# @darcas/refero-design-mcp 
[![Buy me a coffee](https://img.shields.io/badge/buy_me_a_coffee-%E2%9D%A4%EF%B8%8F-FEEBE7?&labelColor=FF0000)](https://www.paypal.com/donate/?hosted_button_id=YZQDE3TEYDBWA)
[![M8ven Score](https://m8ven.ai/badge/mcp/darcas-refero-design-mcp-1077wk)](https://m8ven.ai/mcp/darcas-refero-design-mcp-1077wk?s=readme)

> The underlying data belongs to Refero Design. This is an unofficial client; no
affiliation or endorsement is implied.
 
![Node.js](https://img.shields.io/badge/node.js-%3E%3D22.12-5FA04E?logo=nodedotjs&logoColor=white&style=for-the-badge)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.9-3178C6?style=for-the-badge)](https://www.typescriptlang.org)
[![Tests](https://img.shields.io/badge/tests-125%20passing-brightgreen?style=for-the-badge)](#testing)

![npm](https://img.shields.io/npm/v/@darcas/refero-design-mcp?style=for-the-badge)
![NPM Downloads](https://img.shields.io/npm/dy/%40darcas%2Frefero-design-mcp?style=for-the-badge)

An [MCP](https://modelcontextprotocol.io) server for discovering, searching and
extracting design systems from the public pages of
[styles.refero.design](https://styles.refero.design).

Give an assistant a real design system — palette, type scale, spacing, surfaces,
do/don't guidance — instead of one it invented.

> The package also ships an **Agent Skill**, `refero-design-research`, that teaches
a coding agent to do evidence-based design research with this server before
implementing UI — see [The skill](#the-skill).

---

## Why it is built this way

Two decisions shape everything else.

**The JSON API is not used.** `styles.refero.design/robots.txt` disallows
`/api/`, and the endpoint is gated in practice: it answers `403` to every
non-browser client and `200` to a real browser, which points at TLS fingerprinting
rather than authentication. Spoofing headers would not fix that, and would not be
the right thing to do regardless.

So this server reads only what `Allow: /` permits — the published sitemap and the
public style pages — reachable with a plain `curl` and no pretending to be
anything else. Requests are conditional, concurrency-capped, and sent with a
`User-Agent` that identifies the project.

**Coverage is reported, never implied.** A style page is a few hundred KB and
there are over a thousand of them, so the local index grows on demand. That
makes "I found no matches" ambiguous — it usually means *"I looked at 30 of
1,340"*, not *"nothing exists"*. Every search states its own coverage, and
`refero_index_status` exists so the model can check before concluding a style is
absent.

## Performance

The index is deliberately lazy, so almost every tool call has to answer *"how
much do we actually have?"* before it can answer anything else. Getting that
wrong is the difference between a search that returns in under a millisecond and
one that stalls for seconds.

The store keeps cached summaries **and known-absent ids** in memory, and reads
the cache directory once per session instead of probing a path per style. Both
directions matter: remembering only the hits would leave the larger half of the
catalogue re-read on every call.

| Operation | Before | After | Speed-up |
| --- | ---: | ---: | ---: |
| `refero_search_styles` — rank 1,342 summaries | 15.5 ms | 12.6 ms | 1.2× |
| `refero_index_status` | 27.9 ms | 0.15 ms | **186×** |
| `expand` scan when everything is cached | 49.0 ms | 0.10 ms | **490×** |
| First call in a process | 372 ms | 70 ms | 5.3× |

```
speed-up, log scale

 490x  ████████████████████████████████████████   expand scan, all cached
 186x  ██████████████████████████████████         refero_index_status
 5.3x  ███████████                                first call in a process
 1.2x  █                                          rank 1,342 summaries
```

Two honest notes on those numbers.

**The ranking barely moved, and that is deliberate.** It was measured before
being optimised: at ~13 ms across the whole catalogue it was never the
bottleneck, so the scorer was left alone rather than tuned against a guess.

**The first call in a process is dominated by one sitemap request**, not by the
disk. That fetch is what makes coverage honest, and it is why it happens once
rather than per call.

<sub>Median of 15 runs on an Intel i9-12900F / Linux / Node 22, against a warm
local cache holding 26 of 1,342 published styles. "Before" is the previous
implementation, measured in the same session on the same machine. Your numbers
will differ; the ratio is the point, not the milliseconds.</sub>

## Tools

| Tool | Purpose |
| --- | --- |
| `refero_index_status` | Published styles vs. locally indexed styles |
| `refero_search_styles` | Free-text search over names, north stars, colours, fonts, URLs |
| `refero_match_style` | Ranks styles against a prose design brief |
| `refero_get_design_md` | Renders a style as `design.md`, by section |
| `refero_get_style` | Parsed design system plus measured tokens, as JSON |
| `refero_list_style_ids` | Style UUIDs from the sitemap, without reading any page |

Every tool declares all four MCP behavioural hints — `readOnlyHint: true`,
`destructiveHint: false`, `idempotentHint: true`, `openWorldHint: true`. None of
them mutates the origin, the cache, or anything the caller owns; all of them can
read `styles.refero.design`. Clients that gate on these get an accurate answer
without having to infer it.

A resource, `refero://style/{id}/design.md`, is exposed for clients that prefer
resource reads over tool calls.

### Sections

`refero_get_design_md` takes `sections`, and **honours it**:

```
overview · colors · typography · type_scale · spacing · surfaces
imagery · principles · components · similar · custom
```

Sections with no data are dropped rather than rendered as empty headings.

## Installation

```bash
npm install -g @darcas/refero-design-mcp
```

Installs the `refero-design-mcp` and `refero-design-skill` commands — the
binary names stay unscoped, so client configs stay short.

**Requires Node ≥ 22.12.** Node 20 reached end-of-life on 2026-04-30, so 1.1.0
drops it; npm reports `EBADENGINE` on an older runtime. Everything else about
installing and running is unchanged.

### The skill

The package also ships an **Agent Skill** — `skill/refero-design-research/` —
that teaches a coding agent to do evidence-based design research with the Refero
Design MCP before implementing substantial UI. It is a separate deliverable from
the MCP itself and neither replaces the other: the MCP answers questions about
styles, the skill decides how to ask them.

When the MCP is not installed, the skill can set it up for you — after asking,
and only for this package. The per-client instructions it follows are in
[install-mcp.md](skill/refero-design-research/references/install-mcp.md).

```bash
refero-design-skill install
```

That is the whole command. It detects the agent clients you have, writes one
shared copy under `~/.agents/skills/`, and links each client to it.

```bash
refero-design-skill list                           # what is installed where
refero-design-skill install --client claude,codex  # pick clients
refero-design-skill install --project               # copy into the repository
refero-design-skill install --dry-run              # print the plan, write nothing
refero-design-skill uninstall --client claude      # remove what it installed
```

| Flag | Effect |
| --- | --- |
| `--client <ids>` | Comma-separated ids from `skill/refero-design-research/clients.json`. An unknown id fails and lists the valid ones |
| `--scope global\|project` | Defaults to `global`. Project scope copies into the repository, because a symlink into `$HOME` breaks on another machine and in CI |
| `--global`, `--project` | Shorthands for `--scope global` and `--scope project`, which are what most other installers spell this way |
| `--all` | Every client whose detect directory already exists. Bare `install` does this |
| `--path <dir>` | Escape hatch for an unsupported client or an unusual location |
| `--dry-run` | Print the resolved plan and touch nothing |
| `--force` | Required to overwrite local modifications, and it prints the diff first |

Declare the scope **once**. `--global --project` and `--scope project --global`
both fail: picking one silently would install to the other place while you
believe you chose. A boolean flag given a value (`--force=1`) fails for the same
reason.

Note the two scopes here decide **where the skill files go**. They are unrelated
to the global/local question in [Configuration](#configuration), which decides
where the MCP server entry goes in a client's config.

Global scope links rather than copies, so the skill is one file on disk with
several names pointing at it; project scope copies, so it can be committed.
Links are created as `junction` on Windows, and a skills directory that is
itself a symlink is written *through* rather than replaced.

**There is no `postinstall`.** Nothing is written outside `node_modules` by
installing the package — only an explicit `refero-design-skill install`
writes anything, so upgrade the package and re-run it.

The skill is not affiliated with Refero Design, and it contains no Refero data:
only instructions, plus a tool reference generated from `src/`.

See [skill/refero-design-research/README.md](skill/refero-design-research/README.md)
for what the skill does and who it is for.

### Configuration

Point a client at the package, run on demand through `npx` — no install step,
first run downloads the package:

```jsonc
{
  "mcp": {
    "servers": {
      "refero-design-mcp": {
        "command": ["npx", "-y", "@darcas/refero-design-mcp@1"]
      }
    }
  }
}
```

`-y` suppresses the install prompt, which would otherwise block startup. The
version is pinned to the current major so an unattended `-y` cannot pull a
breaking release. Starts in about 2s on a cold cache, under 1s once cached.

If the package is installed globally (`npm install -g
@darcas/refero-design-mcp`), the entry can call the binary directly instead:

```jsonc
"refero-design-mcp": {
  "command": ["refero-design-mcp"]
}
```

This form works **only** with the global install in place — without it the
client fails to start the server (`exit 127: refero-design-mcp: not found`).
Either form behaves identically once running.

**One trap, for developing this package rather than using it.** Both forms fail
with `exit 127: sh: 1: refero-design-mcp: not found` when the client's working
directory is a checkout of *this* repository, and work everywhere else. Inside
its own checkout the `package.json` matches the package name, so `npx` resolves
the request to the local project instead of the registry — and a project never
links its own bins into `node_modules/.bin`, so `npx` ends up running the bare
binary name, which is not on the PATH. If you hit it, name the package and the
binary separately:

```jsonc
"refero-design-mcp": {
  "command": ["npx", "-y", "-p", "@darcas/refero-design-mcp@1", "refero-design-mcp"]
}
```

No other user hits this: it needs a local `package.json` with this exact name.

The package is scoped but the binary is not, so the command is
`refero-design-mcp` with no `@darcas/` prefix.

The default transport is stdio; see [Remote (Streamable HTTP)](#remote-streamable-http)
to serve it over the network. Diagnostics go to stderr only — stdout carries JSON-RPC
frames and nothing else. A server started by hand (`npm start`) will appear to
hang: it is waiting for a client on stdin, which is correct behaviour.

### Remote (Streamable HTTP)

A second binary, `refero-design-mcp-http`, serves the same tools over
[Streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http)
at `POST /mcp`, so a client that cannot spawn a process can reach it by URL.

```
npx -y -p @darcas/refero-design-mcp refero-design-mcp-http
# listening on http://127.0.0.1:3000/mcp
```

Or with Docker, which keeps the cache in a named volume:

```
docker compose up -d --build
```

Client configuration:

```
{
  "mcp": {
    "servers": {
      "refero": { "type": "remote", "url": "https://mcp.example.com/mcp" }
    }
  }
}
```

- **Stateless.** Each request is self-contained, so there is no session to lose
  on restart. `GET` and `DELETE` on `/mcp` answer `405`.
- **No authentication.** Anyone who can reach the port can use the server, and
  every call can make it fetch from `styles.refero.design`. Keep it on a private
  network, or put a reverse proxy with HTTPS (and whatever access control you
  need) in front. The compose file publishes on loopback for that reason.
- **`GET /health`** returns `{"status":"ok","version":"…"}` for probes.
  `HEAD` answers the same headers with no body.
- **Proxy timeouts.** A call that expands the index can take a few seconds; do
  not set the proxy's read timeout below ~60 s, and disable response buffering
  (`proxy_buffering off` in nginx) so streamed responses arrive as they are sent.

Two settings are specific to this binary and are read by `src/cli-http.ts`, not
`src/config.ts`: `REFERO_HTTP_HOST` (interface to bind, `127.0.0.1` by default,
`0.0.0.0` in the image) and `REFERO_HTTP_PORT` (default `3000`, falls back to
`PORT`). Every variable in the next section applies here too.

### Environment variables

Every setting is optional.

| Variable | Default | Purpose |
| --- | --- | --- |
| `REFERO_CACHE_DIR` | `~/.cache/refero-design-mcp` | On-disk cache location |
| `REFERO_CACHE_TTL_MS` | `604800000` | Cache freshness window (7 days) |
| `REFERO_CONCURRENCY` | `4` | Max simultaneous requests |
| `REFERO_MAX_FETCHES` | `25` | Max page fetches per tool call |
| `REFERO_MAX_RESPONSE_CHARS` | `40000` | Response budget |
| `REFERO_SITE_URL` | `https://styles.refero.design` | Origin base URL |
| `REFERO_STYLES_SITEMAP` | `{site}/sitemaps/styles.xml` | Style index |
| `REFERO_TIMEOUT_MS` | `30000` | Per-request timeout |
| `REFERO_USER_AGENT` | see `src/config.ts` | Request identification |
| `REFERO_VERBOSE` | `false` | Diagnostics to stderr |

Alphabetical, so a variable is found by looking rather than by knowing which
group it sits in. The defaults are the ones `src/config.ts` validates at startup;
`REFERO_MAX_FETCHES`, `REFERO_CONCURRENCY` and `REFERO_TIMEOUT_MS` are the three
worth touching first when you want faster research rather than more of it.

## Example session

```jsonc
// The model calls these in sequence
{
  "name": "refero_index_status",
  "arguments": {}
}
// → "Published styles: 1342 / Indexed locally: 1 / Coverage: <1% (1 styles)"

{
  "name": "refero_match_style",
  "arguments": {
    "brief": "dark, dense dashboard for engineers"
  }
}
// → ranked styles, each with matched terms and the north star that drove the match

{
  "name": "refero_get_design_md",
  "arguments": {
    "style_id": "a73148b9-449b-42cd-9f38-86ef694f500e",
    "sections": ["overview", "colors", "typography"]
  }
}
// → # Apple, with the palette table and type scale
```

## How styles are indexed

```
sitemaps/styles.xml  →  list of ids + lastmod        (small, fetched once)
        ↓
/style/{id}          →  Next.js RSC payload in HTML  (~300 KB, fetched on demand)
        ↓
result.meta / result.raw / result.designSystem         (parsed, validated, cached)
```

The style record is located by `styleId`. A page embeds a dozen or more related
styles that share the same key names, so anchoring on the id is what prevents
returning a neighbour's palette.

The RSC format is internal to Next.js and may change. Every failure surfaces as
a typed error rather than a partially-filled object that looks like a valid
answer.

## Development

```bash
npm install
npm run dev        # watch mode
npm run verify     # typecheck + lint + test + build
```

| Script | Action |
| --- | --- |
| `npm run build` | Compile to `dist/` |
| `npm start` | Run the compiled server |
| `npm run typecheck` | `tsc --noEmit` |
| `npm run lint` | ESLint, type-aware rules |
| `npm test` | Vitest |
| `npm run verify` | All of the above, in order |
| `npm run clean` | Remove `dist/` |
| `npm run deploy` | Test, build, then publish to npm |

## Testing

125 tests across 9 files.

Unit tests run **offline**, against a hand-assembled sample of a real RSC
payload — the fragile parts (id anchoring, brace matching, lazy reference
detection) are exactly the parts worth pinning.

`test/e2e.test.ts` drives the server against the live site and skips itself when
the origin is unreachable. The reachability probe runs at module load rather
than in a hook, because `describe.skipIf` is evaluated during collection: a
probe in `beforeAll` leaves every test silently skipped, which is
indistinguishable from a passing suite.

## Design notes

- **Schemas derived from real payloads, not from documentation.** `spacing.radius`
  is a per-element map (`{cards: "28px", buttons: "9999px"}`), not a string.
  `similar` is `[{business, why}]` on current styles but bare strings on older
  ones. `surfaces` is `{hex, name, level, purpose}`. A `typeScale` size arrives
  as `"17"` on one style and `17` on the next, and is normalised to a string at
  the boundary. Every schema is permissive about type and strict about presence,
  so a partial document degrades instead of throwing away a whole design system.
- **Measured and curated data are both kept.** `result.raw` holds tokens with
  usage frequency and context; `result.designSystem` holds the curated reading.
  Frequency is what separates a signature colour from an incidental one.
- **Truncation respects structure.** Cutting markdown with `slice(0, n)` slices
  code fences in half, which makes the model read broken CSS as if it were the
  design. This server trims on a structural boundary instead, and closes any
  fence it opens, so a shortened document is still valid. Every path that caps a
  response goes through that helper, including `refero_get_design_md`.
- **A malformed `style_id` never costs a request.** Ids are checked against the
  UUID shape before anything reaches the network or the cache directory, so a
  wrong id returns an error naming the tool that lists valid ones instead of a
  bare `404`.
- **Conditional requests everywhere,** so a repeat call usually costs a 304.

## License

This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.

TDQS

A4.1/5.0

Scored across 6 tools

Disambiguation4/5

Search vs match overlap somewhat (both find styles), but descriptions clearly separate keyword matching from qualitative brief matching. The two retrieval tools (get_design_md for prose, get_style for JSON tokens) are explicitly differentiated, and index_status/list_style_ids target distinct metadata concerns.

Naming Consistency5/5

Every tool follows the identical refero_<verb>_<noun> pattern: refero_search_styles, refero_match_style, refero_get_style, refero_list_style_ids, etc. No camelCase or naming deviations; totally predictable.

Tool Count5/5

Six tools is well-scoped for a design-style discovery and retrieval server, with each tool earning its place across discovery, matching, and rendering. No redundancy or bloat.

Completeness4/5

Covers the full read lifecycle: discover ids, search, brief-match, fetch JSON tokens, and render prose. Being a read-only scraper of a public site, no CRUD is needed, though minor gaps (e.g. pagination/batch retrieval) exist.

Maintenance

ActivityMaintained
ResponsivenessNo issues