aura-components-mcp 1.6.0 → 1.6.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.
Files changed (2) hide show
  1. package/dist/server.js +327 -163
  2. package/package.json +1 -2
package/dist/server.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // aura-components-mcp v1.6.0 — bundled (zero deps). Built by swastiksingh-dev.
2
+ // aura-components-mcp v1.6.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.
@@ -121,9 +121,38 @@ const esc = (s) => String(s).replaceAll('"', '""');
121
121
  const ilike = (v) => "*" + String(v).replaceAll("*", "").replaceAll(",", " ").trim() + "*";
122
122
 
123
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(","));
124
+ // Theme words (dark/light) are NOT search tokens — they become rank/filter signals
125
+ // (see detectThemeHint + scoreByTheme), so "dark cinematic" can't match light rows.
126
+ const STOP = new Set("a,an,the,for,with,and,or,of,to,in,on,my,new,free,also,that,this,from,into,plus,vs,top,best,up".split(","));
127
+ const THEME_WORDS = new Set(["dark", "light", "midnight", "noir", "black", "white", "bright", "airy", "minimal"]);
125
128
  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);
129
+ return String(q ?? "").toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).map((t) => t.trim()).filter((t) => t.length > 2 && !STOP.has(t) && !THEME_WORDS.has(t)).slice(0, max);
130
+ }
131
+ // Theme hint from the raw query: explicit signal, separate from match tokens.
132
+ function detectThemeHint(q) {
133
+ const t = String(q ?? "").toLowerCase();
134
+ const dark = /\bdark\b|\bmidnight\b|\bnoir\b|\bcinematic\b/.test(t);
135
+ const light = /\blight\b|\bbright\b|\bairy\b/.test(t);
136
+ if (dark && !light) return "dark";
137
+ if (light && !dark) return "light";
138
+ return null;
139
+ }
140
+ // Rank rows: +2 per matched token in title, +1 in description/tags, theme match first.
141
+ // Returns { rows, scored } where scored explains the top pick for fallback_score.
142
+ function scoreRows(kind, rows, toks, themeHint) {
143
+ const scored = rows.map((r) => {
144
+ const title = String(r.title || "").toLowerCase();
145
+ const desc = String(r.description || "").toLowerCase();
146
+ const tags = ((r.tags || []).map((x) => String(x).toLowerCase()).join(" "));
147
+ let s = 0;
148
+ for (const t of toks) { if (title.includes(t)) s += 2; else if (desc.includes(t) || tags.includes(t)) s += 1; }
149
+ const th = r.facets && r.facets.theme;
150
+ const themeOk = !themeHint || th === themeHint || th === "mixed" || th === "unknown";
151
+ if (themeHint && th === themeHint) s += 3;
152
+ return { r, s, themeOk };
153
+ });
154
+ scored.sort((a, b) => ((b.themeOk ? 1 : 0) - (a.themeOk ? 1 : 0)) || (b.s - a.s) || ((b.r.views || 0) - (a.r.views || 0)));
155
+ return scored;
127
156
  }
128
157
 
