aura-components-mcp 1.7.0 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,40 +1,44 @@
1
- <h1 align="center">aura-components-mcp</h1>
1
+ <h1 align="center">Aura Components MCP</h1>
2
+
3
+ <p align="center"><sub>FREE MCP SERVER · STDIO · ZERO DEPS · MIT</sub></p>
2
4
 
3
5
  <p align="center">
4
6
  <a href="https://www.npmjs.com/package/aura-components-mcp"><img src="https://img.shields.io/npm/v/aura-components-mcp" alt="npm version"></a>
5
7
  <a href="https://www.npmjs.com/package/aura-components-mcp"><img src="https://img.shields.io/npm/dm/aura-components-mcp" alt="npm downloads"></a>
6
8
  <a href="./LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="license: MIT"></a>
7
- <a href="./package.json"><img src="https://img.shields.io/badge/node-%3E%3D18-brightgreen" alt="node >= 18"></a>
9
+ <a href="./package.json"><img src="https://img.shields.io/badge/node-%3E%3D22-brightgreen" alt="node >= 22"></a>
8
10
  <a href="./dist/server.js"><img src="https://img.shields.io/badge/MCP-stdio-blue" alt="MCP stdio"></a>
9
11
  </p>
10
12
 
11
13
  <p align="center"><i>Every free thing on Aura, one call away — no account, no key, no browser.</i></p>
12
14
 
13
15
  <p align="center">
14
- <img src="https://raw.githubusercontent.com/swastiksingh-dev/Aura-Components-MCP/main/assets/site-tour.gif" alt="aura-components-mcp demo: dark WebGL hero, 28 grouped tools, one-minute setup" width="100%">
16
+ <a href="https://raw.githubusercontent.com/swastiksingh-dev/Aura-Components-MCP/main/assets/heart-demo.mp4"><b>▲ WATCH THE 10-SECOND TOUR (MP4) ▲</b></a>
15
17
  </p>
16
18
 
17
19
  > I got tired of opening twenty Aura tabs every time I started a landing page. So I built the MCP server I wanted: every free thing on aura.build, one stdio call away, no account, no key, no browser.
18
20
 
19
21
  <p align="center">
20
22
  <b>2,495</b> free components · <b>192</b> skills · <b>30,688</b> assets · <b>725</b> design systems<br>
21
- <b>29</b> tools · <b>0</b> dependencies · <b>94</b> hermetic tests + <b>35</b> live checks green · version-checked hourly against npm
23
+ <b>29</b> tools · <b>0</b> dependencies · <b>112</b> hermetic tests + <b>38</b> live checks green · version-checked hourly against npm
22
24
  </p>
23
25
 
24
- ---
25
-
26
+ ---
27
+
26
28
 
