aura-components-mcp 1.5.2
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/LICENSE +21 -0
- package/README.md +128 -0
- package/SETUP.md +118 -0
- package/dist/server.js +786 -0
- package/flow/app.js +40 -0
- package/flow/b-compare.html +1 -0
- package/flow/b-docs.html +1 -0
- package/flow/b-how.html +1 -0
- package/flow/b-index.html +1 -0
- package/flow/b-setup.html +4 -0
- package/flow/b-tools.html +1 -0
- package/flow/compare.html +26 -0
- package/flow/docs.html +26 -0
- package/flow/hero-c.html +1 -0
- package/flow/how.html +26 -0
- package/flow/index.html +26 -0
- package/flow/logo-c.html +1 -0
- package/flow/setup.html +29 -0
- package/flow/shell-a.html +7 -0
- package/flow/shell-b.html +7 -0
- package/flow/shell-c.html +12 -0
- package/flow/styles.css +87 -0
- package/flow/tools.html +26 -0
- package/package.json +53 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,786 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// aura-components-mcp v1.0.0 — bundled (zero deps). Built by swastiksingh-dev.
|
|
3
|
+
|
|
4
|
+
// ---- config.mjs ----
|
|
5
|
+
// Module: config — one small interface (loadConfig) over all env parsing.
|
|
6
|
+
// Callers learn one function; env names, defaults, and clamping live here.
|
|
7
|
+
|
|
8
|
+
const num = (raw, fallback, { min = 1, max = 100 } = {}) => {
|
|
9
|
+
const n = Number.parseInt(String(raw ?? ""), 10);
|
|
10
|
+
if (!Number.isFinite(n)) return fallback;
|
|
11
|
+
return Math.min(max, Math.max(min, n));
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const bool = (raw, fallback) => {
|
|
15
|
+
if (raw === undefined) return fallback;
|
|
16
|
+
return /^(1|true|yes|on)$/i.test(String(raw));
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function loadConfig(env = process.env) {
|
|
20
|
+
const defaultKey =
|
|
21
|
+
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImhvaXJxcmtkZ2JtdnB3dXR3dXdqIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDM2Nzc2NTAsImV4cCI6MjA1OTI1MzY1MH0._UsCSHsTELn7m54tOhX3ySm67WEhcyHAPbuxEQZsl3c";
|
|
22
|
+
return {
|
|
23
|
+
supabaseUrl: env.AURA_SUPABASE_URL ?? "https://hoirqrkdgbmvpwutwuwj.supabase.co",
|
|
24
|
+
anonKey: env.AURA_SUPABASE_ANON_KEY ?? defaultKey,
|
|
25
|
+
timeoutMs: num(env.AURA_TIMEOUT_MS, 12_000, { min: 1_000, max: 60_000 }),
|
|
26
|
+
retries: num(env.AURA_RETRIES, 2, { min: 0, max: 5 }),
|
|
27
|
+
cacheTtlMs: num(env.AURA_CACHE_TTL_MS, 60_000, { min: 0, max: 3_600_000 }),
|
|
28
|
+
defaultLimit: num(env.AURA_DEFAULT_LIMIT, 10, { min: 1, max: 50 }),
|
|
29
|
+
maxLimit: num(env.AURA_MAX_LIMIT, 25, { min: 1, max: 50 }),
|
|
30
|
+
codeChars: num(env.AURA_CODE_CHARS, 12_000, { min: 1_000, max: 60_000 }),
|
|
31
|
+
contentChars: num(env.AURA_CONTENT_CHARS, 12_000, { min: 1_000, max: 60_000 }),
|
|
32
|
+
freeOnlyDefault: bool(env.AURA_FREE_ONLY_DEFAULT, true),
|
|
33
|
+
userAgent: env.AURA_USER_AGENT ?? "aura-components-mcp/1.0.0",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ---- http.mjs ----
|
|
38
|
+
// Module: http — one small interface (createFetcher) hiding timeout,
|
|
39
|
+
// retry-with-backoff, error mapping, and the injectable fetch seam.
|
|
40
|
+
// Tests inject a stub fetch; production passes globalThis.fetch.
|
|
41
|
+
|
|
42
|
+
function createFetcher({ fetchImpl, timeoutMs, retries, userAgent }) {
|
|
43
|
+
if (typeof fetchImpl !== "function") throw new Error("fetchImpl must be a function");
|
|
44
|
+
|
|
45
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
46
|
+
|
|
47
|
+
async function once(url, init) {
|
|
48
|
+
const ctrl = new AbortController();
|
|
49
|
+
const t = setTimeout(() => ctrl.abort(new Error("timeout after " + timeoutMs + "ms")), timeoutMs);
|
|
50
|
+
try {
|
|
51
|
+
return await fetchImpl(url, {
|
|
52
|
+
...init,
|
|
53
|
+
signal: ctrl.signal,
|
|
54
|
+
headers: { "user-agent": userAgent, ...(init?.headers ?? {}) },
|
|
55
|
+
});
|
|
56
|
+
} finally {
|
|
57
|
+
clearTimeout(t);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// GET JSON over PostgREST. Returns { rows, total } (total from content-range).
|
|
62
|
+
async function getJson(url, headers) {
|
|
63
|
+
let lastErr;
|
|
64
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
65
|
+
try {
|
|
66
|
+
const res = await once(url, { headers });
|
|
67
|
+
if (res.status === 429 || (res.status >= 500 && res.status < 600)) {
|
|
68
|
+
lastErr = new Error("transient upstream status " + res.status);
|
|
69
|
+
} else if (!res.ok) {
|
|
70
|
+
const body = await res.text().catch(() => "");
|
|
71
|
+
throw new Error("upstream " + res.status + ": " + body.slice(0, 300));
|
|
72
|
+
} else {
|
|
73
|
+
const range = res.headers?.get?.("content-range") ?? null;
|
|
74
|
+
const total = range && range.includes("/") ? Number(range.split("/").pop()) : null;
|
|
75
|
+
const rows = await res.json();
|
|
76
|
+
return { rows: Array.isArray(rows) ? rows : [rows], total: Number.isFinite(total) ? total : null };
|
|
77
|
+
}
|
|
78
|
+
} catch (err) {
|
|
79
|
+
lastErr = err;
|
|
80
|
+
if (err && err.name === "AbortError") break; // timeout: retrying rarely helps
|
|
81
|
+
}
|
|
82
|
+
if (attempt < retries) await sleep(120 * 2 ** attempt + Math.floor(Math.random() * 60));
|
|
83
|
+
}
|
|
84
|
+
throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { getJson };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---- catalog.mjs ----
|
|
91
|
+
// Module: catalog — the deep module. Small interface (searchCatalog,
|
|
92
|
+
// getItem, getStatus) over query building, caching, truncation, author
|
|
93
|
+
// enrichment, and page-URL derivation. Adapters: PostgREST via fetcher.
|
|
94
|
+
|
|
95
|
+
const SITE = "https://www.aura.build";
|
|
96
|
+
|
|
97
|
+
const CATALOG_SORTS = {
|
|
98
|
+
components: { popular: "views.desc", recent: "created_at.desc", trending: "views.desc", updated: "updated_at.desc" },
|
|
99
|
+
skills: { popular: "views.desc", recent: "created_at.desc", trending: "views.desc", updated: "updated_at.desc" },
|
|
100
|
+
assets: { popular: "views.desc", recent: "created_at.desc", trending: "views.desc", updated: "updated_at.desc" },
|
|
101
|
+
design_systems: { popular: "views.desc", recent: "created_at.desc", trending: "views.desc", updated: "updated_at.desc" },
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const LIST_COLS = {
|
|
105
|
+
// Lists exclude code (13KB/row): theme falls back to background + tags heuristics.
|
|
106
|
+
// Full code loads only on get/bundle, where facets recompute exactly.
|
|
107
|
+
components: "id,title,description,tags,premium,views,forks,slug,background,created_by,created_at,updated_at",
|
|
108
|
+
skills: "id,title,description,source_url,views,forks,featured,created_by,created_at,updated_at",
|
|
109
|
+
assets: "id,title,description,keywords,media_type,premium,views,forks,image_800w,video_url,video_poster_url,created_by,created_at",
|
|
110
|
+
design_systems: "id,slug,title,description,views,forks,featured,created_by,created_at,updated_at",
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const DETAIL_COLS = {
|
|
114
|
+
components: "id,title,description,tags,code,premium,views,forks,slug,image_url,background,credit_name,credit_url,created_by,created_at,updated_at",
|
|
115
|
+
skills: "id,title,description,content,source_url,views,forks,featured,created_by,created_at,updated_at",
|
|
116
|
+
assets: "id,title,description,keywords,resolution,colors,media_type,premium,views,forks,image_320w,image_800w,image_1600w,image_3840w,image_original,image_url,video_url,video_poster_url,video_duration,slug,created_by,created_at,updated_at",
|
|
117
|
+
design_systems: "id,slug,title,description,content,preview_html,thumbnail_url,source_name,views,forks,featured,created_by,created_at,updated_at",
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const esc = (s) => String(s).replaceAll('"', '""');
|
|
121
|
+
const ilike = (v) => "*" + String(v).replaceAll("*", "").replaceAll(",", " ").trim() + "*";
|
|
122
|
+
|
|
123
|
+
// Split a goal sentence into significant tokens for OR fallback (stop-word filtered).
|
|
124
|
+
const STOP = new Set("a,an,the,for,with,and,or,of,to,in,on,my,new,free,dark,also,that,this,from,into,plus,vs,top,best,up".split(","));
|
|
125
|
+
function queryTokens(q, max = 4) {
|
|
126
|
+
return String(q ?? "").toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).map((t) => t.trim()).filter((t) => t.length > 2 && !STOP.has(t)).slice(0, max);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function windowAgoIso(days) {
|
|
130
|
+
const d = new Date();
|
|
131
|
+
d.setDate(d.getDate() - days);
|
|
132
|
+
return d.toISOString();
|
|
133
|
+
}
|
|
134
|
+
function weekAgoIso() { return windowAgoIso(7); }
|
|
135
|
+
function trendingIso() { return windowAgoIso(90); } // 7d window is empty: newest DS row is ~12 weeks old; 90d keeps "trending" meaningful
|
|
136
|
+
|
|
137
|
+
function buildSearchParams(kind, q = {}) {
|
|
138
|
+
const p = new URLSearchParams();
|
|
139
|
+
p.set("select", LIST_COLS[kind]);
|
|
140
|
+
const limit = q.limit;
|
|
141
|
+
const offset = q.offset ?? 0;
|
|
142
|
+
p.set("limit", String(limit));
|
|
143
|
+
p.set("offset", String(offset));
|
|
144
|
+
const sort = CATALOG_SORTS[kind][q.sort] ?? CATALOG_SORTS[kind].popular;
|
|
145
|
+
p.set("order", sort);
|
|
146
|
+
const filters = [];
|
|
147
|
+
if (kind === "components" || kind === "assets") {
|
|
148
|
+
filters.push("private=eq.false");
|
|
149
|
+
if (q.freeOnly) filters.push("premium=eq.false");
|
|
150
|
+
} else if (kind === "skills" || kind === "design_systems") {
|
|
151
|
+
filters.push("private=eq.false");
|
|
152
|
+
}
|
|
153
|
+
if (q.tag && kind === "components") filters.push("tags=cs.{" + q.tag.toLowerCase() + "}");
|
|
154
|
+
if (q.mediaType && kind === "assets") filters.push("media_type=eq." + q.mediaType);
|
|
155
|
+
const ors = [];
|
|
156
|
+
if (q.query && q.query.trim()) {
|
|
157
|
+
const v = ilike(q.query);
|
|
158
|
+
if (kind === "components") ors.push("title.ilike." + v + ",description.ilike." + v);
|
|
159
|
+
else if (kind === "skills") ors.push("title.ilike." + v + ",description.ilike." + v);
|
|
160
|
+
else if (kind === "assets") ors.push("title.ilike." + v + ",description.ilike." + v);
|
|
161
|
+
else ors.push("title.ilike." + v + ",description.ilike." + v);
|
|
162
|
+
}
|
|
163
|
+
if (q.sort === "trending") filters.push("created_at=gte." + trendingIso());
|
|
164
|
+
return { params: p, filters, ors };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function buildUrl(base, table, params, filters, ors) {
|
|
168
|
+
let qs = params.toString();
|
|
169
|
+
for (const f of filters) qs += "&" + f;
|
|
170
|
+
if (ors.length) qs += "&or=(" + ors.join(",") + ")";
|
|
171
|
+
return base + "/rest/v1/" + table + "?" + qs;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const trunc = (s, n) => (s && s.length > n ? s.slice(0, n) + "…[truncated]" : s);
|
|
175
|
+
|
|
176
|
+
function pageUrl(kind, row) {
|
|
177
|
+
if (kind === "components") return SITE + "/component/" + row.slug;
|
|
178
|
+
if (kind === "skills") return SITE + "/skills/" + row.id;
|
|
179
|
+
if (kind === "assets") return SITE + "/assets?q=" + encodeURIComponent(row.title ?? "");
|
|
180
|
+
return SITE + "/design-systems/" + (row.slug ?? row.id);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function shapeRow(kind, row, cfg, withFacets) {
|
|
184
|
+
const base = { ...row };
|
|
185
|
+
if (kind === "components" && typeof base.code === "string") { base.code_chars = base.code.length; base.code = trunc(base.code, cfg.codeChars); }
|
|
186
|
+
if (kind === "skills" && typeof base.content === "string") base.content = trunc(base.content, cfg.contentChars);
|
|
187
|
+
if (kind === "design_systems") {
|
|
188
|
+
if (typeof base.content === "string") base.content = trunc(base.content, cfg.contentChars);
|
|
189
|
+
if (typeof base.preview_html === "string") base.preview_html = trunc(base.preview_html, cfg.codeChars);
|
|
190
|
+
}
|
|
191
|
+
base.page_url = pageUrl(kind, row);
|
|
192
|
+
if (withFacets) { try { base.facets = withFacets(kind, base); } catch { /* facets never break rows */ } }
|
|
193
|
+
return base;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function createCatalog({ fetcher, config, now = () => Date.now(), facetsFn = null }) {
|
|
197
|
+
const base = config.supabaseUrl.replace(/\/$/, "");
|
|
198
|
+
const headers = { apikey: config.anonKey, Authorization: "Bearer " + config.anonKey, Prefer: "count=exact" };
|
|
199
|
+
const cache = new Map();
|
|
200
|
+
const MAX_KEYS = 300;
|
|
201
|
+
const cacheGet = (k) => {
|
|
202
|
+
if (!config.cacheTtlMs) return null;
|
|
203
|
+
const e = cache.get(k);
|
|
204
|
+
if (!e) return null;
|
|
205
|
+
if (now() - e.t > config.cacheTtlMs) { cache.delete(k); return null; }
|
|
206
|
+
cache.delete(k); cache.set(k, e); // LRU refresh
|
|
207
|
+
return e.v;
|
|
208
|
+
};
|
|
209
|
+
const cacheSet = (k, v) => {
|
|
210
|
+
if (!config.cacheTtlMs) return;
|
|
211
|
+
if (cache.has(k)) cache.delete(k);
|
|
212
|
+
cache.set(k, { t: now(), v });
|
|
213
|
+
while (cache.size > MAX_KEYS) cache.delete(cache.keys().next().value);
|
|
214
|
+
};
|
|
215
|
+
const authorCache = new Map(); // created_by -> profile (shared across searches)
|
|
216
|
+
const inflight = new Map(); // cacheKey -> Promise (coalesce concurrent identical calls)
|
|
217
|
+
|
|
218
|
+
const clampLimit = (l) => Math.min(config.maxLimit, Math.max(1, l ?? config.defaultLimit));
|
|
219
|
+
|
|
220
|
+
async function enrichAuthors(rows) {
|
|
221
|
+
const ids = [...new Set(rows.map((r) => r.created_by).filter(Boolean).filter((id) => !authorCache.has(id)))];
|
|
222
|
+
if (ids.length) {
|
|
223
|
+
try {
|
|
224
|
+
const url = base + "/rest/v1/public_author_profiles?select=id,full_name,avatar_url,slug&id=in.(" + ids.map(esc).join(",") + ")";
|
|
225
|
+
const { rows: authors } = await fetcher.getJson(url, headers);
|
|
226
|
+
for (const a of authors) { authorCache.set(a.id, a); if (authorCache.size > 500) break; }
|
|
227
|
+
} catch { /* authors stay null, rows still return */ }
|
|
228
|
+
}
|
|
229
|
+
return rows.map((r) => ({ ...r, author: (r.created_by && authorCache.get(r.created_by)) || null }));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function searchCatalog(kind, q = {}) {
|
|
233
|
+
const limit = clampLimit(q.limit);
|
|
234
|
+
const offset = Math.max(0, q.offset ?? 0);
|
|
235
|
+
const sort = CATALOG_SORTS[kind][q.sort] ? q.sort : "popular";
|
|
236
|
+
const freeOnly = q.freeOnly ?? config.freeOnlyDefault;
|
|
237
|
+
const key = ["search", kind, q.query ?? "", q.tag ?? "", q.mediaType ?? "", sort, limit, offset, freeOnly].join("|");
|
|
238
|
+
const hit = cacheGet(key);
|
|
239
|
+
if (hit) return { ...hit, cached: true };
|
|
240
|
+
if (inflight.has(key)) return inflight.get(key);
|
|
241
|
+
const p = (async () => {
|
|
242
|
+
const runOnce = async (qq) => {
|
|
243
|
+
const { params, filters, ors } = buildSearchParams(kind, { ...qq, limit, offset, sort, freeOnly });
|
|
244
|
+
const url = buildUrl(base, kind, params, filters, ors);
|
|
245
|
+
return fetcher.getJson(url, headers);
|
|
246
|
+
};
|
|
247
|
+
let { rows, total } = await runOnce(q);
|
|
248
|
+
let fallback = null;
|
|
249
|
+
// AND-phrase queries like "dark cinematic portfolio" match nothing literally:
|
|
250
|
+
// retry as OR over significant tokens so every surface degrades instead of emptying.
|
|
251
|
+
// Single-token queries that miss also retry bare (drops tag/media filters) + suggest queries.
|
|
252
|
+
const toks = (q.query && q.query.trim() && !q.tag) ? queryTokens(q.query) : [];
|
|
253
|
+
const suggest = (t) => {
|
|
254
|
+
const s = [];
|
|
255
|
+
if (t.length > 1) s.push(t.slice(0, Math.min(3, t.length)).join(" "));
|
|
256
|
+
for (const x of t.slice(0, 3)) s.push(x);
|
|
257
|
+
return [...new Set(s)].slice(0, 4);
|
|
258
|
+
};
|
|
259
|
+
if ((!rows || !rows.length) && toks.length) {
|
|
260
|
+
// Pass 1: OR over tokens, same filters (drops the AND-phrase requirement).
|
|
261
|
+
const orParts = [];
|
|
262
|
+
for (const t of toks) {
|
|
263
|
+
const v = ilike(t);
|
|
264
|
+
orParts.push("title.ilike." + v + ",description.ilike." + v);
|
|
265
|
+
}
|
|
266
|
+
const b1 = buildSearchParams(kind, { ...q, query: "", limit, offset, sort, freeOnly });
|
|
267
|
+
try {
|
|
268
|
+
const fb = await fetcher.getJson(buildUrl(base, kind, b1.params, b1.filters, orParts), headers);
|
|
269
|
+
if (fb.rows && fb.rows.length) { rows = fb.rows; total = fb.total; fallback = "or-tokens:" + toks.join(","); }
|
|
270
|
+
} catch { /* try pass 2 */ }
|
|
271
|
+
// Pass 2: still empty (e.g. trending window + rare tokens): drop sort window,
|
|
272
|
+
// keep OR tokens, rank by views. Guarantees aggregators return something useful.
|
|
273
|
+
if ((!rows || !rows.length)) {
|
|
274
|
+
const b2 = buildSearchParams(kind, { ...q, query: "", limit, offset, sort: "popular", freeOnly });
|
|
275
|
+
try {
|
|
276
|
+
const fb2 = await fetcher.getJson(buildUrl(base, kind, b2.params, b2.filters, orParts), headers);
|
|
277
|
+
if (fb2.rows && fb2.rows.length) { rows = fb2.rows; total = fb2.total; fallback = "or-tokens-unwindowed:" + toks.join(","); }
|
|
278
|
+
} catch { /* keep original empty result */ }
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const enriched = await enrichAuthors(rows);
|
|
282
|
+
const out = { kind, total, limit, offset, items: enriched.map((r) => shapeRow(kind, r, config, facetsFn)), cached: false };
|
|
283
|
+
// Post-filter: theme (dark|light) is derived, not a column — filter shaped rows.
|
|
284
|
+
if (q.theme && (kind === "components")) {
|
|
285
|
+
const want = String(q.theme).toLowerCase();
|
|
286
|
+
out.items = out.items.filter((it) => (it.facets && it.facets.theme) === want);
|
|
287
|
+
}
|
|
288
|
+
if (fallback) out.fallback = fallback;
|
|
289
|
+
else if ((!rows || !rows.length) && toks.length) out.suggested_queries = suggest(toks);
|
|
290
|
+
cacheSet(key, out);
|
|
291
|
+
return out;
|
|
292
|
+
})();
|
|
293
|
+
inflight.set(key, p);
|
|
294
|
+
try { return await p; } finally { inflight.delete(key); }
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function getItem(kind, idOrSlug) {
|
|
298
|
+
const key = ["get", kind, String(idOrSlug)].join("|");
|
|
299
|
+
const hit = cacheGet(key);
|
|
300
|
+
if (hit) return { ...hit, cached: true };
|
|
301
|
+
const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(String(idOrSlug));
|
|
302
|
+
const isNum = /^\d+$/.test(String(idOrSlug));
|
|
303
|
+
const sel = DETAIL_COLS[kind];
|
|
304
|
+
// design_systems + skills: uuid id vs slug; components: numeric id vs slug
|
|
305
|
+
const filt = (kind === "design_systems" || kind === "skills")
|
|
306
|
+
? (isUuid ? "id=eq." + encodeURIComponent(String(idOrSlug)) : "slug=eq." + encodeURIComponent(String(idOrSlug)))
|
|
307
|
+
: (kind === "components" && !isNum
|
|
308
|
+
? "slug=eq." + encodeURIComponent(String(idOrSlug))
|
|
309
|
+
: "id=eq." + encodeURIComponent(String(idOrSlug)));
|
|
310
|
+
const url = base + "/rest/v1/" + kind + "?select=" + encodeURIComponent(sel) + "&" + filt + "&limit=1";
|
|
311
|
+
const { rows } = await fetcher.getJson(url, headers);
|
|
312
|
+
if (!rows.length) { const e = new Error("not found: " + kind + " " + idOrSlug); e.code = "NOT_FOUND"; throw e; }
|
|
313
|
+
const enriched = await enrichAuthors(rows);
|
|
314
|
+
const out = { kind, item: shapeRow(kind, enriched[0], config, facetsFn), cached: false };
|
|
315
|
+
cacheSet(key, out);
|
|
316
|
+
return out;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function getStatus() {
|
|
320
|
+
const key = "status";
|
|
321
|
+
const hit = cacheGet(key);
|
|
322
|
+
if (hit) return { ...hit, cached: true };
|
|
323
|
+
const counts = {};
|
|
324
|
+
const jobs = [
|
|
325
|
+
["components_free", "components?select=id&private=eq.false&premium=eq.false&limit=1"],
|
|
326
|
+
["components_pro", "components?select=id&private=eq.false&premium=eq.true&limit=1"],
|
|
327
|
+
["skills", "skills?select=id&private=eq.false&limit=1"],
|
|
328
|
+
["assets", "assets?select=id&private=eq.false&limit=1"],
|
|
329
|
+
["design_systems", "design_systems?select=id&private=eq.false&limit=1"],
|
|
330
|
+
];
|
|
331
|
+
await Promise.all(jobs.map(async ([k, path]) => {
|
|
332
|
+
try { const { total } = await fetcher.getJson(base + "/rest/v1/" + path, headers); counts[k] = total; }
|
|
333
|
+
catch { counts[k] = null; }
|
|
334
|
+
}));
|
|
335
|
+
const out = { site: SITE, catalogue: "aura.build public catalogue (PostgREST, anon)", counts, cached: false };
|
|
336
|
+
cacheSet(key, out);
|
|
337
|
+
return out;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function categoryCounts() {
|
|
341
|
+
const key = 'categories';
|
|
342
|
+
const hit = cacheGet(key);
|
|
343
|
+
if (hit) return hit.categories;
|
|
344
|
+
const cats = ['hero','section','button','card','background','header','logo','feature','pricing','testimonial','footer','form','heading'];
|
|
345
|
+
const out = await Promise.all(cats.map(async (c) => {
|
|
346
|
+
try {
|
|
347
|
+
const url = base + '/rest/v1/components?select=id&private=eq.false&premium=eq.false&tags=cs.{' + c + '}&limit=1';
|
|
348
|
+
const { total } = await fetcher.getJson(url, headers);
|
|
349
|
+
return { category: c, free: total };
|
|
350
|
+
} catch { return { category: c, free: null }; }
|
|
351
|
+
}));
|
|
352
|
+
cacheSet(key, { categories: out });
|
|
353
|
+
return out;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Bulk fetch: N details in one parallel round (backs aura_bundle + aura_scaffold_page).
|
|
357
|
+
// Per-id errors are captured, never thrown: { ok:true, item } or { ok:false, id, error }.
|
|
358
|
+
async function bundleItems(kind, ids, max = 5) {
|
|
359
|
+
const list = [...new Set((ids || []).map(String))].slice(0, Math.max(1, Math.min(max, 8)));
|
|
360
|
+
const out = await Promise.all(list.map(async (id) => {
|
|
361
|
+
try { const got = await getItem(kind, id); return { ok: true, id, item: got.item }; }
|
|
362
|
+
catch (e) { return { ok: false, id, error: (e && e.code === "NOT_FOUND") ? "not found" : String((e && e.message) || e).slice(0, 160) }; }
|
|
363
|
+
}));
|
|
364
|
+
return out;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Related: same-tag / same-text overlap, views-ranked, excluding self.
|
|
368
|
+
async function relatedItems(kind, idOrSlug, limit = 3) {
|
|
369
|
+
const got = await getItem(kind, idOrSlug);
|
|
370
|
+
const item = got.item;
|
|
371
|
+
const toks = queryTokens(item.title + " " + (item.description || "")).slice(0, 3);
|
|
372
|
+
const orParts = toks.map((t) => { const v = ilike(t); return "title.ilike." + v + ",description.ilike." + v; });
|
|
373
|
+
const tagF = (kind === "components" && item.tags && item.tags.length) ? "&tags=cs.{" + String(item.tags[0]).toLowerCase() + "}" : "";
|
|
374
|
+
const lim = Math.max(1, Math.min(limit || 3, 8));
|
|
375
|
+
const sel = LIST_COLS[kind];
|
|
376
|
+
const idCol = (kind === "components") ? "id" : "id";
|
|
377
|
+
const selfId = encodeURIComponent(String(item.id));
|
|
378
|
+
const url = base + "/rest/v1/" + kind + "?select=" + encodeURIComponent(sel) + "&private=eq.false" + (kind === "components" || kind === "assets" ? "" : "") + tagF + "&" + idCol + "=neq." + selfId + (orParts.length ? "&or=(" + orParts.join(",") + ")" : "") + "&order=views.desc&limit=" + lim;
|
|
379
|
+
try {
|
|
380
|
+
const { rows } = await fetcher.getJson(url, headers);
|
|
381
|
+
const enriched = await enrichAuthors(rows);
|
|
382
|
+
return { item: shapeRow(kind, item, config), related: enriched.map((r) => shapeRow(kind, r, config)) };
|
|
383
|
+
} catch { return { item: shapeRow(kind, item, config), related: [] }; }
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return { searchCatalog, getItem, getStatus, categoryCounts, bundleItems, relatedItems };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ---- protocol.mjs ----
|
|
390
|
+
// Module: protocol — stdio JSON-RPC framing + MCP handshake + error codes.
|
|
391
|
+
// One interface: createSession(send) -> { dispatch(msg) }. No business logic.
|
|
392
|
+
|
|
393
|
+
const SERVER_INFO = { name: "aura-components-mcp", version: "1.0.0" };
|
|
394
|
+
const PROTOCOL_VERSION = "2024-11-05";
|
|
395
|
+
|
|
396
|
+
const protocolFn_err = (code, message, data) => ({ code, message, ...(data === undefined ? {} : { data }) });
|
|
397
|
+
const Errors = {
|
|
398
|
+
parse: (m) => protocolFn_err(-32700, "Parse error" + (m ? ": " + m : "")),
|
|
399
|
+
invalidRequest: (m) => protocolFn_err(-32600, "Invalid Request" + (m ? ": " + m : "")),
|
|
400
|
+
methodNotFound: (m) => protocolFn_err(-32601, "Method not found: " + m),
|
|
401
|
+
invalidParams: (m) => protocolFn_err(-32602, "Invalid params" + (m ? ": " + m : "")),
|
|
402
|
+
upstream: (m) => protocolFn_err(-32000, "Upstream error" + (m ? ": " + m : "")),
|
|
403
|
+
notFound: (m) => protocolFn_err(-32004, m || "Not found"),
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
function textResult(obj) {
|
|
407
|
+
return { content: [{ type: "text", text: JSON.stringify(obj, null, 2) }] };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function createSession(send, { tools, handlers }) {
|
|
411
|
+
let initialized = false;
|
|
412
|
+
const ok = (id, result) => send({ jsonrpc: "2.0", id, result });
|
|
413
|
+
const fail = (id, e) => send({ jsonrpc: "2.0", id, error: e });
|
|
414
|
+
|
|
415
|
+
async function dispatch(msg) {
|
|
416
|
+
if (!msg || typeof msg !== "object" || msg.jsonrpc !== "2.0") {
|
|
417
|
+
if (msg && "id" in msg && msg.id !== undefined && msg.id !== null) fail(msg.id, Errors.invalidRequest("bad envelope"));
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const { id, method, params } = msg;
|
|
421
|
+
const isCall = id !== undefined && id !== null;
|
|
422
|
+
try {
|
|
423
|
+
if (method === "initialize") {
|
|
424
|
+
initialized = true;
|
|
425
|
+
if (!isCall) return;
|
|
426
|
+
return ok(id, { protocolVersion: PROTOCOL_VERSION, capabilities: { tools: {} }, serverInfo: SERVER_INFO });
|
|
427
|
+
}
|
|
428
|
+
if (method === "notifications/initialized") return;
|
|
429
|
+
if (method === "ping") { if (isCall) return ok(id, {}); return; }
|
|
430
|
+
if (!initialized) { if (isCall) fail(id, Errors.invalidRequest("not initialized")); return; }
|
|
431
|
+
if (method === "tools/list") { if (isCall) return ok(id, { tools }); return; }
|
|
432
|
+
if (method === "tools/call") {
|
|
433
|
+
if (!isCall) return;
|
|
434
|
+
const name = params?.name;
|
|
435
|
+
const args = params?.arguments ?? {};
|
|
436
|
+
const h = handlers[name];
|
|
437
|
+
if (!h) return fail(id, Errors.methodNotFound("tool " + name));
|
|
438
|
+
try {
|
|
439
|
+
const result = await h(args);
|
|
440
|
+
return ok(id, result);
|
|
441
|
+
} catch (e) {
|
|
442
|
+
if (e && e.code === "NOT_FOUND") return fail(id, Errors.notFound(e.message));
|
|
443
|
+
if (e && e.code === "BAD_ARGS") return fail(id, Errors.invalidParams(e.message));
|
|
444
|
+
return fail(id, Errors.upstream(e?.message ?? String(e)));
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (isCall) return fail(id, Errors.methodNotFound(method));
|
|
448
|
+
} catch (e) {
|
|
449
|
+
if (isCall) fail(id, Errors.upstream(e?.message ?? String(e)));
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return { dispatch };
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ---- guide.mjs ----
|
|
457
|
+
// Module: guide — small interface (installGuide, tokenHints, detectNeeds)
|
|
458
|
+
// turning raw catalogue rows into paste-ready help: setup steps, file map,
|
|
459
|
+
// dependency list, token starter. Derived from row fields, no extra fetches.
|
|
460
|
+
|
|
461
|
+
const GUIDE_CDN = {
|
|
462
|
+
tailwind: 'https://cdn.tailwindcss.com',
|
|
463
|
+
iconify: 'https://code.iconify.design/iconify-icon/1.0.7/iconify-icon.min.js',
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
function detectNeeds(code, tags) {
|
|
467
|
+
const c = String(code || '').toLowerCase();
|
|
468
|
+
const t = new Set((tags || []).map((x) => String(x).toLowerCase()));
|
|
469
|
+
const needsTailwind = c.includes('class=') || t.has('tailwind');
|
|
470
|
+
const needsIcons = c.includes('iconify') || c.includes('svg');
|
|
471
|
+
const needsKeyframes = c.includes('keyframes') || c.includes('animation:');
|
|
472
|
+
const fonts = new Set();
|
|
473
|
+
for (const m of c.matchAll(/font-([a-z0-9-]+)/g)) fonts.add(m[1]);
|
|
474
|
+
return { needsTailwind, needsIcons, needsKeyframes, fonts: [...fonts].slice(0, 6) };
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
// Facets: cheap derived signals so search lists answer "dark? heavy? pro?" without a get.
|
|
478
|
+
// theme: dark|light|mixed|unknown from background field + code palette probes.
|
|
479
|
+
// weight: code_chars bucket (s/m/l) so agents can prefer light embeds.
|
|
480
|
+
function facets(kind, row) {
|
|
481
|
+
const f = {};
|
|
482
|
+
if (kind === 'components') {
|
|
483
|
+
const bg = String(row.background || '').toLowerCase();
|
|
484
|
+
const tags = (row.tags || []).map((x) => String(x).toLowerCase());
|
|
485
|
+
const code = String(row.code || '').toLowerCase();
|
|
486
|
+
const tagDark = tags.includes('dark');
|
|
487
|
+
const tagLight = tags.includes('light');
|
|
488
|
+
const darkHits = (code.match(/#0{3,6}\b|#1[0-9a-f]{5}\b|bg-black|bg-neutral-9|bg-zinc-9|bg-slate-9|text-white|slate-300/g) || []).length;
|
|
489
|
+
const lightHits = (code.match(/bg-white|bg-neutral-50|bg-slate-50|bg-gray-50|text-black|text-neutral-9/g) || []).length;
|
|
490
|
+
const bgDark = bg.includes('000') && !bg.includes('fff');
|
|
491
|
+
const bgLight = bg.includes('fff') && !bg.includes('000');
|
|
492
|
+
f.theme = bgDark || tagDark ? 'dark' : (bgLight || tagLight ? 'light' : (code ? (darkHits > lightHits * 2 ? 'dark' : (lightHits > darkHits * 2 ? 'light' : (darkHits || lightHits ? 'mixed' : 'unknown'))) : (tags.includes('saas') || tags.includes('minimal') ? 'light' : 'unknown')));
|
|
493
|
+
const n = String(row.code || '').length;
|
|
494
|
+
f.weight = n > 20000 ? 'l' : (n > 8000 ? 'm' : 's');
|
|
495
|
+
f.code_chars = n;
|
|
496
|
+
const needs = detectNeeds(row.code || '', row.tags || []);
|
|
497
|
+
f.needsTailwind = needs.needsTailwind;
|
|
498
|
+
f.needsIcons = needs.needsIcons;
|
|
499
|
+
f.fonts = needs.fonts;
|
|
500
|
+
}
|
|
501
|
+
if (kind === 'assets' || row.image_800w || row.image_original || row.video_url) {
|
|
502
|
+
// Resolved 2026-09-13 from primary source https://www.aura.build/terms §4-5:
|
|
503
|
+
// catalogue content is the exclusive property of DESIGNCODE IO PTE. LTD.;
|
|
504
|
+
// no per-asset license column exists, so commercial reuse needs Aura's permission.
|
|
505
|
+
f.license = 'all-rights-reserved (Aura Terms §4: DESIGNCODE IO PTE. LTD.) — personal/preview use via page_url; commercial reuse needs Aura permission (support@designcode.io)';
|
|
506
|
+
f.download = row.image_original || row.image_1600w || row.image_800w || row.video_url || null;
|
|
507
|
+
f.preview = row.image_800w || row.video_poster_url || null;
|
|
508
|
+
}
|
|
509
|
+
if (kind === 'design_systems') f.has_preview = Boolean(row.preview_html);
|
|
510
|
+
return f;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function installGuide(kind, row) {
|
|
514
|
+
const steps = [];
|
|
515
|
+
const files = [];
|
|
516
|
+
const deps = [];
|
|
517
|
+
const url = row.page_url || '';
|
|
518
|
+
if (kind === 'components') {
|
|
519
|
+
const needs = detectNeeds(row.code || '', row.tags || []);
|
|
520
|
+
if (needs.needsTailwind) {
|
|
521
|
+
deps.push({ name: 'tailwindcss', via: 'CDN or project setup', cdn: GUIDE_CDN.tailwind });
|
|
522
|
+
steps.push('Add the Tailwind CDN script to head for a quick preview, or paste the markup into a Tailwind project.');
|
|
523
|
+
}
|
|
524
|
+
if (needs.needsIcons) {
|
|
525
|
+
deps.push({ name: 'iconify-icon', via: 'CDN', cdn: GUIDE_CDN.iconify });
|
|
526
|
+
steps.push('Add the iconify-icon CDN script so the icon tags render.');
|
|
527
|
+
}
|
|
528
|
+
if (needs.needsKeyframes) steps.push('Keep the embedded style block with the markup: that is where the keyframes live.');
|
|
529
|
+
steps.push('Paste the markup where the section belongs and update the CTA link (it ships as #).');
|
|
530
|
+
if (url) steps.push('Preview first: ' + url);
|
|
531
|
+
files.push({ path: 'components/' + (row.slug || row.id) + '.html', contains: 'section markup plus its style block' });
|
|
532
|
+
if (needs.fonts.length) files.push({ path: 'styles/fonts.css', contains: 'font families referenced: ' + needs.fonts.join(', ') });
|
|
533
|
+
return { kind, title: row.title, page_url: url, free: row.premium === false, steps, deps, files, needs };
|
|
534
|
+
}
|
|
535
|
+
if (kind === 'skills') {
|
|
536
|
+
steps.push('Read the SKILL.md content from aura_get_skill first: it names its own triggers and pitfalls.');
|
|
537
|
+
steps.push('Save it as SKILL.md inside your agent skills folder so the agent can load it.');
|
|
538
|
+
if (row.source_url) steps.push('Upstream source: ' + row.source_url);
|
|
539
|
+
return { kind, title: row.title, page_url: url, steps, deps, files: [{ path: 'skills/' + row.id + '/SKILL.md', contains: 'full skill content' }] };
|
|
540
|
+
}
|
|
541
|
+
if (kind === 'design_systems') {
|
|
542
|
+
steps.push('Copy the DESIGN.md content into your repo as DESIGN.md and treat it as the source of truth.');
|
|
543
|
+
steps.push('Apply the color, type, and spacing tokens before copying any component markup.');
|
|
544
|
+
return { kind, title: row.title, page_url: url, steps, deps, files: [{ path: 'DESIGN.md', contains: 'tokens and rules' }] };
|
|
545
|
+
}
|
|
546
|
+
steps.push('Use the image_800w URL for previews and image_original for production.');
|
|
547
|
+
return { kind, title: row.title, page_url: url, steps, deps, files };
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
function tokenHints(content) {
|
|
551
|
+
const Q = String.fromCharCode(39);
|
|
552
|
+
const lines = String(content || '').split(Q + 'n' === Q + 'n' ? '\n' : '\n');
|
|
553
|
+
const get = (key) => {
|
|
554
|
+
const line = lines.find((l) => { const t = l.trim().toLowerCase(); return t === key + ':' || t.startsWith(key + ': ') || t.startsWith(key + ' :') || t.startsWith(key + ':\"') || t.startsWith(key + ":'"); });
|
|
555
|
+
if (!line) return null;
|
|
556
|
+
const t = line.trim();
|
|
557
|
+
const ci = t.toLowerCase().indexOf(key);
|
|
558
|
+
let v = t.slice(ci + key.length).trim();
|
|
559
|
+
if (v.startsWith(':')) v = v.slice(1).trim();
|
|
560
|
+
while (v.startsWith(Q) || v.startsWith('"')) v = v.slice(1);
|
|
561
|
+
while (v.endsWith(Q) || v.endsWith('"')) v = v.slice(0, -1);
|
|
562
|
+
return v.trim() || null;
|
|
563
|
+
};
|
|
564
|
+
const tokens = {
|
|
565
|
+
primary: get('primary'),
|
|
566
|
+
background: get('background'),
|
|
567
|
+
surface: get('surface'),
|
|
568
|
+
text: get('text-primary') || get('text_primary'),
|
|
569
|
+
};
|
|
570
|
+
const rows = Object.entries(tokens).filter((e) => e[1]).map((e) => ' --aura-' + e[0] + ': ' + e[1] + ';');
|
|
571
|
+
const css = rows.length ? ':root{' + '\n' + rows.join('\n') + '\n}' : '';
|
|
572
|
+
return { tokens, css };
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
// ---- tools.mjs ----
|
|
576
|
+
// Module: tools — thin adapters. Each tool validates args at the seam,
|
|
577
|
+
// calls one catalog/guide method, wraps textResult. No retries/caching here.
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
const TOOL_KINDS = ['components', 'skills', 'assets', 'design_systems'];
|
|
581
|
+
const TOOL_SORTS = ['popular', 'recent', 'trending', 'updated'];
|
|
582
|
+
const TOOL_CATS = ['hero','section','button','card','background','header','logo','feature','pricing','testimonial','footer','form','heading'];
|
|
583
|
+
|
|
584
|
+
function toolFn_bad(msg) { const e = new Error(msg); e.code = 'BAD_ARGS'; throw e; }
|
|
585
|
+
function toolFn_str(v, name) { if (v !== undefined && typeof v !== 'string') toolFn_bad(name + ' must be a string'); return v; }
|
|
586
|
+
function toolFn_num(v, name) { if (v !== undefined && typeof v !== 'number') toolFn_bad(name + ' must be a number'); return v; }
|
|
587
|
+
function toolFn_bool(v, name) { if (v !== undefined && typeof v !== 'boolean') toolFn_bad(name + ' must be a boolean'); return v; }
|
|
588
|
+
function toolFn_sort(v) { if (v !== undefined && !TOOL_SORTS.includes(v)) toolFn_bad('sort must be one of ' + TOOL_SORTS.join('|')); return v; }
|
|
589
|
+
function theme(v) { if (v !== undefined && v !== 'dark' && v !== 'light') toolFn_bad('theme must be dark|light'); return v; }
|
|
590
|
+
function idList(v, name) { if (v === undefined) return v; if (!Array.isArray(v) || !v.length || v.length > 8 || v.some((x) => typeof x !== 'string' && typeof x !== 'number')) toolFn_bad(name + ' must be an array of 1-8 ids'); return v; }
|
|
591
|
+
function toolFn_cat(v) { if (v !== undefined && !TOOL_CATS.includes(v)) toolFn_bad('category must be one of ' + TOOL_CATS.join('|')); return v; }
|
|
592
|
+
|
|
593
|
+
const TOOL_DEFS = [
|
|
594
|
+
{ name: 'aura_status', description: 'Catalogue health plus free counts (components, skills, assets, design systems). Free only, no login. Start here.', inputSchema: { type: 'object', properties: {}, additionalProperties: false } },
|
|
595
|
+
{ name: 'aura_search_components', description: 'Search free Aura UI components (2,495). Text over title and description, optional category tag, theme dark|light filter, sorts. Items carry facets (theme/weight/needs).', inputSchema: { type: 'object', properties: { query: { type: 'string' }, category: { type: 'string', enum: TOOL_CATS }, theme: { type: 'string', enum: ['dark', 'light'] }, sort: { type: 'string', enum: TOOL_SORTS }, limit: { type: 'number' }, offset: { type: 'number' } }, additionalProperties: false } },
|
|
596
|
+
{ name: 'aura_get_component', description: 'Full free component detail with HTML/Tailwind source, preview image, page URL. Numeric id or slug.', inputSchema: { type: 'object', properties: { id: {}, slug: { type: 'string' } }, additionalProperties: false } },
|
|
597
|
+
{ name: 'aura_search_skills', description: 'Search free Aura agent skills (187). Metadata only; use aura_get_skill for the full SKILL.md content.', inputSchema: { type: 'object', properties: { query: { type: 'string' }, sort: { type: 'string', enum: TOOL_SORTS }, limit: { type: 'number' }, offset: { type: 'number' } }, additionalProperties: false } },
|
|
598
|
+
{ name: 'aura_get_skill', description: 'Full free agent-skill content (SKILL.md body) plus source_url and page URL. Skill id.', inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'], additionalProperties: false } },
|
|
599
|
+
{ name: 'aura_search_assets', description: 'Search free Aura assets (images and video). Keywords, media_type image or video.', inputSchema: { type: 'object', properties: { query: { type: 'string' }, mediaType: { type: 'string', enum: ['image', 'video'] }, sort: { type: 'string', enum: TOOL_SORTS }, limit: { type: 'number' }, offset: { type: 'number' } }, additionalProperties: false } },
|
|
600
|
+
{ name: 'aura_search_design_systems', description: 'Search free Aura DESIGN.md systems (725). Metadata only; use aura_get_design_system for content.', inputSchema: { type: 'object', properties: { query: { type: 'string' }, sort: { type: 'string', enum: TOOL_SORTS }, limit: { type: 'number' }, offset: { type: 'number' } }, additionalProperties: false } },
|
|
601
|
+
{ name: 'aura_get_design_system', description: 'Full free DESIGN.md content plus preview_html, tokens, and page URL. System id or slug.', inputSchema: { type: 'object', properties: { id: { type: 'string' }, slug: { type: 'string' } }, additionalProperties: false } },
|
|
602
|
+
{ name: 'aura_search_all', description: 'One call across components, skills, assets, and design systems in parallel. Free only.', inputSchema: { type: 'object', properties: { query: { type: 'string' }, limit: { type: 'number' } }, additionalProperties: false } },
|
|
603
|
+
{ name: 'aura_recommend', description: 'Starter kit for a goal: top free components, skills, design systems, and assets with page URLs and rationale.', inputSchema: { type: 'object', properties: { goal: { type: 'string' } }, required: ['goal'], additionalProperties: false } },
|
|
604
|
+
{ name: 'aura_install_component', description: 'Paste-ready setup for a free component: dependency list, setup steps, file map, fonts. Goes beyond the official Aura MCP, which only reads project source.', inputSchema: { type: 'object', properties: { id: {}, slug: { type: 'string' } }, additionalProperties: false } },
|
|
605
|
+
{ name: 'aura_install_skill', description: 'Save-and-load plan for a free skill: where to put SKILL.md per client plus upstream source.', inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'], additionalProperties: false } },
|
|
606
|
+
{ name: 'aura_use_design_system', description: 'Apply a free DESIGN.md system: token starter CSS plus copy order (tokens first, then markup).', inputSchema: { type: 'object', properties: { id: { type: 'string' }, slug: { type: 'string' } }, additionalProperties: false } },
|
|
607
|
+
{ name: 'aura_trending', description: 'What is new and popular across the free catalogue: top components, skills, assets, design systems in one call.', inputSchema: { type: 'object', properties: { limit: { type: 'number' } }, additionalProperties: false } },
|
|
608
|
+
{ name: 'aura_bundle', description: 'Bulk-fetch 2-8 component details in one call (ids or slugs). Per-item errors never fail the batch.', inputSchema: { type: 'object', properties: { ids: { type: 'array', items: {} }, slugs: { type: 'array', items: { type: 'string' } } }, additionalProperties: false } },
|
|
609
|
+
{ name: 'aura_scaffold_page', description: 'One ordered page build: DESIGN.md tokens.css + system preview + component markup in dependency order, combined deps + files[]. Merges install_* + use_* in a single turn.', inputSchema: { type: 'object', properties: { goal: { type: 'string' }, system: { type: 'string' }, components: { type: 'array', items: {} } }, required: ['goal'], additionalProperties: false } },
|
|
610
|
+
{ name: 'aura_related', description: 'More-like-this: 3 related items for a component/skill/design-system by tag + text overlap. Discovery never dead-ends.', inputSchema: { type: 'object', properties: { kind: { type: 'string', enum: ['components', 'skills', 'design_systems'] }, id: {} }, required: ['kind', 'id'], additionalProperties: false } },
|
|
611
|
+
{ name: 'aura_bulk_fetch', description: 'Bulk-fetch 2-8 details in one call for components, design_systems, or assets. Alias-friendly name for aura_bundle. Per-item errors never fail the batch.', inputSchema: { type: 'object', properties: { kind: { type: 'string', enum: ['components', 'design_systems', 'assets'] }, ids: { type: 'array', items: {} } }, required: ['kind', 'ids'], additionalProperties: false } },
|
|
612
|
+
{ name: 'aura_install_asset', description: 'Legal drop-in plan for an asset: direct download URL, preview URL, license (all-rights-reserved per Aura Terms §4 — check page before commercial use), suggested file path.', inputSchema: { type: 'object', properties: { id: { type: 'number' } }, required: ['id'], additionalProperties: false } },
|
|
613
|
+
{ name: 'aura_categories', description: 'The 13 component categories with live free counts. Pick one, then search within it.', inputSchema: { type: 'object', properties: {}, additionalProperties: false } },
|
|
614
|
+
];
|
|
615
|
+
|
|
616
|
+
function createHandlers(catalog) {
|
|
617
|
+
const free = { freeOnly: true };
|
|
618
|
+
return {
|
|
619
|
+
aura_status: async () => textResult(await catalog.getStatus()),
|
|
620
|
+
aura_search_components: async (a) => { a = a || {}; return textResult(await catalog.searchCatalog('components', {
|
|
621
|
+
query: toolFn_str(a.query, 'query'), tag: toolFn_cat(a.category), freeOnly: true, theme: theme(a.theme),
|
|
622
|
+
sort: toolFn_sort(a.sort), limit: toolFn_num(a.limit, 'limit'), offset: toolFn_num(a.offset, 'offset'),
|
|
623
|
+
})); },
|
|
624
|
+
aura_get_component: async (a) => { a = a || {}; const id = a.id !== undefined ? a.id : a.slug;
|
|
625
|
+
if (id === undefined || (typeof id !== 'string' && typeof id !== 'number')) toolFn_bad('provide id (number) or slug (string)');
|
|
626
|
+
return textResult(await catalog.getItem('components', id)); },
|
|
627
|
+
aura_search_skills: async (a) => { a = a || {}; return textResult(await catalog.searchCatalog('skills', {
|
|
628
|
+
query: toolFn_str(a.query, 'query'), sort: toolFn_sort(a.sort), limit: toolFn_num(a.limit, 'limit'), offset: toolFn_num(a.offset, 'offset'),
|
|
629
|
+
})); },
|
|
630
|
+
aura_get_skill: async (a) => { a = a || {}; if (typeof a.id !== 'string' || !a.id) toolFn_bad('id (string) is required');
|
|
631
|
+
return textResult(await catalog.getItem('skills', a.id)); },
|
|
632
|
+
aura_search_assets: async (a) => { a = a || {}; return textResult(await catalog.searchCatalog('assets', {
|
|
633
|
+
query: toolFn_str(a.query, 'query'),
|
|
634
|
+
mediaType: a.mediaType === undefined ? undefined : (a.mediaType === 'image' || a.mediaType === 'video' ? a.mediaType : toolFn_bad('mediaType must be image|video')),
|
|
635
|
+
freeOnly: true, sort: toolFn_sort(a.sort), limit: toolFn_num(a.limit, 'limit'), offset: toolFn_num(a.offset, 'offset'),
|
|
636
|
+
})); },
|
|
637
|
+
aura_search_design_systems: async (a) => { a = a || {}; return textResult(await catalog.searchCatalog('design_systems', {
|
|
638
|
+
query: toolFn_str(a.query, 'query'), sort: toolFn_sort(a.sort), limit: toolFn_num(a.limit, 'limit'), offset: toolFn_num(a.offset, 'offset'),
|
|
639
|
+
})); },
|
|
640
|
+
aura_get_design_system: async (a) => { a = a || {}; const id = a.id !== undefined ? a.id : a.slug;
|
|
641
|
+
if (typeof id !== 'string' || !id) toolFn_bad('provide id or slug (string)');
|
|
642
|
+
return textResult(await catalog.getItem('design_systems', id)); },
|
|
643
|
+
aura_search_all: async (a) => { a = a || {}; const query = toolFn_str(a.query, 'query'), limit = toolFn_num(a.limit, 'limit') || 5;
|
|
644
|
+
const r = await Promise.all([
|
|
645
|
+
catalog.searchCatalog('components', { query, freeOnly: true, limit }),
|
|
646
|
+
catalog.searchCatalog('skills', { query, limit }),
|
|
647
|
+
catalog.searchCatalog('assets', { query, freeOnly: true, limit }),
|
|
648
|
+
catalog.searchCatalog('design_systems', { query, limit }),
|
|
649
|
+
]);
|
|
650
|
+
return textResult({ query: query || null, components: r[0], skills: r[1], assets: r[2], design_systems: r[3] }); },
|
|
651
|
+
aura_recommend: async (a) => { a = a || {}; if (typeof a.goal !== 'string' || !a.goal.trim()) toolFn_bad('goal (string) is required');
|
|
652
|
+
const r = await Promise.all([
|
|
653
|
+
catalog.searchCatalog('components', { query: a.goal, freeOnly: true, limit: 5 }),
|
|
654
|
+
catalog.searchCatalog('skills', { query: a.goal, limit: 5 }),
|
|
655
|
+
catalog.searchCatalog('assets', { query: a.goal, freeOnly: true, limit: 3 }),
|
|
656
|
+
catalog.searchCatalog('design_systems', { query: a.goal, limit: 3 }),
|
|
657
|
+
]);
|
|
658
|
+
const pick = (x) => (x.items || []).slice(0, 3).map((i) => ({ title: i.title, page_url: i.page_url }));
|
|
659
|
+
return textResult({ goal: a.goal, freeOnly: true,
|
|
660
|
+
rationale: 'Top free catalogue hits per surface, ranked by Aura views. Open the page_url previews, then fetch full source or content for finalists.',
|
|
661
|
+
components: r[0], skills: r[1], assets: r[2], design_systems: r[3],
|
|
662
|
+
starter_kit: { components: pick(r[0]), skills: pick(r[1]), assets: pick(r[2]), design_systems: pick(r[3]) } }); },
|
|
663
|
+
aura_install_component: async (a) => { a = a || {}; const id = a.id !== undefined ? a.id : a.slug;
|
|
664
|
+
if (id === undefined || (typeof id !== 'string' && typeof id !== 'number')) toolFn_bad('provide id (number) or slug (string)');
|
|
665
|
+
const got = await catalog.getItem('components', id);
|
|
666
|
+
if (got.item && got.item.premium !== false) toolFn_bad('that component is Pro (paid). Search with freeOnly or pick a free one.');
|
|
667
|
+
return textResult({ item: got.item, install: installGuide('components', got.item) }); },
|
|
668
|
+
aura_install_skill: async (a) => { a = a || {}; if (typeof a.id !== 'string' || !a.id) toolFn_bad('id (string) is required');
|
|
669
|
+
const got = await catalog.getItem('skills', a.id);
|
|
670
|
+
return textResult({ item: got.item, install: installGuide('skills', got.item) }); },
|
|
671
|
+
aura_use_design_system: async (a) => { a = a || {}; const id = a.id !== undefined ? a.id : a.slug;
|
|
672
|
+
if (typeof id !== 'string' || !id) toolFn_bad('provide id or slug (string)');
|
|
673
|
+
const got = await catalog.getItem('design_systems', id);
|
|
674
|
+
return textResult({ item: got.item, install: installGuide('design_systems', got.item), tokens: tokenHints(got.item.content || '') }); },
|
|
675
|
+
aura_trending: async (a) => { a = a || {}; const limit = toolFn_num(a.limit, 'limit') || 5;
|
|
676
|
+
const r = await Promise.all([
|
|
677
|
+
catalog.searchCatalog('components', { freeOnly: true, sort: 'trending', limit }),
|
|
678
|
+
catalog.searchCatalog('skills', { sort: 'trending', limit }),
|
|
679
|
+
catalog.searchCatalog('assets', { freeOnly: true, sort: 'trending', limit }),
|
|
680
|
+
catalog.searchCatalog('design_systems', { sort: 'trending', limit }),
|
|
681
|
+
]);
|
|
682
|
+
return textResult({ window: 'last 90 days by views (7-day seed is empty: newest catalogue rows are months old)', components: r[0], skills: r[1], assets: r[2], design_systems: r[3] }); },
|
|
683
|
+
aura_categories: async () => textResult({ categories: await catalog.categoryCounts() }),
|
|
684
|
+
aura_bundle: async (a) => { a = a || {}; const ids = idList(a.ids, 'ids') || idList(a.slugs, 'slugs');
|
|
685
|
+
if (!ids || !ids.length) toolFn_bad('provide ids (array of 1-8 numbers/strings) or slugs (array of strings)');
|
|
686
|
+
return textResult({ kind: 'components', results: await catalog.bundleItems('components', ids) }); },
|
|
687
|
+
aura_related: async (a) => { a = a || {}; const k = a.kind;
|
|
688
|
+
if (k !== 'components' && k !== 'skills' && k !== 'design_systems') toolFn_bad('kind must be components|skills|design_systems');
|
|
689
|
+
const id = a.id; if (typeof id !== 'string' && typeof id !== 'number') toolFn_bad('id is required');
|
|
690
|
+
return textResult(await catalog.relatedItems(k, id, 3)); },
|
|
691
|
+
aura_install_asset: async (a) => { a = a || {}; if (typeof a.id !== 'number') toolFn_bad('id (number) is required');
|
|
692
|
+
const got = await catalog.getItem('assets', a.id);
|
|
693
|
+
const it = got.item; const fx = (it.facets || {});
|
|
694
|
+
return textResult({ item: it, install: { kind: 'assets', title: it.title, page_url: it.page_url, license: fx.license || 'all-rights-reserved (Aura Terms §4)', terms_url: 'https://www.aura.build/terms', download: fx.download || null, preview: fx.preview || null, files: [{ path: 'assets/' + it.id + '-' + String(it.title || 'asset').toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 40) + '.jpg', contains: 'downloaded original' }], steps: ['Check the license on the Aura asset page before commercial use — this server reports unknown, never assumes free-to-sell.', 'Download the download URL into the suggested path.', 'Use the preview URL for <img> srcset while drafting.'] } }); },
|
|
695
|
+
aura_bulk_fetch: async (a) => { a = a || {}; const kind = a.kind;
|
|
696
|
+
if (kind !== 'components' && kind !== 'design_systems' && kind !== 'assets') toolFn_bad('kind must be components|design_systems|assets');
|
|
697
|
+
const ids = idList(a.ids, 'ids');
|
|
698
|
+
if (!ids || !ids.length) toolFn_bad('provide ids (array of 1-8)');
|
|
699
|
+
return textResult({ kind, results: await catalog.bundleItems(kind, ids) }); },
|
|
700
|
+
aura_scaffold_page: async (a) => { a = a || {}; if (typeof a.goal !== 'string' || !a.goal.trim()) toolFn_bad('goal (string) is required');
|
|
701
|
+
const sysRef = (typeof a.system === 'string' && a.system) ? a.system : null;
|
|
702
|
+
const compRefs = idList(a.components, 'components') || [];
|
|
703
|
+
const [sys, comps] = await Promise.all([
|
|
704
|
+
sysRef ? catalog.getItem('design_systems', sysRef).catch(() => null) : catalog.searchCatalog('design_systems', { query: a.goal, limit: 1 }).then((r) => (r.items[0] ? { item: r.items[0] } : null)),
|
|
705
|
+
compRefs.length ? catalog.bundleItems('components', compRefs).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),
|
|
706
|
+
]);
|
|
707
|
+
const sysItem = sys && sys.item ? sys.item : null;
|
|
708
|
+
const toks = sysItem ? tokenHints(sysItem.content || '') : { tokens: {}, css: '' };
|
|
709
|
+
const guides = (comps || []).map((c) => installGuide('components', c));
|
|
710
|
+
const deps = []; const seen = new Set();
|
|
711
|
+
for (const g of guides) for (const d of (g.deps || [])) if (!seen.has(d.name)) { seen.add(d.name); deps.push(d); }
|
|
712
|
+
const files = [{ path: 'styles/tokens.css', contains: 'tokens.css from use_design_system' }];
|
|
713
|
+
if (sysItem) files.push({ path: 'DESIGN.md', contains: 'system rules' });
|
|
714
|
+
for (const g of guides) for (const f of (g.files || [])) files.push(f);
|
|
715
|
+
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: comps || [], guides, combined_deps: deps, files }); },
|
|
716
|
+
};
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
// ---- server.mjs (main) ----
|
|
720
|
+
// Module: server — composition root. Wires config -> fetcher -> catalog ->
|
|
721
|
+
// tools -> protocol session over stdio. Only framing + wiring lives here.
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
const config = loadConfig();
|
|
730
|
+
const fetcher = createFetcher({ fetchImpl: globalThis.fetch, timeoutMs: config.timeoutMs, retries: config.retries, userAgent: config.userAgent });
|
|
731
|
+
const catalog = createCatalog({ fetcher, config, facetsFn: facets });
|
|
732
|
+
const handlers = createHandlers(catalog);
|
|
733
|
+
|
|
734
|
+
let buffer = "";
|
|
735
|
+
const send = (msg) => process.stdout.write(JSON.stringify(msg) + "\n");
|
|
736
|
+
const { dispatch } = createSession(send, { tools: TOOL_DEFS, handlers });
|
|
737
|
+
|
|
738
|
+
process.stdin.setEncoding("utf8");
|
|
739
|
+
process.stdin.on("data", (chunk) => {
|
|
740
|
+
buffer += chunk;
|
|
741
|
+
let i;
|
|
742
|
+
while ((i = buffer.indexOf("\n")) >= 0) {
|
|
743
|
+
const line = buffer.slice(0, i).trim();
|
|
744
|
+
buffer = buffer.slice(i + 1);
|
|
745
|
+
if (!line) continue;
|
|
746
|
+
let msg;
|
|
747
|
+
try { msg = JSON.parse(line); }
|
|
748
|
+
catch { send({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } }); continue; }
|
|
749
|
+
dispatch(msg);
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
process.stdin.on("end", () => process.exit(0));
|
|
753
|
+
process.stdin.resume();
|
|
754
|
+
|
|
755
|
+
// Startup cinematics live on stderr only: stdout is reserved for JSON-RPC.
|
|
756
|
+
// Sequence: aura moment -> catalogue credit -> builder credit -> running.
|
|
757
|
+
const BANNER = [
|
|
758
|
+
" <<< aura-components-mcp >>>",
|
|
759
|
+
" free Aura catalogue, every AI agent",
|
|
760
|
+
];
|
|
761
|
+
const sleepMs = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
762
|
+
const paint = (s) => process.stderr.write(s + "\n");
|
|
763
|
+
async function typeLine(text, cps = 90) {
|
|
764
|
+
for (const ch of text) { process.stderr.write(ch); await sleepMs(1000 / cps); }
|
|
765
|
+
process.stderr.write("\n");
|
|
766
|
+
}
|
|
767
|
+
async function dots(label, n = 3) {
|
|
768
|
+
process.stderr.write(label);
|
|
769
|
+
for (let i = 0; i < n; i++) { await sleepMs(160); process.stderr.write("."); }
|
|
770
|
+
process.stderr.write("\n");
|
|
771
|
+
}
|
|
772
|
+
async function boot() {
|
|
773
|
+
if (process.env.AURA_QUIET === "1" || process.argv.includes("--quiet")) {
|
|
774
|
+
paint(SERVER_INFO.name + " v" + SERVER_INFO.version + " listening on stdio");
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
for (const l of BANNER) paint(l);
|
|
778
|
+
await typeLine("starting your aura for designing...");
|
|
779
|
+
await dots("warming catalogue cache");
|
|
780
|
+
paint("catalogue credit: Aura (aura.build) — components, skills, assets, systems");
|
|
781
|
+
await sleepMs(180);
|
|
782
|
+
paint("built by swastiksingh-dev — free to use, MIT, no paywall");
|
|
783
|
+
await sleepMs(180);
|
|
784
|
+
paint(SERVER_INFO.name + " v" + SERVER_INFO.version + " running — 15 tools on stdio");
|
|
785
|
+
}
|
|
786
|
+
boot();
|