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 +42 -37
- package/SETUP.md +2 -2
- package/dist/server.js +90 -19
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,40 +1,44 @@
|
|
|
1
|
-
<h1 align="center">
|
|
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%
|
|
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
|
-
<
|
|
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>
|
|
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-
|
|
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
|
|
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"><
|
|
123
|
-
<br><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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
699
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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.
|
|
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": ">=
|
|
32
|
+
"node": ">=22"
|
|
33
33
|
},
|
|
34
34
|
"keywords": [
|
|
35
35
|
"mcp",
|