27
29
  - [Why not the official MCP](#why-i-built-this-instead-of-using-the-official-mcp)
28
30
  - [What is inside](#what-is-inside)
29
31
  - [Install](#install)
30
32
  - [Demo](#demo)
31
- - [The 29 tools](#the-28-tools)
33
+ - [The 29 tools](#the-29-tools)
32
34
  - [Fallback rules](#fallback-rules-read-this-once)
33
35
  - [Prompt for AI agents](#-prompt-for-ai-agents--start-here)
34
36
  - [Free only, no login](#free-only-no-login)
35
37
  - [Develop](#develop)
36
38
  - [Credits](#credits)
37
39
 
40
+ > **v1.7.1** — bug-fix release, no new tools. Central free-only enforcement: Pro components/assets by direct ID are now refused (`-32602`, fail-closed on ambiguous entitlement) instead of returned in full — search, install, bundle, scaffold, and related all inherit it. Search cache isolates explicit `theme` (dark/light no longer share entries). Bundles honor all 8 refs end-to-end with `dropped_refs` disclosure. New `min_views_filtered_out` count (`total` stays upstream). Full icon outages surface as upstream errors, not misses. Upstream hardening: malformed rows disclosed via `malformed_dropped` (single fetch fails loud), non-429 4xx never retried. Node floor raised to maintained releases (engines `>=22`, CI matrix 22.x/24.x). Dead code removed, scaffold merge extracted, verify extended.
41
+
38
42
  > **v1.7.0** — 29 tools: 5 pure `aura_inspo_*` mappers (recommend/components/reference/system/compare) bridge Inspo evidence to Aura calls with zero extra deps — strict macro gate (`need_macro`), palette priority brand > Inspo > Aura, enforced tokens-first, mandated image srcset plan, fast preview-URL look gate — plus 2 pure `aura_docs_*` helpers (plan/check) wiring Context7 fresh docs for tailwind/gsap/lenis/iconify, with `docs_stamp` on every install/scaffold and a look-preserving conflict rule (fresh API wins, look stays identical) — plus live `aura_icon` (Font Awesome Free + HackerNoon pixel SVGs, auto-fallback, CC BY 4.0 credit always) — plus `aura_version` (hourly npm check, prefer-latest notice, verdict inside every `aura_status`). `aura_prompt` now asks 6 questions (+macrostructure, +vibe) and leads Pass 2 with `inspo.recommend`. `aura_scaffold_page` refuses explicit Pro ids (`pro_filtered_out`). Build hardened for CRLF checkouts.
39
43
 
40
44
  <details>
@@ -48,8 +52,8 @@ Point any MCP client at `dist/server.js` and your agent can search 2,495 free co
48
52
 
49
53
  Built by [swastiksingh-dev](https://github.com/swastiksingh-dev). Catalogue content belongs to Aura (aura.build, by Meng To / DesignCode). This project is not affiliated with Aura.
50
54
 
51
- ---
52
-
55
+ ---
56
+
53
57
 
54
58
  Aura already ships an official MCP at `https://mcp.aura.build/mcp`. It is good at what it does: your Canvases, your projects, publishing, all behind OAuth. This server does the other half. It covers the public catalogue, and it skips the parts that slow an agent down: no OAuth dance, no account, no per-project scopes. A few things the official one does not do:
55
59
 
@@ -59,8 +63,8 @@ Aura already ships an official MCP at `https://mcp.aura.build/mcp`. It is good a
59
63
  - `aura_recommend` builds a starter kit across all four surfaces in one parallel call.
60
64
  - Everything defaults to free. Pro rows never sneak into results.
61
65
 
62
- ---
63
-
66
+ ---
67
+
64
68
 
65
69
  - 2,495 free components with HTML/Tailwind source, preview images, tags, and author credit
66
70
  - 192 agent skills with full SKILL.md bodies (GSAP, Tailwind v4, Anime.js, copywriting, and more)
@@ -73,10 +77,10 @@ Aura already ships an official MCP at `https://mcp.aura.build/mcp`. It is good a
73
77
 
74
78
  Start with `aura_status`, then `aura_search_all`. That order matters: status confirms the catalogue is reachable, search_all shows which surface has the best match before you spend calls on details.
75
79
 
76
- ---
77
-
80
+ ---
81
+
78
82
 
79
- Needs Node 18 or newer. No other dependency. Pick one:
83
+ Needs Node 22 or newer. No other dependency. Pick one:
80
84
 
81
85
  ```bash
82
86
  # Option A — from source (GitHub)
@@ -115,18 +119,18 @@ claude mcp add aura-components -- node ./dist/server.js
115
119
  { "$schema": "https://opencode.ai/config.json", "mcp": { "aura-components": { "type": "local", "command": ["node", "./dist/server.js"], "enabled": true } } }
116
120
  ```
117
121
 
118
- ---
119
-
122
+ ---
123
+
120
124
 
121
125
  <p align="center">
122
- <a href="https://raw.githubusercontent.com/swastiksingh-dev/Aura-Components-MCP/main/assets/heart-demo.mp4"><img src="https://raw.githubusercontent.com/swastiksingh-dev/Aura-Components-MCP/main/assets/site-tour.gif" alt="Watch the 10-second flow tour (MP4) — click to play" width="100%"></a>
123
- <br><em>Click the preview to watch the 10-second flow tour (MP4, 1918x936, ~8MB) — same file as <a href="./assets/heart-demo.mp4">assets/heart-demo.mp4</a></em>
126
+ <a href="https://raw.githubusercontent.com/swastiksingh-dev/Aura-Components-MCP/main/assets/heart-demo.mp4"><b>▲ CLICK TO WATCH THE 10-SECOND FLOW TOUR (MP4) ▲</b></a>
127
+ <br><em>Same file as <a href="./assets/heart-demo.mp4">assets/heart-demo.mp4</a></em>
124
128
  </p>
125
129
 
126
130
  *`aura_prompt` turns one sentence into a confirmed prompt, `aura_recommend` turns it into a starter kit with links. Full clip in [`assets/`](assets/).*
127
131
 
128
- ---
129
-
132
+ ---
133
+
130
134
 
131
135
  Start with `aura_status`, then `aura_search_all`. That order matters: status confirms the catalogue is reachable, search_all shows which surface has the best match before you spend calls on details.
132
136
 
@@ -181,16 +185,17 @@ Start with `aura_status`, then `aura_search_all`. That order matters: status con
181
185
 
182
186
  Every row carries `page_url` (open it to see the design) and `author` where Aura credits one.
183
187
 
184
- ---
185
-
188
+ ---
189
+
186
190
 
187
191
  - **Per surface, not global.** `aura_search_all` fans out to four independent pipelines (phrase → OR-tokens → safety net → kind-aware refusal). Mixed outcomes are normal and correct: skills may return `fallback:or-tokens:...` while components return `total:0, no_match:true`. Read `isFallback` / `no_match` on each surface — never assume one flag covers all four.
188
192
  - **Recommend fills the kit.** If components come back `no_match`, `aura_recommend` auto-retries once with `suggested_queries[0]` (`retried:true, retried_from:<query>`), then falls back to human-grade editorial picks (`editorial_fallback:true`). The starter kit is never left empty on a text goal.
189
193
  - **Big code in one call.** Search rows are slim excerpts by design. Skip the 3-chunk manual concat: `aura_get_component {id, full:true}` and `aura_scaffold_page {goal, full:true}` return complete bodies (33,897-char component verified in one call). Default stays chunked to protect context.
190
194
  - **Counts are live upstream.** `aura_status` counts mirror the Aura catalogue right now; `freshness` shows max `updated_at` per surface. Counts only grow when Aura publishes — a freshness bump with flat counts means metadata moved, not new components.
195
+ - **`total` means upstream matches** (for limit/offset paging), not post-filter rows. Local drops are disclosed, never silent: `min_views_filtered_out`, `negative_theme_filtered_out`, `deduped`, bundle `dropped_refs`.
196
+
197
+ ---
191
198
 
192
- ---
193
-
194
199
 
195
200
  > The fastest way to a shippable page. Two passes, one confirmation.
196
201
 
@@ -226,8 +231,8 @@ Sources: [tips-for-prompting](https://www.aura.build/learn/tips-for-prompting),
226
231
  [introduction](https://www.aura.build/learn/introduction),
227
232
  [design-systems](https://www.aura.build/design-systems).
228
233
 
229
- ---
230
-
234
+ ---
235
+
231
236
 
232
237
  New landing page, dark cinematic portfolio, SaaS pricing section. The shape is the same each time:
233
238
 
@@ -239,10 +244,10 @@ New landing page, dark cinematic portfolio, SaaS pricing section. The shape is t
239
244
 
240
245
  Prompts that work well are in [SETUP.md](./SETUP.md#prompts-that-work).
241
246
 
242
- ---
243
-
247
+ ---
248
+
244
249
 
245
- There is no auth in this server. No OAuth, no API key to paste, no account to create. Reads go to the same public catalogue endpoints the Aura website uses. The `premium` flag stays on every component row, free-only is the default, and `aura_install_component` refuses Pro ids instead of guessing. If Aura rotates its public key, set `AURA_SUPABASE_ANON_KEY` and rebuild; nothing else changes.
250
+ There is no auth in this server. No OAuth, no API key to paste, no account to create. Reads go to the same public catalogue endpoints the Aura website uses. The `premium` flag stays on every component row, free-only is the default, and Pro rows are refused everywhere — search filters them, and direct ID lookup, install, bundle, and scaffold refuse them (`-32602`, fail-closed) instead of guessing. If Aura rotates its public key, set `AURA_SUPABASE_ANON_KEY` and rebuild; nothing else changes.
246
251
 
247
252
  Optional tuning (all with defaults):
248
253
 
@@ -260,8 +265,8 @@ Optional tuning (all with defaults):
260
265
 
261
266
  </details>
262
267
 
263
- ---
264
-
268
+ ---
269
+
265
270
 
266
271
  ```bash
267
272
  npm test # hermetic tests, no network
@@ -271,8 +276,8 @@ npm run build # rebundle src/*.mjs into dist/server.js
271
276
 
272
277
  Layout: `src/config.mjs` holds env parsing. `src/http.mjs` hides timeouts and retries behind an injectable fetch. `src/catalog.mjs` is the deep module (queries, cache, shaping, counts). `src/icons.mjs` fetches free icons (FA + pixel lib) with its own 24h cache. `src/guide.mjs` turns rows into install plans plus pure mappers (`buildPrompt`, `combineScaffold`, `buildInspo*`, `buildDocs*` — same seam for tools + tests + docs). `src/tools.mjs` validates args. `src/protocol.mjs` speaks JSON-RPC. `src/server.mjs` only wires stdio.
273
278
 
274
- ---
275
-
279
+ ---
280
+
276
281
 
277
282
  Star it if it saved you a tab or twenty: [github.com/swastiksingh-dev/Aura-Components-MCP](https://github.com/swastiksingh-dev/Aura-Components-MCP). Reposits welcome — see [DIRECTION.md](./DIRECTION.md) for where this is headed.
278
283
 
@@ -285,8 +290,8 @@ npx -y aura-components-mcp@latest
285
290
  https://github.com/swastiksingh-dev/Aura-Components-MCP
286
291
  ```
287
292
 
288
- ---
289
-
293
+ ---
294
+
290
295
 
291
296
  Server code by [swastiksingh-dev](https://github.com/swastiksingh-dev), MIT. Free to fork, use, and sell with. If you fork this, keep the credit line and point people at the original repo.
292
297
 
@@ -300,7 +305,7 @@ Server code by [swastiksingh-dev](https://github.com/swastiksingh-dev), MIT. Fre
300
305
  | [HackerNoon Pixel Icon Library](https://github.com/hackernoon/pixel-icon-library) ([site](https://pixeliconlibrary.com)) | Pixel icons via `aura_icon` (served from jsDelivr) | Icons CC BY 4.0, other files MIT — credit on every hit, enforced by the tool |
301
306
  | Upstream libraries (Tailwind CSS, GSAP, Lenis, Iconify) | The code this server's guides point at | Each project's own license — check before shipping |
302
307
 
303
- ---
304
-
308
+ ---
309
+
305
310
 
306
311
  MIT — see [LICENSE](./LICENSE).
package/SETUP.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Setup
2
2
 
3
- One server, every client below. No account, no key, nothing to log into. If Node 18+ runs `node ./dist/server.js`, the client can use this.
3
+ One server, every client below. No account, no key, nothing to log into. If Node 22+ runs `node ./dist/server.js`, the client can use this.
4
4
 
5
5
  Build once:
6
6
 
@@ -120,7 +120,7 @@ Transport is stdio, no auth. Command `node`, args `["/absolute/path/to/aura-comp
120
120
 
121
121
  ## Troubleshooting
122
122
 
123
- - Server exits instantly: check `node --version` (needs 18+), then `npm run build` again.
123
+ - Server exits instantly: check `node --version` (needs 22+), then `npm run build` again.
124
124
  - Client shows 0 tools: absolute path wrong, or the client needs a restart after config edit.
125
125
  - `upstream` errors on every call: outbound network blocked. `npm run verify` from the same machine tells you in seconds.
126
126
  - Counts look stale: 60-second cache. Wait a minute or set `AURA_CACHE_TTL_MS=0`.
package/dist/server.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // aura-components-mcp v1.7.0 — bundled (zero deps). Built by swastiksingh-dev.
2
+ // aura-components-mcp v1.7.1 — bundled (zero deps). Built by swastiksingh-dev.
3
3
 
4
4
  // ---- config.mjs ----
5
5
  // Module: config — one small interface (loadConfig) over all env parsing.
@@ -30,7 +30,7 @@ function loadConfig(env = process.env) {
30
30
  codeChars: num(env.AURA_CODE_CHARS, 12_000, { min: 1_000, max: 60_000 }),
31
31
  contentChars: num(env.AURA_CONTENT_CHARS, 12_000, { min: 1_000, max: 60_000 }),
32
32
  freeOnlyDefault: bool(env.AURA_FREE_ONLY_DEFAULT, true),
33
- userAgent: env.AURA_USER_AGENT ?? "aura-components-mcp/1.7.0",
33
+ userAgent: env.AURA_USER_AGENT ?? "aura-components-mcp/1.7.1",
34
34
  };
35
35
  }
36
36
 
@@ -59,6 +59,9 @@ function createFetcher({ fetchImpl, timeoutMs, retries, userAgent }) {
59
59
  }
60
60
 
61
61
  // GET JSON over PostgREST. Returns { rows, total } (total from content-range).
62
+ // Retryable: 429/5xx, network errors, timeouts (except AbortError: hung
63
+ // requests are not retried). Non-retryable: other 4xx (invalid requests
64
+ // never succeed on retry) — thrown immediately with noRetry set.
62
65
  async function getJson(url, headers) {
63
66
  let lastErr;
64
67
  for (let attempt = 0; attempt <= retries; attempt++) {
@@ -68,7 +71,9 @@ function createFetcher({ fetchImpl, timeoutMs, retries, userAgent }) {
68
71
  lastErr = new Error("transient upstream status " + res.status);
69
72
  } else if (!res.ok) {
70
73
  const body = await res.text().catch(() => "");
71
- throw new Error("upstream " + res.status + ": " + body.slice(0, 300));
74
+ const e = new Error("upstream " + res.status + ": " + body.slice(0, 300));
75
+ if (res.status >= 400 && res.status < 500) e.noRetry = true;
76
+ throw e;
72
77
  } else {
73
78
  const range = res.headers?.get?.("content-range") ?? null;
74
79
  const total = range && range.includes("/") ? Number(range.split("/").pop()) : null;
@@ -76,8 +81,8 @@ function createFetcher({ fetchImpl, timeoutMs, retries, userAgent }) {
76
81
  return { rows: Array.isArray(rows) ? rows : [rows], total: Number.isFinite(total) ? total : null };
77
82
  }
78
83
  } catch (err) {
84
+ if (err && (err.noRetry || err.name === "AbortError")) throw err;
79
85
  lastErr = err;
80
- if (err && err.name === "AbortError") break; // timeout: retrying rarely helps
81
86
  }
82
87
  if (attempt < retries) await sleep(120 * 2 ** attempt + Math.floor(Math.random() * 60));
83
88
  }
@@ -280,6 +285,39 @@ function slimRows(kind, items) {
280
285
  });
281
286
  }
282
287
 
288
+ // Central access policy: only components/assets carry a premium flag, and a
289
+ // row is free ⟺ premium === false (fail-closed: missing or ambiguous
290
+ // entitlement is refused, never assumed public). Skills/design_systems
291
+ // have no premium concept and are always allowed. Every retrieval path
292
+ // (getItem, and therefore bundle/bulk/scaffold/related/install) funnels
293
+ // through assertFreeRow, so callers cannot reach restricted content by ID.
294
+ function isFreeRow(kind, row) {
295
+ if (kind !== "components" && kind !== "assets") return true;
296
+ return !!row && row.premium === false;
297
+ }
298
+
299
+ function assertFreeRow(kind, row) {
300
+ if (isFreeRow(kind, row)) return;
301
+ const what = kind === "assets" ? "asset" : "component";
302
+ const e = new Error("that " + what + " is Pro (paid). Search with freeOnly or pick a free one.");
303
+ e.code = "BAD_ARGS";
304
+ throw e;
305
+ }
306
+
307
+ // Row-shape gate: the minimum fields every surface needs to build a usable
308
+ // item (identity for page_url + title for display/matching). Rows that fail
309
+ // are upstream schema drift, not empty results: listings disclose them via
310
+ // malformed_dropped, single-record fetch fails loud. Never silently shaped
311
+ // into "undefined" URLs or untitled items.
312
+ function isWellFormedRow(kind, row) {
313
+ if (!row || typeof row !== "object") return false;
314
+ if (typeof row.title !== "string" || !row.title) return false;
315
+ const hasId = row.id !== undefined && row.id !== null;
316
+ const hasSlug = typeof row.slug === "string" && !!row.slug;
317
+ if (kind === "components" || kind === "design_systems") return hasId || hasSlug;
318
+ return hasId;
319
+ }
320
+
283
321
  function pageUrl(kind, row) {
284
322
  if (kind === "components") return SITE + "/component/" + row.slug;
285
323
  if (kind === "skills") return SITE + "/skills/" + row.id;
@@ -364,7 +402,7 @@ function createCatalog({ fetcher, config, now = () => Date.now(), facetsFn = nul
364
402
  const authorCache = new Map(); // created_by -> profile (shared across searches)
365
403
  const inflight = new Map(); // cacheKey -> Promise (coalesce concurrent identical calls)
366
404
 
367
- const clampLimit = (l) => Math.min(config.maxLimit, Math.max(1, l ?? config.defaultLimit));
405
+ const clampLimit = (l) => Math.min(config.maxLimit, Math.max(1, Math.floor(l ?? config.defaultLimit)));
368
406
 
369
407
  async function enrichAuthors(rows) {
370
408
  const ids = [...new Set(rows.map((r) => r.created_by).filter(Boolean).filter((id) => !authorCache.has(id)))];
@@ -384,12 +422,15 @@ function createCatalog({ fetcher, config, now = () => Date.now(), facetsFn = nul
384
422
  const sort = CATALOG_SORTS[kind][q.sort] ? q.sort : "popular";
385
423
  const freeOnly = q.freeOnly ?? config.freeOnlyDefault;
386
424
  // Gap 1: negative_theme (exclude a theme) + min_views (quality floor). Both ride
387
- // the cache key so filtered/unfiltered variants never collide.
425
+ // the cache key so filtered/unfiltered variants never collide. Explicit
426
+ // theme also rides the key: it re-ranks results, so dark/light variants
427
+ // must never share a cache entry or an in-flight request.
428
+ const themeKey = q.theme ? String(q.theme).toLowerCase() : "";
388
429
  const negTheme = q.negative_theme ? String(q.negative_theme).toLowerCase() : "";
389
430
  const minViews = Math.max(0, q.min_views ?? 0);
390
431
  const slim = q.slim ?? true;
391
432
  const exactFlag = q.exact_match === true ? "exact" : "fuzzy";
392
- const key = ["search", kind, q.query ?? "", q.tag ?? "", q.mediaType ?? "", sort, limit, offset, freeOnly, negTheme, minViews, slim ? "slim" : "full", exactFlag].join("|");
433
+ const key = ["search", kind, q.query ?? "", q.tag ?? "", q.mediaType ?? "", sort, limit, offset, freeOnly, themeKey, negTheme, minViews, slim ? "slim" : "full", exactFlag].join("|");
393
434
  const hit = cacheGet(key);
394
435
  if (hit) return { ...hit, cached: true };
395
436
  if (inflight.has(key)) return inflight.get(key);
@@ -490,11 +531,28 @@ function createCatalog({ fetcher, config, now = () => Date.now(), facetsFn = nul
490
531
  } catch { /* snippets are best-effort; rows still return */ }
491
532
  }
492
533
  const enriched = await enrichAuthors(rows);
493
- const out = { kind, total, limit, offset, items: enriched.map((r) => shapeRow(kind, r, config, facetsFn)), cached: false };
534
+ // Schema-drift gate: malformed rows are disclosed, never shaped into
535
+ // untitled items or ".../undefined" page URLs, and never counted as
536
+ // successful empty results (total keeps its upstream meaning).
537
+ const formed = [];
538
+ let malformed = 0;
539
+ for (const r of enriched) {
540
+ if (isWellFormedRow(kind, r)) formed.push(r);
541
+ else malformed++;
542
+ }
543
+ const out = { kind, total, limit, offset, items: formed.map((r) => shapeRow(kind, r, config, facetsFn)), cached: false };
544
+ if (malformed) out.malformed_dropped = malformed;
494
545
  if (deduped) out.deduped = deduped;
495
546
  // Gap 1: min_views quality floor (views-gated surfaces only) + negative_theme
496
547
  // hard exclusion. Both apply before theme re-rank so counts stay honest.
497
- if (minViews > 0) out.items = out.items.filter((it) => (it.views || 0) >= minViews);
548
+ // total keeps its upstream meaning (all matches for limit/offset paging);
549
+ // local drops are disclosed via min_views_filtered_out / deduped /
550
+ // negative_theme_filtered_out instead of rewriting total.
551
+ if (minViews > 0) {
552
+ const beforeViews = out.items.length;
553
+ out.items = out.items.filter((it) => (it.views || 0) >= minViews);
554
+ if (out.items.length < beforeViews) out.min_views_filtered_out = beforeViews - out.items.length;
555
+ }
498
556
  if (negTheme && kind === "components") {
499
557
  const before = out.items.length;
500
558
  out.items = out.items.filter((it) => !it.facets || it.facets.theme !== negTheme);
@@ -580,6 +638,8 @@ function createCatalog({ fetcher, config, now = () => Date.now(), facetsFn = nul
580
638
  const url = base + "/rest/v1/" + kind + "?select=" + encodeURIComponent(sel) + "&" + filt + "&limit=1";
581
639
  const { rows } = await fetcher.getJson(url, headers);
582
640
  if (!rows.length) { const e = new Error("not found: " + kind + " " + idOrSlug); e.code = "NOT_FOUND"; throw e; }
641
+ assertFreeRow(kind, rows[0]); // before enrich/cache: refusals are never cached
642
+ if (!isWellFormedRow(kind, rows[0])) throw new Error("malformed upstream row for " + kind + " " + idOrSlug + ": missing identity/title fields");
583
643
  const enriched = await enrichAuthors(rows);
584
644
  const full = enriched[0];
585
645
  // Chunked fetch: re-slice full blobs per requested chunk so agents can page
@@ -690,13 +750,15 @@ function createCatalog({ fetcher, config, now = () => Date.now(), facetsFn = nul
690
750
  try {
691
751
  const { rows } = await fetcher.getJson(url, headers);
692
752
  const enriched = await enrichAuthors(rows);
693
- return enriched.map((r) => slimRows("components", [shapeRow("components", r, config, facetsFn)])[0]);
753
+ return enriched.filter((r) => isWellFormedRow("components", r)).map((r) => slimRows("components", [shapeRow("components", r, config, facetsFn)])[0]);
694
754
  } catch { return []; }
695
755
  }
696
756
 
697
757
  // Bulk fetch: N details in one parallel round (backs aura_bundle + aura_scaffold_page).
698
- // Per-id errors are captured, never thrown: { ok:true, item } or { ok:false, id, error }.
699
- async function bundleItems(kind, ids, max = 5, opts = {}) {
758
+ // Limit is 8 end-to-end (matches normRefs validation and the 2–8 docs):
759
+ // duplicates collapse to one fetch, order follows the request, and
760
+ // per-id errors are captured, never thrown: { ok:true, item } or { ok:false, id, error }.
761
+ async function bundleItems(kind, ids, max = 8, opts = {}) {
700
762
  const list = [...new Set((ids || []).map(String))].slice(0, Math.max(1, Math.min(max, 8)));
701
763
  const out = await Promise.all(list.map(async (id) => {
702
764
  try { const got = await getItem(kind, id, { full: !!opts.full }); return { ok: true, id, item: got.item }; }
@@ -722,7 +784,8 @@ function createCatalog({ fetcher, config, now = () => Date.now(), facetsFn = nul
722
784
  try {
723
785
  const { rows } = await fetcher.getJson(url, headers);
724
786
  const enriched = await enrichAuthors(rows);
725
- return { item: shapeRow(kind, item, config), related: slimRows(kind, enriched.map((r) => shapeRow(kind, r, config))) };
787
+ const wellFormed = enriched.filter((r) => isWellFormedRow(kind, r));
788
+ return { item: shapeRow(kind, item, config), related: slimRows(kind, wellFormed.map((r) => shapeRow(kind, r, config))) };
726
789
  } catch { return { item: shapeRow(kind, item, config), related: [] }; }
727
790
  }
728
791
 
@@ -730,7 +793,7 @@ function createCatalog({ fetcher, config, now = () => Date.now(), facetsFn = nul
730
793
  }
731
794
 
732
795
  // ---- protocol.mjs ----
733
- const SERVER_INFO = { name: "aura-components-mcp", version: "1.7.0" };
796
+ const SERVER_INFO = { name: "aura-components-mcp", version: "1.7.1" };
734
797
  // Module: protocol — stdio JSON-RPC framing + MCP handshake + error codes.
735
798
  // One interface: createSession(send) -> { dispatch(msg) }. No business logic.
736
799
 
@@ -1484,11 +1547,17 @@ function createIcons({ fetcher, now = () => Date.now() }) {
1484
1547
  if (hit) cache.delete(key);
1485
1548
 
1486
1549
  const tried = [];
1550
+ let lastFetchErr = null;
1487
1551
  for (const c of iconCandidates(raw, source, style)) {
1488
1552
  let body = null;
1489
1553
  try {
1490
1554
  body = await fetcher.getText(c.url, {});
1491
- } catch {
1555
+ } catch (e) {
1556
+ // Network-level failure is not a miss: remember it, keep walking the
1557
+ // chain in case only one host is down, and rethrow at the end if
1558
+ // nothing else explains the failure (never disguise an outage as
1559
+ // "not found").
1560
+ lastFetchErr = e instanceof Error ? e : new Error(String(e));
1492
1561
  tried.push({ url: c.url, ok: false, why: "fetch-error" });
1493
1562
  continue;
1494
1563
  }
@@ -1510,6 +1579,7 @@ function createIcons({ fetcher, now = () => Date.now() }) {
1510
1579
  while (cache.size > ICON_MAX_KEYS) cache.delete(cache.keys().next().value);
1511
1580
  return out;
1512
1581
  }
1582
+ if (lastFetchErr && tried.every((t) => t.why === "fetch-error")) throw lastFetchErr;
1513
1583
  iconMissing("icon not found: '" + raw + "' (" + source + "/" + style + ") — checked " + tried.length + " URLs; check spelling (FA names use dashes, e.g. 'arrow-down')");
1514
1584
  }
1515
1585
 
@@ -1744,7 +1814,7 @@ function createHandlers(catalog, icons, version) {
1744
1814
  aura_install_component: async (a) => { a = a || {}; const id = a.id !== undefined ? a.id : a.slug;
1745
1815
  if (id === undefined || (typeof id !== 'string' && typeof id !== 'number')) toolFn_bad('provide id (number) or slug (string)');
1746
1816
  const got = await catalog.getItem('components', id);
1747
- if (got.item && got.item.premium !== false) toolFn_bad('that component is Pro (paid). Search with freeOnly or pick a free one.');
1817
+ // Pro refusal enforced centrally in catalog.getItem (fail-closed).
1748
1818
  const compInstall = installGuide('components', got.item);
1749
1819
  return textResult({ item: got.item, install: compInstall, docs_stamp: docsStampFor(compInstall.deps) }); },
1750
1820
  aura_install_skill: async (a) => { a = a || {}; if (typeof a.id !== 'string' || !a.id) toolFn_bad('id (string) is required');
@@ -1799,17 +1869,18 @@ function createHandlers(catalog, icons, version) {
1799
1869
  const compRefs = normRefs(a.components, 'components') || [];
1800
1870
  const full = a.full === undefined ? undefined : toolFn_bool(a.full, 'full');
1801
1871
  const fullOpt = full === undefined ? {} : { full };
1802
- const [sys, comps] = await Promise.all([
1872
+ const [sys, bundle] = await Promise.all([
1803
1873
  sysRef ? catalog.getItem('design_systems', sysRef, fullOpt).catch(() => null) : catalog.searchCatalog('design_systems', { query: a.goal, limit: 1 }).then((r) => (r.items[0] ? { item: r.items[0] } : null)),
1804
- compRefs.length ? catalog.bundleItems('components', compRefs, 5, fullOpt).then((rs) => rs.filter((x) => x.ok).map((x) => x.item)) : catalog.searchCatalog('components', { query: a.goal, freeOnly: true, limit: 3 }).then((r) => r.items),
1874
+ compRefs.length ? catalog.bundleItems('components', compRefs, 8, fullOpt).then((rs) => ({ items: rs.filter((x) => x.ok).map((x) => x.item), dropped: rs.filter((x) => !x.ok).length })) : catalog.searchCatalog('components', { query: a.goal, freeOnly: true, limit: 3 }).then((r) => ({ items: r.items, dropped: 0 })),
1805
1875
  ]);
1876
+ const comps = bundle.items;
1806
1877
  // Pro guard: explicit-id bundles can name Pro rows — free flows never ship them.
1807
1878
  const freeComps = (comps || []).filter((c) => c && c.premium === false);
1808
1879
  const proFiltered = (comps || []).length - freeComps.length;
1809
1880
  const sysItem = sys && sys.item ? sys.item : null;
1810
1881
  const toks = sysItem ? tokenHints(sysItem.content || '') : { tokens: {}, css: '' };
1811
1882
  const { guides, combined_deps: deps, files } = combineScaffold(sysItem, freeComps);
1812
- return textResult({ goal: a.goal, order: ['1 tokens.css', '2 system preview_html', '3 component markup in listed order'], tokens_css: toks.css || '', system: sysItem, components: freeComps || [], guides, combined_deps: deps, files, docs_stamp: docsStampFor(deps), ...(proFiltered ? { pro_filtered_out: proFiltered, pro_note: 'Pro (paid) components were refused — free-only scaffold ships free rows.' } : {}), license_note: 'Markup and any pulled assets are all-rights-reserved per Aura Terms §4 — fine to prototype, check each page_url before commercial ship.' }); },
1883
+ return textResult({ goal: a.goal, order: ['1 tokens.css', '2 system preview_html', '3 component markup in listed order'], tokens_css: toks.css || '', system: sysItem, components: freeComps || [], guides, combined_deps: deps, files, docs_stamp: docsStampFor(deps), ...(bundle.dropped ? { dropped_refs: bundle.dropped, dropped_note: 'Some requested refs resolved to missing or Pro (paid) components and were omitted — see per-id errors via aura_bundle.' } : {}), ...(proFiltered ? { pro_filtered_out: proFiltered, pro_note: 'Pro (paid) components were refused — free-only scaffold ships free rows.' } : {}), license_note: 'Markup and any pulled assets are all-rights-reserved per Aura Terms §4 — fine to prototype, check each page_url before commercial ship.' }); },
1813
1884
  };
1814
1885
  };
1815
1886
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aura-components-mcp",
3
- "version": "1.7.0",
3
+ "version": "1.7.1",
4
4
  "description": "Free-only MCP server for the Aura (aura.build) public catalogue: 2,495 components, 192 skills, 30,688 assets, 725 design systems. No login. Built by swastiksingh-dev.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -29,7 +29,7 @@
29
29
  "verify": "node ./scripts/verify.mjs"
30
30
  },
31
31
  "engines": {
32
- "node": ">=18"
32
+ "node": ">=22"
33
33
  },
34
34
  "keywords": [
35
35
  "mcp",