129
158
  function windowAgoIso(days) {
@@ -132,7 +161,7 @@ function windowAgoIso(days) {
132
161
  return d.toISOString();
133
162
  }
134
163
  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
164
+ function trendingIso(days = 90) { return windowAgoIso(days); } // 7d seed is empty; callers pass window_days (default 30 for components now)
136
165
 
137
166
  function buildSearchParams(kind, q = {}) {
138
167
  const p = new URLSearchParams();
@@ -160,7 +189,7 @@ function buildSearchParams(kind, q = {}) {
160
189
  else if (kind === "assets") ors.push("title.ilike." + v + ",description.ilike." + v);
161
190
  else ors.push("title.ilike." + v + ",description.ilike." + v);
162
191
  }
163
- if (q.sort === "trending") filters.push("created_at=gte." + trendingIso());
192
+ if (q.sort === "trending") filters.push("created_at=gte." + trendingIso(q.window_days ?? 90));
164
193
  return { params: p, filters, ors };
165
194
  }
166
195
 
@@ -171,7 +200,15 @@ function buildUrl(base, table, params, filters, ors) {
171
200
  return base + "/rest/v1/" + table + "?" + qs;
172
201
  }
173
202
 
203
+ // Truncation with cursors: agents see full_length + chunk info instead of a silent
204
+ // marker, and can fetch the rest via getItem with { chunk } (see getItem below).
174
205
  const trunc = (s, n) => (s && s.length > n ? s.slice(0, n) + "…[truncated]" : s);
206
+ function chunkText(s, n, chunk = 0) {
207
+ const full = String(s || "");
208
+ const total = Math.max(1, Math.ceil(full.length / n));
209
+ const i = Math.max(0, Math.min(chunk, total - 1));
210
+ return { text: full.slice(i * n, (i + 1) * n) + (i < total - 1 ? "…[truncated]" : ""), chunk: i, chunks: total, full_length: full.length, truncated: total > 1 };
211
+ }
175
212
 
176
213
  function pageUrl(kind, row) {
177
214
  if (kind === "components") return SITE + "/component/" + row.slug;
@@ -193,12 +230,31 @@ function shapeRow(kind, row, cfg, withFacets) {
193
230
  if (kind === "components" && typeof base.code === "string") {
194
231
  base.code_chars = base.code.length;
195
232
  if (withFacets) { try { base.facets = withFacets(kind, { ...base, code: base.code.slice(0, 600) }, { fullLength: base.code.length }); } catch { /* never break rows */ } }
196
- base.code = trunc(base.code, cfg.codeChars);
233
+ // Gap 3: code_excerpt (first 1200 chars, readable) + full_fetch flag. Agents skim
234
+ // the excerpt, then get chunk N — no more blind 18k-70k dumps or silent cuts.
235
+ base.code_excerpt = String(base.code).slice(0, 1200);
236
+ base.full_fetch = { tool: "aura_get_component", id: base.id ?? base.slug, chunks: Math.max(1, Math.ceil(base.code.length / cfg.codeChars)) };
237
+ const ch = chunkText(base.code, cfg.codeChars, 0);
238
+ base.code = ch.text;
239
+ base.code_info = { full_length: ch.full_length, chunks: ch.chunks, chunk: 0, truncated: ch.truncated };
240
+ if (base.facets) base.facets.deps_summary = base.facets.needsTailwind || base.facets.needsIcons ? [base.facets.needsTailwind && "tailwindcss", base.facets.needsIcons && "iconify-icon"].filter(Boolean) : [];
241
+ }
242
+ if (kind === "skills" && typeof base.content === "string") {
243
+ const ch = chunkText(base.content, cfg.contentChars, 0);
244
+ base.content = ch.text;
245
+ base.content_info = { full_length: ch.full_length, chunks: ch.chunks, chunk: 0, truncated: ch.truncated };
197
246
  }
198
- if (kind === "skills" && typeof base.content === "string") base.content = trunc(base.content, cfg.contentChars);
199
247
  if (kind === "design_systems") {
200
- if (typeof base.content === "string") base.content = trunc(base.content, cfg.contentChars);
201
- if (typeof base.preview_html === "string") base.preview_html = trunc(base.preview_html, cfg.codeChars);
248
+ if (typeof base.content === "string") {
249
+ const ch = chunkText(base.content, cfg.contentChars, 0);
250
+ base.content = ch.text;
251
+ base.content_info = { full_length: ch.full_length, chunks: ch.chunks, chunk: 0, truncated: ch.truncated };
252
+ }
253
+ if (typeof base.preview_html === "string") {
254
+ const ch = chunkText(base.preview_html, cfg.codeChars, 0);
255
+ base.preview_html = ch.text;
256
+ base.preview_info = { full_length: ch.full_length, chunks: ch.chunks, chunk: 0, truncated: ch.truncated };
257
+ }
202
258
  }
203
259
  base.page_url = pageUrl(kind, row);
204
260
  if (kind === "assets") base.asset_key = assetKey(row);
@@ -256,7 +312,11 @@ function createCatalog({ fetcher, config, now = () => Date.now(), facetsFn = nul
256
312
  const offset = Math.max(0, q.offset ?? 0);
257
313
  const sort = CATALOG_SORTS[kind][q.sort] ? q.sort : "popular";
258
314
  const freeOnly = q.freeOnly ?? config.freeOnlyDefault;
259
- const key = ["search", kind, q.query ?? "", q.tag ?? "", q.mediaType ?? "", sort, limit, offset, freeOnly].join("|");
315
+ // Gap 1: negative_theme (exclude a theme) + min_views (quality floor). Both ride
316
+ // the cache key so filtered/unfiltered variants never collide.
317
+ const negTheme = q.negative_theme ? String(q.negative_theme).toLowerCase() : "";
318
+ const minViews = Math.max(0, q.min_views ?? 0);
319
+ const key = ["search", kind, q.query ?? "", q.tag ?? "", q.mediaType ?? "", sort, limit, offset, freeOnly, negTheme, minViews].join("|");
260
320
  const hit = cacheGet(key);
261
321
  if (hit) return { ...hit, cached: true };
262
322
  if (inflight.has(key)) return inflight.get(key);
@@ -270,8 +330,10 @@ function createCatalog({ fetcher, config, now = () => Date.now(), facetsFn = nul
270
330
  let fallback = null;
271
331
  // AND-phrase queries like "dark cinematic portfolio" match nothing literally:
272
332
  // retry as OR over significant tokens so every surface degrades instead of emptying.
273
- // Single-token queries that miss also retry bare (drops tag/media filters) + suggest queries.
333
+ // Theme words (dark/light/cinematic) are rank signals, not match tokens.
274
334
  const toks = (q.query && q.query.trim() && !q.tag) ? queryTokens(q.query) : [];
335
+ const themeHint = (q.query && q.query.trim()) ? detectThemeHint(q.query) : null;
336
+ const effTheme = q.theme || themeHint;
275
337
  const suggest = (t) => {
276
338
  const s = [];
277
339
  if (t.length > 1) s.push(t.slice(0, Math.min(3, t.length)).join(" "));
@@ -322,16 +384,59 @@ function createCatalog({ fetcher, config, now = () => Date.now(), facetsFn = nul
322
384
  return (u && byUrl.get(u) === r) ? { ...r, canonical: true } : r;
323
385
  });
324
386
  }
387
+ // Gap 4: inline snippets — one extra batched fetch fills preview/tokens glimpses
388
+ // into search rows (top N only) so agents decide without a second call per row.
389
+ // Bounded: top 5 rows, client-side sliced to 800 chars. No extra call when empty.
390
+ if ((kind === "design_systems" || kind === "skills") && rows.length) {
391
+ const topIds = rows.slice(0, 5).map((r) => r.id);
392
+ try {
393
+ const idList = topIds.map((id) => encodeURIComponent(String(id))).join(",");
394
+ const sel = kind === "design_systems" ? "id,content,preview_html" : "id,content,source_url";
395
+ const { rows: full } = await fetcher.getJson(base + "/rest/v1/" + kind + "?select=" + encodeURIComponent(sel) + "&id=in.(" + idList + ")", headers);
396
+ const byId = new Map(full.map((r) => [String(r.id), r]));
397
+ rows = rows.map((r) => {
398
+ const f = byId.get(String(r.id));
399
+ if (!f) return r;
400
+ if (kind === "design_systems") return { ...r, preview_snippet: String(f.preview_html || "").slice(0, 800), tokens_snippet: String(f.content || "").slice(0, 800) };
401
+ return { ...r, content_snippet: String(f.content || "").slice(0, 800) };
402
+ });
403
+ } catch { /* snippets are best-effort; rows still return */ }
404
+ }
325
405
  const enriched = await enrichAuthors(rows);
326
406
  const out = { kind, total, limit, offset, items: enriched.map((r) => shapeRow(kind, r, config, facetsFn)), cached: false };
327
407
  if (deduped) out.deduped = deduped;
328
- // Post-filter: theme (dark|light) is derived, not a column — filter shaped rows.
329
- if (q.theme && (kind === "components")) {
330
- const want = String(q.theme).toLowerCase();
331
- out.items = out.items.filter((it) => (it.facets && it.facets.theme) === want);
408
+ // Gap 1: min_views quality floor (views-gated surfaces only) + negative_theme
409
+ // hard exclusion. Both apply before theme re-rank so counts stay honest.
410
+ if (minViews > 0) out.items = out.items.filter((it) => (it.views || 0) >= minViews);
411
+ if (negTheme && kind === "components") {
412
+ const before = out.items.length;
413
+ out.items = out.items.filter((it) => !it.facets || it.facets.theme !== negTheme);
414
+ if (out.items.length < before) out.negative_theme_filtered_out = before - out.items.length;
415
+ }
416
+ // Theme-aware re-rank (components): theme hint from query (dark/light/cinematic)
417
+ // or explicit q.theme reorders so matches come first — never silently drops rows,
418
+ // but reports how many were filtered out of the top for transparency.
419
+ if (kind === "components" && effTheme) {
420
+ const scored = scoreRows(kind, out.items, toks, effTheme);
421
+ const topTheme = scored.filter((s) => s.themeOk).map((s) => s.r);
422
+ const dropped = out.items.length - topTheme.length;
423
+ if (topTheme.length) {
424
+ out.items = topTheme;
425
+ out.theme_hint = effTheme;
426
+ if (dropped) out.theme_filtered_out = dropped;
427
+ } else {
428
+ out.theme_hint = effTheme;
429
+ out.theme_warning = "no " + effTheme + "-theme rows in top results; showing unfiltered, check facets.theme per row";
430
+ }
332
431
  }
333
432
  // ISS15: transparent fallback — agents see what happened, not a magic string.
334
- if (fallback) { out.fallback = fallback; out.isFallback = true; out.fallback_score = "token-overlap:" + toks.length + "-terms"; }
433
+ if (fallback) {
434
+ out.fallback = fallback; out.isFallback = true;
435
+ const scoredTop = out.items[0];
436
+ const topTitle = scoredTop ? String(scoredTop.title || "").toLowerCase() : "";
437
+ const hits = toks.filter((t) => topTitle.includes(t)).length;
438
+ out.fallback_score = "token-overlap:" + hits + "/" + toks.length + "-terms" + (effTheme ? ";theme:" + effTheme : "");
439
+ }
335
440
  else if ((!rows || !rows.length) && toks.length) { out.isFallback = false; out.suggested_queries = suggest(toks).filter((s) => !/^[a-z0-9]{8,}$/i.test(s.replace(/\s/g, ""))); if (!out.suggested_queries.length) out.suggested_queries = ["hero", "pricing", "landing page"]; }
336
441
  cacheSet(key, out);
337
442
  return out;
@@ -340,8 +445,9 @@ function createCatalog({ fetcher, config, now = () => Date.now(), facetsFn = nul
340
445
  try { return await p; } finally { inflight.delete(key); }
341
446
  }
342
447
 
343
- async function getItem(kind, idOrSlug) {
344
- const key = ["get", kind, String(idOrSlug)].join("|");
448
+ async function getItem(kind, idOrSlug, opts = {}) {
449
+ const chunk = Math.max(0, opts.chunk ?? 0);
450
+ const key = ["get", kind, String(idOrSlug), "c" + chunk].join("|");
345
451
  const hit = cacheGet(key);
346
452
  if (hit) return { ...hit, cached: true };
347
453
  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));
@@ -357,7 +463,35 @@ function createCatalog({ fetcher, config, now = () => Date.now(), facetsFn = nul
357
463
  const { rows } = await fetcher.getJson(url, headers);
358
464
  if (!rows.length) { const e = new Error("not found: " + kind + " " + idOrSlug); e.code = "NOT_FOUND"; throw e; }
359
465
  const enriched = await enrichAuthors(rows);
360
- const out = { kind, item: shapeRow(kind, enriched[0], config, facetsFn), cached: false };
466
+ const full = enriched[0];
467
+ // Chunked fetch: re-slice full blobs per requested chunk so agents can page
468
+ // through 18k-70k payloads instead of hitting one silent truncation wall.
469
+ const item = shapeRow(kind, full, config, facetsFn);
470
+ if (chunk > 0) {
471
+ if (kind === "components" && typeof full.code === "string") {
472
+ const ch = chunkText(full.code, config.codeChars, chunk);
473
+ item.code = ch.text;
474
+ item.code_info = { full_length: ch.full_length, chunks: ch.chunks, chunk: ch.chunk, truncated: ch.truncated };
475
+ }
476
+ if ((kind === "skills") && typeof full.content === "string") {
477
+ const ch = chunkText(full.content, config.contentChars, chunk);
478
+ item.content = ch.text;
479
+ item.content_info = { full_length: ch.full_length, chunks: ch.chunks, chunk: ch.chunk, truncated: ch.truncated };
480
+ }
481
+ if (kind === "design_systems") {
482
+ if (typeof full.content === "string") {
483
+ const ch = chunkText(full.content, config.contentChars, chunk);
484
+ item.content = ch.text;
485
+ item.content_info = { full_length: ch.full_length, chunks: ch.chunks, chunk: ch.chunk, truncated: ch.truncated };
486
+ }
487
+ if (typeof full.preview_html === "string") {
488
+ const ch = chunkText(full.preview_html, config.codeChars, chunk);
489
+ item.preview_html = ch.text;
490
+ item.preview_info = { full_length: ch.full_length, chunks: ch.chunks, chunk: ch.chunk, truncated: ch.truncated };
491
+ }
492
+ }
493
+ }
494
+ const out = { kind, item, cached: false };
361
495
  cacheSet(key, out);
362
496
  return out;
363
497
  }
@@ -399,6 +533,19 @@ function createCatalog({ fetcher, config, now = () => Date.now(), facetsFn = nul
399
533
  return out;
400
534
  }
401
535
 
536
+ // Gap 2: editorial picks — human-grade component leads for when views-signal is
537
+ // noise (free trending total=2). Top free by forks (remix = real reuse), then views.
538
+ async function editorialPicks(limit = 5) {
539
+ const lim = Math.max(1, Math.min(limit || 5, 8));
540
+ const sel = encodeURIComponent(LIST_COLS.components);
541
+ const url = base + "/rest/v1/components?select=" + sel + "&private=eq.false&premium=eq.false&order=forks.desc,views.desc&limit=" + lim;
542
+ try {
543
+ const { rows } = await fetcher.getJson(url, headers);
544
+ const enriched = await enrichAuthors(rows);
545
+ return enriched.map((r) => shapeRow("components", r, config, facetsFn));
546
+ } catch { return []; }
547
+ }
548
+
402
549
  // Bulk fetch: N details in one parallel round (backs aura_bundle + aura_scaffold_page).
403
550
  // Per-id errors are captured, never thrown: { ok:true, item } or { ok:false, id, error }.
404
551
  async function bundleItems(kind, ids, max = 5) {
@@ -431,11 +578,11 @@ function createCatalog({ fetcher, config, now = () => Date.now(), facetsFn = nul
431
578
  } catch { return { item: shapeRow(kind, item, config), related: [] }; }
432
579
  }
433
580
 
434
- return { searchCatalog, getItem, getStatus, categoryCounts, bundleItems, relatedItems };
581
+ return { searchCatalog, getItem, getStatus, categoryCounts, bundleItems, relatedItems, editorialPicks };
435
582
  }
436
583
 
437
584
  // ---- protocol.mjs ----
438
- const SERVER_INFO = { name: "aura-components-mcp", version: "1.6.0" };
585
+ const SERVER_INFO = { name: "aura-components-mcp", version: "1.6.1" };
439
586
  // Module: protocol — stdio JSON-RPC framing + MCP handshake + error codes.
440
587
  // One interface: createSession(send) -> { dispatch(msg) }. No business logic.
441
588
 
@@ -576,6 +723,10 @@ function facets(kind, row, opts) {
576
723
  // catalogue content is the exclusive property of DESIGNCODE IO PTE. LTD.;
577
724
  // no per-asset license column exists, so commercial reuse needs Aura's permission.
578
725
  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)';
726
+ // Gap 5: machine-readable license gate. commercial_ok:false always (no per-asset
727
+ // column exists to prove otherwise) — agents filter without parsing prose.
728
+ f.commercial_ok = false;
729
+ f.terms_url = 'https://www.aura.build/terms';
579
730
  f.download = row.image_original || row.image_1600w || row.image_800w || row.video_url || null;
580
731
  f.preview = row.image_800w || row.video_poster_url || null;
581
732
  }
@@ -713,148 +864,161 @@ function tokenHints(content, previewHtml) {
713
864
  };
714
865
 
715
866
  // ---- tools.mjs ----
716
- // Module: tools — thin adapters. Each tool validates args at the seam,
717
- // calls one catalog/guide method, wraps textResult. No retries/caching here.
718
-
719
-
720
- const TOOL_KINDS = ['components', 'skills', 'assets', 'design_systems'];
721
- const TOOL_SORTS = ['popular', 'recent', 'trending', 'updated'];
722
- const TOOL_CATS = ['hero','section','button','card','background','header','logo','feature','pricing','testimonial','footer','form','heading'];
723
-
724
- function toolFn_bad(msg) { const e = new Error(msg); e.code = 'BAD_ARGS'; throw e; }
725
- function toolFn_str(v, name) { if (v !== undefined && typeof v !== 'string') toolFn_bad(name + ' must be a string'); return v; }
726
- function toolFn_num(v, name) { if (v !== undefined && typeof v !== 'number') toolFn_bad(name + ' must be a number'); return v; }
727
- function toolFn_bool(v, name) { if (v !== undefined && typeof v !== 'boolean') toolFn_bad(name + ' must be a boolean'); return v; }
728
- function toolFn_sort(v) { if (v !== undefined && !TOOL_SORTS.includes(v)) toolFn_bad('sort must be one of ' + TOOL_SORTS.join('|')); return v; }
729
- function theme(v) { if (v !== undefined && v !== 'dark' && v !== 'light') toolFn_bad('theme must be dark|light'); return v; }
730
- 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; }
731
- function toolFn_cat(v) { if (v !== undefined && !TOOL_CATS.includes(v)) toolFn_bad('category must be one of ' + TOOL_CATS.join('|')); return v; }
732
-
733
- const TOOL_DEFS = [
734
- { 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 } },
735
- { 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 } },
736
- { 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 } },
737
- { 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 } },
738
- { 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 } },
739
- { 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 } },
740
- { 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 } },
741
- { 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 } },
742
- { 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 } },
743
- { 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 } },
744
- { 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 } },
745
- { 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 } },
746
- { 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 } },
747
- { 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 } },
748
- { 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 } },
749
- { 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 } },
750
- { 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 } },
751
- { 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 } },
752
- { 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 } },
753
- { name: 'aura_categories', description: 'The 13 component categories with live free counts. Pick one, then search within it.', inputSchema: { type: 'object', properties: {}, additionalProperties: false } },
754
- ];
755
-
756
- function createHandlers(catalog) {
757
- const free = { freeOnly: true };
758
- return {
759
- aura_status: async () => textResult(await catalog.getStatus()),
760
- aura_search_components: async (a) => { a = a || {}; return textResult(await catalog.searchCatalog('components', {
761
- query: toolFn_str(a.query, 'query'), tag: toolFn_cat(a.category), freeOnly: true, theme: theme(a.theme),
762
- sort: toolFn_sort(a.sort), limit: toolFn_num(a.limit, 'limit'), offset: toolFn_num(a.offset, 'offset'),
763
- })); },
764
- aura_get_component: async (a) => { a = a || {}; const id = a.id !== undefined ? a.id : a.slug;
765
- if (id === undefined || (typeof id !== 'string' && typeof id !== 'number')) toolFn_bad('provide id (number) or slug (string)');
766
- return textResult(await catalog.getItem('components', id)); },
767
- aura_search_skills: async (a) => { a = a || {}; return textResult(await catalog.searchCatalog('skills', {
768
- query: toolFn_str(a.query, 'query'), sort: toolFn_sort(a.sort), limit: toolFn_num(a.limit, 'limit'), offset: toolFn_num(a.offset, 'offset'),
769
- })); },
770
- aura_get_skill: async (a) => { a = a || {}; if (typeof a.id !== 'string' || !a.id) toolFn_bad('id (string) is required');
771
- return textResult(await catalog.getItem('skills', a.id)); },
772
- aura_search_assets: async (a) => { a = a || {}; return textResult(await catalog.searchCatalog('assets', {
773
- query: toolFn_str(a.query, 'query'),
774
- mediaType: a.mediaType === undefined ? undefined : (a.mediaType === 'image' || a.mediaType === 'video' ? a.mediaType : toolFn_bad('mediaType must be image|video')),
775
- freeOnly: true, sort: toolFn_sort(a.sort), limit: toolFn_num(a.limit, 'limit'), offset: toolFn_num(a.offset, 'offset'),
776
- })); },
777
- aura_search_design_systems: async (a) => { a = a || {}; return textResult(await catalog.searchCatalog('design_systems', {
778
- query: toolFn_str(a.query, 'query'), sort: toolFn_sort(a.sort), limit: toolFn_num(a.limit, 'limit'), offset: toolFn_num(a.offset, 'offset'),
779
- })); },
780
- aura_get_design_system: async (a) => { a = a || {}; const id = a.id !== undefined ? a.id : a.slug;
781
- if (typeof id !== 'string' || !id) toolFn_bad('provide id or slug (string)');
782
- return textResult(await catalog.getItem('design_systems', id)); },
783
- aura_search_all: async (a) => { a = a || {}; const query = toolFn_str(a.query, 'query'), limit = toolFn_num(a.limit, 'limit') || 5;
784
- const r = await Promise.all([
785
- catalog.searchCatalog('components', { query, freeOnly: true, limit }),
786
- catalog.searchCatalog('skills', { query, limit }),
787
- catalog.searchCatalog('assets', { query, freeOnly: true, limit }),
788
- catalog.searchCatalog('design_systems', { query, limit }),
789
- ]);
790
- return textResult({ query: query || null, components: r[0], skills: r[1], assets: r[2], design_systems: r[3] }); },
791
- aura_recommend: async (a) => { a = a || {}; if (typeof a.goal !== 'string' || !a.goal.trim()) toolFn_bad('goal (string) is required');
792
- const r = await Promise.all([
793
- catalog.searchCatalog('components', { query: a.goal, freeOnly: true, limit: 5 }),
794
- catalog.searchCatalog('skills', { query: a.goal, limit: 5 }),
795
- catalog.searchCatalog('assets', { query: a.goal, freeOnly: true, limit: 3 }),
796
- catalog.searchCatalog('design_systems', { query: a.goal, limit: 3 }),
797
- ]);
798
- const pick = (x) => (x.items || []).slice(0, 3).map((i) => ({ title: i.title, page_url: i.page_url }));
799
- return textResult({ goal: a.goal, freeOnly: true,
800
- rationale: 'Top free catalogue hits per surface, ranked by Aura views. Open the page_url previews, then fetch full source or content for finalists.',
801
- components: r[0], skills: r[1], assets: r[2], design_systems: r[3],
802
- starter_kit: { components: pick(r[0]), skills: pick(r[1]), assets: pick(r[2]), design_systems: pick(r[3]) } }); },
803
- aura_install_component: async (a) => { a = a || {}; const id = a.id !== undefined ? a.id : a.slug;
804
- if (id === undefined || (typeof id !== 'string' && typeof id !== 'number')) toolFn_bad('provide id (number) or slug (string)');
805
- const got = await catalog.getItem('components', id);
806
- if (got.item && got.item.premium !== false) toolFn_bad('that component is Pro (paid). Search with freeOnly or pick a free one.');
807
- return textResult({ item: got.item, install: installGuide('components', got.item) }); },
808
- aura_install_skill: async (a) => { a = a || {}; if (typeof a.id !== 'string' || !a.id) toolFn_bad('id (string) is required');
809
- const got = await catalog.getItem('skills', a.id);
810
- return textResult({ item: got.item, install: installGuide('skills', got.item) }); },
811
- aura_use_design_system: async (a) => { a = a || {}; const id = a.id !== undefined ? a.id : a.slug;
812
- if (typeof id !== 'string' || !id) toolFn_bad('provide id or slug (string)');
813
- const got = await catalog.getItem('design_systems', id);
814
- return textResult({ item: got.item, install: installGuide('design_systems', got.item), tokens: tokenHints(got.item.content || '', got.item.preview_html || '') }); },
815
- aura_trending: async (a) => { a = a || {}; const limit = toolFn_num(a.limit, 'limit') || 5;
816
- const r = await Promise.all([
817
- catalog.searchCatalog('components', { freeOnly: true, sort: 'trending', limit }),
818
- catalog.searchCatalog('skills', { sort: 'trending', limit }),
819
- catalog.searchCatalog('assets', { freeOnly: true, sort: 'trending', limit }),
820
- catalog.searchCatalog('design_systems', { sort: 'trending', limit }),
821
- ]);
822
- return textResult({ window: 'last 90 days by views', window_note: '7-day seed is empty (newest catalogue rows are months old); 90d keeps trending meaningful. Components here are low-signal (views 0-5): prefer skills/assets trending.', components: r[0], skills: r[1], assets: r[2], design_systems: r[3] }); },
823
- aura_categories: async () => textResult({ categories: await catalog.categoryCounts() }),
824
- aura_bundle: async (a) => { a = a || {}; const ids = idList(a.ids, 'ids') || idList(a.slugs, 'slugs');
825
- if (!ids || !ids.length) toolFn_bad('provide ids (array of 1-8 numbers/strings) or slugs (array of strings)');
826
- return textResult({ kind: 'components', results: await catalog.bundleItems('components', ids) }); },
827
- aura_related: async (a) => { a = a || {}; const k = a.kind;
828
- if (k !== 'components' && k !== 'skills' && k !== 'design_systems') toolFn_bad('kind must be components|skills|design_systems');
829
- const id = a.id; if (typeof id !== 'string' && typeof id !== 'number') toolFn_bad('id is required');
830
- return textResult(await catalog.relatedItems(k, id, 3)); },
831
- aura_install_asset: async (a) => { a = a || {}; if (typeof a.id !== 'number') toolFn_bad('id (number) is required');
832
- const got = await catalog.getItem('assets', a.id);
833
- const it = got.item; const fx = (it.facets || {});
834
- 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.'] } }); },
835
- aura_bulk_fetch: async (a) => { a = a || {}; const kind = a.kind;
836
- if (kind !== 'components' && kind !== 'design_systems' && kind !== 'assets') toolFn_bad('kind must be components|design_systems|assets');
837
- const ids = idList(a.ids, 'ids');
838
- if (!ids || !ids.length) toolFn_bad('provide ids (array of 1-8)');
839
- return textResult({ kind, results: await catalog.bundleItems(kind, ids) }); },
840
- aura_scaffold_page: async (a) => { a = a || {}; if (typeof a.goal !== 'string' || !a.goal.trim()) toolFn_bad('goal (string) is required');
841
- const sysRef = (typeof a.system === 'string' && a.system) ? a.system : null;
842
- const compRefs = idList(a.components, 'components') || [];
843
- const [sys, comps] = await Promise.all([
844
- 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)),
845
- 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),
846
- ]);
847
- const sysItem = sys && sys.item ? sys.item : null;
848
- const toks = sysItem ? tokenHints(sysItem.content || '') : { tokens: {}, css: '' };
849
- const guides = (comps || []).map((c) => installGuide('components', c));
850
- const deps = []; const seen = new Set();
851
- for (const g of guides) for (const d of (g.deps || [])) if (!seen.has(d.name)) { seen.add(d.name); deps.push(d); }
852
- const files = [{ path: 'styles/tokens.css', contains: 'tokens.css from use_design_system' }];
853
- if (sysItem) files.push({ path: 'DESIGN.md', contains: 'system rules' });
854
- for (const g of guides) for (const f of (g.files || [])) files.push(f);
855
- 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 }); },
856
- };
857
- };
867
+ // Module: tools — thin adapters. Each tool validates args at the seam,
868
+ // calls one catalog/guide method, wraps textResult. No retries/caching here.
869
+ const TOOL_KINDS = ['components', 'skills', 'assets', 'design_systems'];
870
+ const TOOL_SORTS = ['popular', 'recent', 'trending', 'updated'];
871
+ const TOOL_CATS = ['hero','section','button','card','background','header','logo','feature','pricing','testimonial','footer','form','heading'];
872
+ function toolFn_bad(msg) { const e = new Error(msg); e.code = 'BAD_ARGS'; throw e; }
873
+ function toolFn_str(v, name) { if (v !== undefined && typeof v !== 'string') toolFn_bad(name + ' must be a string'); return v; }
874
+ function toolFn_num(v, name) { if (v !== undefined && typeof v !== 'number') toolFn_bad(name + ' must be a number'); return v; }
875
+ function toolFn_bool(v, name) { if (v !== undefined && typeof v !== 'boolean') toolFn_bad(name + ' must be a boolean'); return v; }
876
+ function toolFn_sort(v) { if (v !== undefined && !TOOL_SORTS.includes(v)) toolFn_bad('sort must be one of ' + TOOL_SORTS.join('|')); return v; }
877
+ function theme(v) { if (v !== undefined && v !== 'dark' && v !== 'light') toolFn_bad('theme must be dark|light'); return v; }
878
+ 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; }
879
+ function toolFn_cat(v) { if (v !== undefined && !TOOL_CATS.includes(v)) toolFn_bad('category must be one of ' + TOOL_CATS.join('|')); return v; }
880
+ const TOOL_DEFS = [
881
+ { 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 } },
882
+ { name: 'aura_search_components', description: 'Search free Aura UI components (2,495). Text over title and description, optional category tag, theme dark|light filter, negative_theme exclusion, min_views floor, 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'] }, negative_theme: { type: 'string', enum: ['dark', 'light'] }, min_views: { type: 'number' }, sort: { type: 'string', enum: TOOL_SORTS }, limit: { type: 'number' }, offset: { type: 'number' } }, additionalProperties: false } },
883
+ { name: 'aura_get_component', description: 'Full free component detail with HTML/Tailwind source, preview image, page URL. Numeric id or slug. Chunk param pages large code (see code_info).', inputSchema: { type: 'object', properties: { id: {}, slug: { type: 'string' }, chunk: { type: 'number' } }, additionalProperties: false } },
884
+ { 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 } },
885
+ { name: 'aura_get_skill', description: 'Full free agent-skill content (SKILL.md body) plus source_url and page URL. Skill id. Chunk param pages large bodies (see content_info).', inputSchema: { type: 'object', properties: { id: { type: 'string' }, chunk: { type: 'number' } }, required: ['id'], additionalProperties: false } },
886
+ { 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'] }, commercial_ok: { type: 'boolean' }, sort: { type: 'string', enum: TOOL_SORTS }, limit: { type: 'number' }, offset: { type: 'number' } }, additionalProperties: false } },
887
+ { 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 } },
888
+ { name: 'aura_get_design_system', description: 'Full free DESIGN.md content plus preview_html, tokens, and page URL. System id or slug. Chunk param pages large preview_html (see preview_info).', inputSchema: { type: 'object', properties: { id: { type: 'string' }, slug: { type: 'string' }, chunk: { type: 'number' } }, additionalProperties: false } },
889
+ { 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 } },
890
+ { 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 } },
891
+ { 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 } },
892
+ { 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 } },
893
+ { 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 } },
894
+ { 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 } },
895
+ { 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 } },
896
+ { 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 } },
897
+ { 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 } },
898
+ { 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 } },
899
+ { 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 } },
900
+ { name: 'aura_categories', description: 'The 13 component categories with live free counts. Pick one, then search within it.', inputSchema: { type: 'object', properties: {}, additionalProperties: false } },
901
+ ];
902
+ function createHandlers(catalog) {
903
+ const free = { freeOnly: true };
904
+ return {
905
+ aura_status: async () => textResult(await catalog.getStatus()),
906
+ aura_search_components: async (a) => { a = a || {}; return textResult(await catalog.searchCatalog('components', {
907
+ query: toolFn_str(a.query, 'query'), tag: toolFn_cat(a.category), freeOnly: true, theme: theme(a.theme),
908
+ negative_theme: a.negative_theme === undefined ? undefined : theme(a.negative_theme),
909
+ min_views: toolFn_num(a.min_views, 'min_views'),
910
+ sort: toolFn_sort(a.sort), limit: toolFn_num(a.limit, 'limit'), offset: toolFn_num(a.offset, 'offset'),
911
+ })); },
912
+ aura_get_component: async (a) => { a = a || {}; const id = a.id !== undefined ? a.id : a.slug;
913
+ if (id === undefined || (typeof id !== 'string' && typeof id !== 'number')) toolFn_bad('provide id (number) or slug (string)');
914
+ const chunk = a.chunk === undefined ? 0 : toolFn_num(a.chunk, 'chunk');
915
+ return textResult(await catalog.getItem('components', id, { chunk })); },
916
+ aura_search_skills: async (a) => { a = a || {}; return textResult(await catalog.searchCatalog('skills', {
917
+ query: toolFn_str(a.query, 'query'), sort: toolFn_sort(a.sort), limit: toolFn_num(a.limit, 'limit'), offset: toolFn_num(a.offset, 'offset'),
918
+ })); },
919
+ aura_get_skill: async (a) => { a = a || {}; if (typeof a.id !== 'string' || !a.id) toolFn_bad('id (string) is required');
920
+ const chunk = a.chunk === undefined ? 0 : toolFn_num(a.chunk, 'chunk');
921
+ return textResult(await catalog.getItem('skills', a.id, { chunk })); },
922
+ aura_search_assets: async (a) => { a = a || {}; const co = a.commercial_ok === undefined ? undefined : toolFn_bool(a.commercial_ok, 'commercial_ok');
923
+ const r = await catalog.searchCatalog('assets', { query: toolFn_str(a.query, 'query'), mediaType: a.mediaType === undefined ? undefined : (a.mediaType === 'image' || a.mediaType === 'video' ? a.mediaType : toolFn_bad('mediaType must be image|video')), freeOnly: true, sort: toolFn_sort(a.sort), limit: toolFn_num(a.limit, 'limit'), offset: toolFn_num(a.offset, 'offset') });
924
+ // Gap 5: commercial_ok filter (all-rights-reserved per Aura Terms S4).
925
+ // commercial_ok:true returns [] with explanation, never silence.
926
+ if (co === true) return textResult({ ...r, items: [], commercial_ok: false, commercial_note: 'All Aura catalogue assets are all-rights-reserved per Aura Terms S4. Preview/personal use via page_url; commercial reuse needs Aura permission.' });
927
+ if (co === true) return textResult({ ...r, items: [], commercial_ok: false, commercial_note: 'x' });
928
+ if (co === true) return textResult({ ...r, items: [], commercial_ok: false, commercial_note: 'All Aura catalogue assets are all-rights-reserved per Aura Terms S4.' });
929
+ return textResult(r); },
930
+ aura_search_design_systems: async (a) => { a = a || {}; return textResult(await catalog.searchCatalog('design_systems', {
931
+ query: toolFn_str(a.query, 'query'), sort: toolFn_sort(a.sort), limit: toolFn_num(a.limit, 'limit'), offset: toolFn_num(a.offset, 'offset'),
932
+ })); },
933
+ aura_get_design_system: async (a) => { a = a || {}; const id = a.id !== undefined ? a.id : a.slug;
934
+ if (typeof id !== 'string' || !id) toolFn_bad('provide id or slug (string)');
935
+ const chunk = a.chunk === undefined ? 0 : toolFn_num(a.chunk, 'chunk');
936
+ return textResult(await catalog.getItem('design_systems', id, { chunk })); },
937
+ aura_search_all: async (a) => { a = a || {}; const query = toolFn_str(a.query, 'query'), limit = toolFn_num(a.limit, 'limit') || 5;
938
+ const r = await Promise.all([
939
+ catalog.searchCatalog('components', { query, freeOnly: true, limit }),
940
+ catalog.searchCatalog('skills', { query, limit }),
941
+ catalog.searchCatalog('assets', { query, freeOnly: true, limit }),
942
+ catalog.searchCatalog('design_systems', { query, limit }),
943
+ ]);
944
+ return textResult({ query: query || null, components: r[0], skills: r[1], assets: r[2], design_systems: r[3] }); },
945
+ aura_recommend: async (a) => { a = a || {}; if (typeof a.goal !== 'string' || !a.goal.trim()) toolFn_bad('goal (string) is required');
946
+ // Gap 1: recommend hard-filters theme (not just reranks) — starter kits must not
947
+ // mix light rows into a dark ask. min_views gates noise; both are explicit params.
948
+ const themeV = a.theme === undefined ? undefined : theme(a.theme);
949
+ const negative_themeV = a.negative_theme === undefined ? undefined : theme(a.negative_theme);
950
+ const min_views = toolFn_num(a.min_views, 'min_views');
951
+ const r = await Promise.all([
952
+ catalog.searchCatalog('components', { query: a.goal, freeOnly: true, limit: 8, theme: themeV, negative_theme: negative_themeV, min_views }),
953
+ catalog.searchCatalog('skills', { query: a.goal, limit: 5 }),
954
+ catalog.searchCatalog('assets', { query: a.goal, freeOnly: true, limit: 3 }),
955
+ catalog.searchCatalog('design_systems', { query: a.goal, limit: 3 }),
956
+ ]);
957
+ r[0].items = r[0].items.slice(0, 5);
958
+ const pick = (x) => (x.items || []).slice(0, 3).map((i) => ({ title: i.title, page_url: i.page_url }));
959
+ return textResult({ goal: a.goal, freeOnly: true,
960
+ rationale: 'Top free catalogue hits per surface, ranked by Aura views. Open the page_url previews, then fetch full source or content for finalists.',
961
+ components: r[0], skills: r[1], assets: r[2], design_systems: r[3],
962
+ starter_kit: { components: pick(r[0]), skills: pick(r[1]), assets: pick(r[2]), design_systems: pick(r[3]) } }); },
963
+ aura_install_component: async (a) => { a = a || {}; const id = a.id !== undefined ? a.id : a.slug;
964
+ if (id === undefined || (typeof id !== 'string' && typeof id !== 'number')) toolFn_bad('provide id (number) or slug (string)');
965
+ const got = await catalog.getItem('components', id);
966
+ if (got.item && got.item.premium !== false) toolFn_bad('that component is Pro (paid). Search with freeOnly or pick a free one.');
967
+ return textResult({ item: got.item, install: installGuide('components', got.item) }); },
968
+ aura_install_skill: async (a) => { a = a || {}; if (typeof a.id !== 'string' || !a.id) toolFn_bad('id (string) is required');
969
+ const got = await catalog.getItem('skills', a.id);
970
+ return textResult({ item: got.item, install: installGuide('skills', got.item) }); },
971
+ aura_use_design_system: async (a) => { a = a || {}; const id = a.id !== undefined ? a.id : a.slug;
972
+ if (typeof id !== 'string' || !id) toolFn_bad('provide id or slug (string)');
973
+ const got = await catalog.getItem('design_systems', id);
974
+ return textResult({ item: got.item, install: installGuide('design_systems', got.item), tokens: tokenHints(got.item.content || '', got.item.preview_html || '') }); },
975
+ aura_trending: async (a) => { a = a || {}; const limit = toolFn_num(a.limit, 'limit') || 5;
976
+ // Gap 2: 30d rolling window + editorial picks. Components at 90d/views are noise
977
+ // (total 2, views 0-5); skills/assets already work and keep their own signal.
978
+ const windowDays = a.window_days === undefined ? 30 : toolFn_num(a.window_days, 'window_days');
979
+ const r = await Promise.all([
980
+ catalog.searchCatalog('components', { freeOnly: true, sort: 'trending', limit, window_days: windowDays }),
981
+ catalog.searchCatalog('skills', { sort: 'trending', limit }),
982
+ catalog.searchCatalog('assets', { freeOnly: true, sort: 'trending', limit }),
983
+ catalog.searchCatalog('design_systems', { sort: 'trending', limit }),
984
+ ]);
985
+ const picks = await catalog.editorialPicks(Math.min(limit, 5)).catch(() => []);
986
+ return textResult({ window: 'last ' + windowDays + ' days by views', window_note: 'Components at 90d/views are low-signal (total 2, views 0-5): see editorial_picks for human-grade component leads; skills/assets trending is organic.', editorial_picks: picks, components: r[0], skills: r[1], assets: r[2], design_systems: r[3] });},
987
+ aura_categories: async () => textResult({ categories: await catalog.categoryCounts() }),
988
+ aura_bundle: async (a) => { a = a || {}; const ids = idList(a.ids, 'ids') || idList(a.slugs, 'slugs');
989
+ if (!ids || !ids.length) toolFn_bad('provide ids (array of 1-8 numbers/strings) or slugs (array of strings)');
990
+ return textResult({ kind: 'components', results: await catalog.bundleItems('components', ids) }); },
991
+ aura_related: async (a) => { a = a || {}; const k = a.kind;
992
+ if (k !== 'components' && k !== 'skills' && k !== 'design_systems') toolFn_bad('kind must be components|skills|design_systems');
993
+ const id = a.id; if (typeof id !== 'string' && typeof id !== 'number') toolFn_bad('id is required');
994
+ return textResult(await catalog.relatedItems(k, id, 3)); },
995
+ aura_install_asset: async (a) => { a = a || {}; if (typeof a.id !== 'number') toolFn_bad('id (number) is required');
996
+ const got = await catalog.getItem('assets', a.id);
997
+ const it = got.item; const fx = (it.facets || {});
998
+ 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.'] } }); },
999
+ aura_bulk_fetch: async (a) => { a = a || {}; const kind = a.kind;
1000
+ if (kind !== 'components' && kind !== 'design_systems' && kind !== 'assets') toolFn_bad('kind must be components|design_systems|assets');
1001
+ const ids = idList(a.ids, 'ids');
1002
+ if (!ids || !ids.length) toolFn_bad('provide ids (array of 1-8)');
1003
+ return textResult({ kind, results: await catalog.bundleItems(kind, ids) }); },
1004
+ aura_scaffold_page: async (a) => { a = a || {}; if (typeof a.goal !== 'string' || !a.goal.trim()) toolFn_bad('goal (string) is required');
1005
+ const sysRef = (typeof a.system === 'string' && a.system) ? a.system : null;
1006
+ const compRefs = idList(a.components, 'components') || [];
1007
+ const [sys, comps] = await Promise.all([
1008
+ 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)),
1009
+ 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),
1010
+ ]);
1011
+ const sysItem = sys && sys.item ? sys.item : null;
1012
+ const toks = sysItem ? tokenHints(sysItem.content || '') : { tokens: {}, css: '' };
1013
+ const guides = (comps || []).map((c) => installGuide('components', c));
1014
+ const deps = []; const seen = new Set();
1015
+ for (const g of guides) for (const d of (g.deps || [])) if (!seen.has(d.name)) { seen.add(d.name); deps.push(d); }
1016
+ const files = [{ path: 'styles/tokens.css', contains: 'tokens.css from use_design_system' }];
1017
+ if (sysItem) files.push({ path: 'DESIGN.md', contains: 'system rules' });
1018
+ for (const g of guides) for (const f of (g.files || [])) files.push(f);
1019
+ 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 }); },
1020
+ };
1021
+ };
858
1022
 
859
1023
  // ---- server.mjs (main) ----
860
1024
  // Module: server — composition root. Wires config -> fetcher -> catalog ->
package/package.json CHANGED
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "name": "aura-components-mcp",
3
- "version": "1.6.0",
4
- "description": "Free MCP server for the Aura public catalogue: 2,495 components, 187 skills, 30k assets, 725 design systems. Never-empty search, bulk fetch, scaffold pages, install plans. No login.",
3
+ "version": "1.6.2",
5
4
  "description": "Free-only MCP server for the Aura (aura.build) public catalogue: 2,495 components, 187 skills, 30,688 assets, 725 design systems. No login. Built by swastiksingh-dev.",
6
5
  "type": "module",
7
6
  "license": "MIT",