@astralkit/mcp 1.5.0 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/review.js ADDED
@@ -0,0 +1,182 @@
1
+ // review_app — a STRUCTURAL audit (the architectural sibling of validate_code's
2
+ // visual audit). It flags high-signal "messy app" smells: a missing four-state
3
+ // contract, swallowed errors, atomic-design/separation-of-concerns violations,
4
+ // missing mobile navigation, and Radix a11y gaps. Heuristic, not a compiler —
5
+ // deliberately conservative (only flags what it is confident about) so agents
6
+ // trust it. Rules mirror the named anti-patterns in docs/empty-states-guide.md,
7
+ // docs/error-handing.md, and docs/CODE_ARCHITECTURE_GUIDE.md.
8
+ const FILE_SIZE_LIMIT = 300;
9
+ // Strip line/block comments so we don't flag commented-out code.
10
+ function decomment(code) {
11
+ return code.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:])\/\/[^\n]*/g, '$1');
12
+ }
13
+ function countLines(s) {
14
+ return s.split('\n').length;
15
+ }
16
+ // ── Checks that work on any code snippet ──────────────────────────────────────
17
+ function snippetChecks(raw, where) {
18
+ const issues = [];
19
+ const code = decomment(raw);
20
+ const at = where ? ` (${where})` : '';
21
+ // --- State contract: the "Ghost" (render nothing when empty) ---
22
+ // `{items.length > 0 && <JSX>}` or `{items.length && <JSX>}` with no empty branch.
23
+ const ghost = /\{\s*[\w.]+\.length\s*(?:>\s*0\s*)?&&\s*[(<]/.test(code);
24
+ const hasEmptyHandling = /length\s*===\s*0|length\s*<\s*1|!\w+\??\.length|\bEmptyState\b|isEmpty|no[\s-]?results/i.test(code);
25
+ if (ghost && !hasEmptyHandling) {
26
+ issues.push({
27
+ rule: 'ghost-render',
28
+ severity: 'high',
29
+ detail: `Renders a list only when it has items, with no empty branch${at} — users see blank space and assume it's broken (the "Ghost" anti-pattern).`,
30
+ fix: 'Add an explicit empty branch: `if (!data?.length) return <EmptyState .../>` before rendering the list. See get_build_standards topic "state-lifecycle".',
31
+ });
32
+ }
33
+ // --- State contract: the "Naked Empty" ---
34
+ if (/return\s*\(?\s*<\w+[^>]*>\s*(No data|Nothing here|No results|Empty|None)\b/i.test(code)) {
35
+ issues.push({
36
+ rule: 'naked-empty-state',
37
+ severity: 'medium',
38
+ detail: `A bare "No data"-style empty state${at} with no icon, explanation, or action.`,
39
+ fix: 'Use a real empty state (icon + title + description + CTA) — reuse atoms/empty-state.tsx. See get_build_standards topic "empty-states".',
40
+ });
41
+ }
42
+ // --- State contract: a data view with no loading/empty handling ---
43
+ const isDataView = /\buseQuery\s*\(|\buseSWR\s*\(/.test(code);
44
+ const handlesLoadingOrEmpty = /isLoading|isPending|isFetching|Skeleton|EmptyState|length\s*===\s*0|!\w+\??\.length/.test(code);
45
+ if (isDataView && !handlesLoadingOrEmpty) {
46
+ issues.push({
47
+ rule: 'incomplete-states',
48
+ severity: 'medium',
49
+ detail: `Fetches data${at} but appears to render no loading skeleton or empty state.`,
50
+ fix: 'Handle all four states: loading (skeleton) -> empty -> error -> data. See get_build_standards topic "state-lifecycle".',
51
+ });
52
+ }
53
+ // --- Error handling: empty catch ---
54
+ if (/catch\s*\([^)]*\)\s*\{\s*\}/.test(code)) {
55
+ issues.push({
56
+ rule: 'empty-catch',
57
+ severity: 'high',
58
+ detail: `An empty catch block${at} silently swallows errors.`,
59
+ fix: 'Every catch must log AND update the UI (set an error state / show a recovery toast). See get_build_standards topic "error-handling".',
60
+ });
61
+ }
62
+ // --- Error handling: alert() ---
63
+ if (/(?:^|[^.\w])alert\s*\(/.test(code)) {
64
+ issues.push({
65
+ rule: 'alert-used',
66
+ severity: 'high',
67
+ detail: `Uses alert()${at} for user feedback.`,
68
+ fix: 'Replace alert() with a toast (react-hot-toast / Sonner / Radix Toast). Error toasts should not auto-dismiss.',
69
+ });
70
+ }
71
+ // --- Error handling: raw error rendered to the UI (the "Confuser") ---
72
+ if (/\{\s*\w*[eE]rr\w*\.message\s*\}/.test(code) || /Error\s*\d{3}\b/.test(code) || /ECONNREFUSED|ETIMEDOUT/.test(code)) {
73
+ issues.push({
74
+ rule: 'raw-error-shown',
75
+ severity: 'medium',
76
+ detail: `A raw error message or error code is shown to the user${at}.`,
77
+ fix: 'Show a plain-language title + recovery action; log the technical detail. Never surface stack traces / status codes / errno to users. See get_build_standards topic "error-handling".',
78
+ });
79
+ }
80
+ // --- Responsive: a nav with no mobile menu ---
81
+ const looksLikeNav = /<nav[\s>]|role=["']navigation["']|<header[\s>]/.test(code);
82
+ const hasDesktopLinks = /hidden\s+md:|md:flex|md:items-center/.test(code);
83
+ const hasMobileMenu = /md:hidden|Sheet|Drawer|Dialog|hamburger|mobile[-\s]?menu|MobileMenu|\bList\b.*weight|aria-label=["'][^"']*menu/i.test(code);
84
+ if (looksLikeNav && hasDesktopLinks && !hasMobileMenu) {
85
+ issues.push({
86
+ rule: 'missing-mobile-menu',
87
+ severity: 'high',
88
+ detail: `Navigation${at} has desktop links but appears to have no mobile menu (no hamburger / Dialog sheet / md:hidden toggle).`,
89
+ fix: 'Add a hamburger (md:hidden) that opens a Radix Dialog used as a side sheet (w-[85vw] max-w-sm). See get_build_standards topic "responsive" and get_screen_blueprint("nav").',
90
+ });
91
+ }
92
+ // --- Radix: Dialog/AlertDialog content without a Title (a11y) ---
93
+ const hasDialogContent = /DialogContent|Dialog\.Content|AlertDialogContent|AlertDialog\.Content/.test(code);
94
+ const hasDialogTitle = /DialogTitle|Dialog\.Title|AlertDialogTitle|AlertDialog\.Title/.test(code);
95
+ if (hasDialogContent && !hasDialogTitle) {
96
+ issues.push({
97
+ rule: 'dialog-without-title',
98
+ severity: 'medium',
99
+ detail: `A Radix Dialog/AlertDialog${at} has content but no Title — screen readers announce nothing and Radix warns.`,
100
+ fix: 'Add a Title (wrap in a visually-hidden span if there is no visible heading) and a description / aria-describedby. See get_build_standards topic "radix".',
101
+ });
102
+ }
103
+ return issues;
104
+ }
105
+ // ── Path-aware checks (only available when `files` is provided) ───────────────
106
+ function fileChecks(path, content) {
107
+ const issues = [];
108
+ const lines = countLines(content);
109
+ const lower = path.toLowerCase();
110
+ if (lines > FILE_SIZE_LIMIT) {
111
+ issues.push({
112
+ rule: 'file-too-large',
113
+ severity: 'medium',
114
+ detail: `${path} is ${lines} lines (limit ${FILE_SIZE_LIMIT}).`,
115
+ fix: 'Split it: extract sub-components, a custom hook, or utilities. See get_build_standards topic "architecture" (file-size limits) and "refactoring".',
116
+ });
117
+ }
118
+ const code = decomment(content);
119
+ const isAtomOrMolecule = /[\\/](atoms|molecules)[\\/]/.test(lower);
120
+ const hasSideEffects = /\bfetch\s*\(|\buseQuery\s*\(|\buseSWR\s*\(|await\s+prisma|['"]use server['"]/.test(code);
121
+ if (isAtomOrMolecule && hasSideEffects) {
122
+ issues.push({
123
+ rule: 'atom-with-side-effects',
124
+ severity: 'high',
125
+ detail: `${path} is an atom/molecule but fetches data or runs a server action — it should be presentational (data via props).`,
126
+ fix: 'Move data fetching to a hook/container; keep this component dumb. See get_build_standards topic "architecture" (atomic design + presentational vs container).',
127
+ });
128
+ }
129
+ const isRouteFile = /[\\/]page\.tsx$/.test(lower);
130
+ const hasBusinessLogic = /\buseState\s*\(|\buseEffect\s*\(|\buseReducer\s*\(/.test(code);
131
+ if (isRouteFile && hasBusinessLogic && lines > 60) {
132
+ issues.push({
133
+ rule: 'fat-page',
134
+ severity: 'medium',
135
+ detail: `${path} (a route file) holds component state/logic and is ${lines} lines.`,
136
+ fix: 'Keep page.tsx thin (wire data + compose). Move state/logic into a hook and the UI into an organism. See get_build_standards topic "architecture".',
137
+ });
138
+ }
139
+ return issues;
140
+ }
141
+ export function reviewApp(input) {
142
+ const issues = [];
143
+ if (input.files && Object.keys(input.files).length > 0) {
144
+ const routePaths = new Set();
145
+ for (const [path, content] of Object.entries(input.files)) {
146
+ issues.push(...fileChecks(path, content));
147
+ issues.push(...snippetChecks(content, path));
148
+ const lower = path.toLowerCase();
149
+ const m = lower.match(/(.*[\\/])(page|layout)\.tsx$/);
150
+ if (m)
151
+ routePaths.add(m[1]);
152
+ }
153
+ // Route hygiene: a route folder with page.tsx should ship error.tsx + loading.tsx.
154
+ for (const dir of routePaths) {
155
+ const has = (name) => Object.keys(input.files).some(p => p.toLowerCase() === `${dir}${name}`);
156
+ const missing = ['error.tsx', 'loading.tsx'].filter(n => !has(n));
157
+ if (missing.length) {
158
+ issues.push({
159
+ rule: 'route-missing-boundaries',
160
+ severity: 'medium',
161
+ detail: `Route ${dir} is missing ${missing.join(' + ')}.`,
162
+ fix: 'Every route should ship error.tsx (recovery UI) and loading.tsx (skeleton). See get_build_standards topic "error-handling". (Note: only the files you passed were checked.)',
163
+ });
164
+ }
165
+ }
166
+ }
167
+ else if (input.code) {
168
+ issues.push(...snippetChecks(input.code, ''));
169
+ }
170
+ const screenNote = input.screenType
171
+ ? ` For screen-specific must-haves, also see get_screen_blueprint("${input.screenType}").`
172
+ : '';
173
+ return {
174
+ valid: issues.length === 0,
175
+ issueCount: issues.length,
176
+ issues,
177
+ summary: issues.length === 0
178
+ ? `No structural issues detected. NOTE: review_app is a heuristic structural audit, not a compiler or a substitute for review — it checks for common "messy app" smells (missing states, swallowed errors, layering, mobile nav, Radix a11y), not correctness or compilation.${screenNote}`
179
+ : `Found ${issues.length} structural issue${issues.length === 1 ? '' : 's'}. Apply each fix; pair with validate_code for visual/token issues. review_app is heuristic — confirm each finding in context.${screenNote}`,
180
+ };
181
+ }
182
+ //# sourceMappingURL=review.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"review.js","sourceRoot":"","sources":["../src/review.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AA05BA,wBAAsB,WAAW,kBA2BhC"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AA4kCA,wBAAsB,WAAW,kBA2BhC"}
package/dist/server.js CHANGED
@@ -9,7 +9,10 @@ import { PHOSPHOR_ICONS, PHOSPHOR_ICON_NAMES } from './data/icons.js';
9
9
  import { SETUP } from './data/setup.js';
10
10
  import { CROSSWALK, nearestToken } from './data/crosswalk.js';
11
11
  import { buildPolishGuide, ARCHETYPE_KEYS } from './data/polish.js';
12
- const VERSION = '1.4.0';
12
+ import { buildStandardsGuide, BUILD_STANDARD_TOPICS } from './data/build-standards.js';
13
+ import { buildScreenBlueprint, SCREEN_TYPES } from './data/screens.js';
14
+ import { reviewApp } from './review.js';
15
+ const VERSION = '1.7.1';
13
16
  /** Build "raw → ak" suggestions for a list of flagged raw classes (for validate_code). */
14
17
  function tokenSuggestions(classes) {
15
18
  const seen = new Set();
@@ -26,24 +29,29 @@ function tokenSuggestions(classes) {
26
29
  }
27
30
  // The golden path — surfaced as server `instructions` so even a naive agent
28
31
  // (one that just gets "build X") self-guides to design-system-quality output.
29
- const SERVER_INSTRUCTIONS = `AstralKit builds polished, accessible, on-brand UI with a design-token system.
32
+ const SERVER_INSTRUCTIONS = `AstralKit builds polished, accessible, on-brand UI. The AstralKit COMPONENT LIBRARY is the source of truth — build FROM real library components; do NOT freestyle UI. Hand-writing a component the library already provides is a failure, even if it is token-compliant.
30
33
 
31
- GOLDEN PATH — follow this for ANY UI request ("build a …", "make a …", a page/section/component):
34
+ GOLDEN PATH — for ANY UI request (build, redesign, or improve a page/section/component):
32
35
  1. get_coding_standards — the mandatory rules. Read first.
33
36
  2. get_design_tokens — use ONLY ak-* tokens. NEVER raw Tailwind colors (bg-gray-*), NEVER arbitrary values (p-[1rem]).
34
- 3. search_components — find candidate AstralKit components to use as a quality REFERENCE. The library ranges from minimal to bold showcase designs favor the striking ones that fit.
35
- 4. get_preview — SEE a candidate's rendered design (returns an image) and pick the best-looking fit before committing.
36
- 5. get_component (mode:"recipe") — study the chosen reference and ADAPT it into a new screen; do not paste verbatim. PRESERVE its distinctive design (watermarks, display type, complex layout). The recipe lists the exact tokens, icons, font, dependencies, and setup it uses.
37
- 6. get_iconsbefore using any Phosphor icon, confirm the name exists here. Never guess icon names.
38
- 7. get_setupthe exact CSS imports + font-load step so the screen actually renders (declaring a font is not loading it).
39
- 8. validate_code — run on your final code and fix every diff it returns.
40
- 9. BUILD IT — validate_code checks conventions, NOT that the code compiles. Always run a real build/typecheck (tsc / next build) before declaring done; it will not catch syntax errors (e.g. a stray */ inside a block comment, unbalanced braces).
37
+ 3. search_components — ALWAYS do this before writing any UI. Find the closest real component(s) for what you are building; never skip straight to JSX.
38
+ 4. get_preview — SEE candidates (returns an image) and pick the best-looking fit.
39
+ 5. get_component (mode:"recipe") — get the real component and REUSE it: keep its structure, layout, and polish. "Adapt" = RE-CONTENT, not rebuild swap its placeholder copy, nav items, logo, and sample data for the app's real content, and match the app's theme. Do NOT regenerate the layout into something generic, and do NOT freestyle a replacement.
40
+ 6. install_componentinstall the chosen component(s) into the project (e.g. components/ui) and import them. The library piece is what ships; your job is to wire it in + re-content it.
41
+ 7. get_iconsconfirm Phosphor names exist (never guess). get_setup the CSS imports + font-load step.
42
+ 8. validate_code + review_app fix every issue, then BUILD (run tsc / next build validate_code is NOT a compiler; it won't catch syntax errors).
41
43
 
42
- TOKENIZE EXISTING UI (the source is already well-designed; you only need ak-* tokens): SKIP steps 3–5. (a) get_coding_standards, (b) get_design_tokens — it includes a raw-Tailwind→ak CROSSWALK; map every raw class (bg-gray-*, text-sm, p-6, indigo gradients, etc.) to its ak-* token, (c) get_icons — swap any icon library / inline <svg> UI icons to Phosphor, (d) validate_code it returns the exact "raw → ak" suggestion per flagged class; apply each and re-run until clean, (e) BUILD IT — validate_code is not a compiler; run a real build/typecheck before declaring done. Also: raise sub-16px body to text-ak-base, add focus rings + 48px targets.
44
+ IF NOTHING FITS EXACTLY: still start from the CLOSEST component's recipe and modify it (adjust layout/content as needed) — NEVER from a blank file. The reference sets the quality bar; match it.
43
45
 
44
- POLISH / REVAMP EXISTING UI (the source also has weak hierarchy, cramped/uneven spacing, flat weights, or off-brand surfaces it needs design-quality elevation, not just tokens): call polish_ui FIRST. It returns a 6-phase revamp procedure (tokenize is Phase 1 inside it), a design-smell rubric, the allowed/forbidden structural changes, a self-check, and per-region reference targets. Pass the UI regions you see (e.g. ["kanban-card","sidebar","form"]). Polish keeps the original layout, content, and behavior it is a revamp, not a redesign.
46
+ REDESIGN / IMPROVE AN EXISTING APP: for each bespoke piece (sidebar, top bar, cards, forms, hero…) search the AstralKit equivalent, install it, and REPLACE the bespoke one then RE-CONTENT it with the app's real nav, logo, copy, and data. PRESERVE the app's existing theme: if the app is dark, keep it dark (set the matching data-ak-theme never flip dark↔light). Never invent colors. ("*-light" showcase components are LIGHT demos match them to the app's theme, don't drag the app to light.)
45
47
 
46
- Hard rules: 16px body floor (text-ak-base), 48px touch targets (min-h-ak-control-lg), Phosphor icons only, semantic color tokens, focus-visible states. Prefer the resources astralkit://tokens, ://rules, ://icons, ://setup if your client supports them (load once).`;
48
+ TOKENIZE (source already well-designed, only needs ak-* tokens): get_coding_standards get_design_tokens (raw-Tailwind→ak CROSSWALK) map every raw class → swap icons to Phosphor validate_code build. PRESERVE the source theme the crosswalk assumes a LIGHT source, so for a dark app map to dark surfaces / inverse roles, not the light defaults (mapping a dark app naively flips it to a broken light theme).
49
+
50
+ POLISH / REVAMP (weak hierarchy, cramped spacing, off-brand surfaces): call polish_ui FIRST. It keeps the original layout, content, and behavior.
51
+
52
+ BUILD IT RIGHT (architecture & screen UX): get_build_standards (atomic design, separation of concerns, the loading/empty/error/populated state contract, error handling, responsive/mobile-menu, radix; pass a topic). For a specific screen call get_screen_blueprint(screen_type) — it tells you WHICH AstralKit components to install for that screen + the must-haves. Audit existing/AI-generated code with review_app.
53
+
54
+ Hard rules: build from library components, never freestyle; 16px body floor (text-ak-base); 48px touch targets; Phosphor icons only; semantic color tokens; PRESERVE the app's theme. Prefer the resources astralkit://tokens, ://rules, ://icons, ://setup, ://standards if your client supports them.`;
47
55
  // ─── Security: Input validation ───────────────────────────────────────────────
48
56
  const MAX_QUERY_LENGTH = 200;
49
57
  const MAX_SLUG_LENGTH = 100;
@@ -59,6 +67,10 @@ function validateSlug(slug) {
59
67
  }
60
68
  // ─── Security: Periodic auth revalidation ─────────────────────────────────────
61
69
  const REVALIDATION_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes
70
+ // During a transient network outage, keep serving on the last good validation
71
+ // for up to this long rather than bricking a valid subscriber. A genuinely
72
+ // invalid/expired key is still caught immediately (that's an AuthError, not transient).
73
+ const AUTH_GRACE_MS = 24 * 60 * 60 * 1000; // 24 hours
62
74
  class AuthGuard {
63
75
  lastCheck;
64
76
  apiKey;
@@ -80,11 +92,17 @@ class AuthGuard {
80
92
  }
81
93
  catch (err) {
82
94
  if (err instanceof AuthError) {
95
+ // Genuine auth failure (invalid key / plan downgrade) — revoke immediately.
83
96
  this.revoked = true;
84
97
  console.error('[astralkit-mcp] Subscription revalidation failed — access revoked.');
98
+ throw err;
99
+ }
100
+ // Transient (network/timeout, ApiError). Do NOT revoke: keep serving on the
101
+ // last good validation within the grace window, and let the next call retry.
102
+ if (Date.now() - this.lastCheck < AUTH_GRACE_MS) {
103
+ console.error('[astralkit-mcp] Revalidation network error; serving on cached auth (will retry).');
104
+ return;
85
105
  }
86
- // Network errors (ApiError with TIMEOUT/NETWORK_ERROR) are transient —
87
- // don't permanently revoke; the next check will retry.
88
106
  throw err;
89
107
  }
90
108
  }
@@ -110,14 +128,61 @@ function imageResult(base64, mimeType) {
110
128
  }
111
129
  // ─── Recipe extraction (makes get_component build-ready / self-describing) ─────
112
130
  const VALID_ICONS = new Set(PHOSPHOR_ICON_NAMES);
113
- /** Strip block, line, and JSX comments to shrink the recipe payload. */
131
+ /**
132
+ * Strip block, line, and JSX comments to shrink the recipe payload — string-aware,
133
+ * so it never eats comment-like text INSIDE a string/template literal (e.g. a glob
134
+ * "src/**\/*.tsx" or a protocol-relative URL "//cdn.example.com"). A naive regex
135
+ * pass corrupts such source; this walks the code tracking literal context.
136
+ */
114
137
  function stripComments(code) {
115
- return code
116
- .replace(/\{\/\*[\s\S]*?\*\/\}/g, '') // {/* jsx */}
117
- .replace(/\/\*[\s\S]*?\*\//g, '') // /* block */
118
- .replace(/(^|[^:])\/\/.*$/gm, '$1') // // line (avoid http://)
119
- .replace(/\n{3,}/g, '\n\n')
120
- .trim();
138
+ let out = '';
139
+ const n = code.length;
140
+ for (let i = 0; i < n;) {
141
+ const c = code[i];
142
+ const c2 = code[i + 1];
143
+ // string / template literal — copy verbatim (incl. escapes), never scan for comments inside
144
+ if (c === '"' || c === "'" || c === '`') {
145
+ const q = c;
146
+ out += c;
147
+ i++;
148
+ while (i < n) {
149
+ if (code[i] === '\\') {
150
+ out += code[i] + (code[i + 1] ?? '');
151
+ i += 2;
152
+ continue;
153
+ }
154
+ out += code[i];
155
+ const done = code[i] === q;
156
+ i++;
157
+ if (done)
158
+ break;
159
+ }
160
+ continue;
161
+ }
162
+ // JSX comment {/* ... */}
163
+ if (c === '{' && c2 === '/' && code[i + 2] === '*') {
164
+ const end = code.indexOf('*/}', i + 3);
165
+ if (end !== -1) {
166
+ i = end + 3;
167
+ continue;
168
+ }
169
+ }
170
+ // block comment /* ... */
171
+ if (c === '/' && c2 === '*') {
172
+ const end = code.indexOf('*/', i + 2);
173
+ i = end === -1 ? n : end + 2;
174
+ continue;
175
+ }
176
+ // line comment // ...
177
+ if (c === '/' && c2 === '/') {
178
+ while (i < n && code[i] !== '\n')
179
+ i++;
180
+ continue;
181
+ }
182
+ out += c;
183
+ i++;
184
+ }
185
+ return out.replace(/[ \t]+$/gm, '').replace(/\n{3,}/g, '\n\n').trim();
121
186
  }
122
187
  /** Every ak-* token/utility referenced in the code (for tokensUsed). */
123
188
  function extractAkTokens(code) {
@@ -144,10 +209,10 @@ function createServer(auth, guard, apiKey) {
144
209
  server.registerTool('search_components', {
145
210
  title: 'Search Components',
146
211
  description: 'START HERE when building any UI with AstralKit. Search components by name, description, or category ' +
147
- 'to find a quality REFERENCE, then call get_component (recipe) to adapt it. ' +
212
+ 'to find the real component to INSTALL & REUSE (never freestyle one the library already has). ' +
148
213
  'Read the descriptions: the library ranges from minimal to BOLD showcase designs (watermarks, oversized display type, complex/asymmetric layouts) — prefer the most striking option that fits the brand, not just the most literal keyword match. ' +
149
- 'Each result includes a previewImage URL; call get_preview(slug) to SEE a candidate and pick the best-looking fit before adapting. ' +
150
- 'Returns matching components with metadata. (Golden path: get_coding_standards → get_design_tokens → search_components → get_preview → get_component → get_icons get_setup → validate_code.)',
214
+ 'Each result includes a previewImage URL; call get_preview(slug) to SEE a candidate and pick the best fit, then get_component (recipe) + install_component to reuse it. ' +
215
+ 'Returns matching components with metadata. (Golden path: get_coding_standards → get_design_tokens → search_components → get_preview → get_component → install_component (reuse + re-content) → validate_code.)',
151
216
  inputSchema: {
152
217
  query: z.string().max(MAX_QUERY_LENGTH).describe('Search query (component name, description, or keyword)'),
153
218
  category: z.string().max(MAX_SLUG_LENGTH).optional().describe('Filter by category slug'),
@@ -261,7 +326,7 @@ function createServer(auth, guard, apiKey) {
261
326
  });
262
327
  server.registerTool('get_component', {
263
328
  title: 'Get Component',
264
- description: 'Get a build-ready AstralKit component to ADAPT into a new screen (do not paste verbatim). ' +
329
+ description: 'Get a real, build-ready AstralKit component to INSTALL and REUSE keep its structure/layout/polish and re-content it (swap copy, nav, logo, data) to fit the app; do not freestyle a replacement. ' +
265
330
  'Call search_components/list_components first to find the slug. ' +
266
331
  'Default mode "recipe" returns a lean, self-describing payload: source (comments stripped) plus ' +
267
332
  'the exact ak-* tokens, Phosphor icons, intended font, setup, and dependencies it uses — so you never guess. ' +
@@ -301,7 +366,7 @@ function createServer(auth, guard, apiKey) {
301
366
  slug: result.slug,
302
367
  framework: fw,
303
368
  previewImage: result.previewImage,
304
- howToAdapt: 'Use this as a quality reference and PRESERVE its distinctive design watermarks, oversized/display type, asymmetric or complex layout, and effects ARE the premium character you want. Rewrite the COPY/content to fit the request, but do NOT flatten it into a generic layout. The library ranges from minimal to bold showcase designs; lean into the striking ones. Build a NEW screen at the same level of polish do not paste verbatim.',
369
+ howToAdapt: 'REUSE this component install it into the project and keep its structure, layout, spacing, and polish (watermarks, display type, complex layout, effects ARE the premium character). ADAPT = RE-CONTENT only: replace placeholder copy, nav items, logo, and sample data with the real app content, and match the app theme (preserve dark/light never flip it). Do NOT regenerate the layout into something generic, and do NOT freestyle a replacement. If it is not an exact fit, modify THIS recipe rather than building from a blank file.',
305
370
  intendedFont: 'Inter via font-ak-sans (load with next/font and wire to --font-ak-sans — see get_setup)',
306
371
  tokensUsed,
307
372
  iconsUsed: icons.used,
@@ -384,6 +449,51 @@ function createServer(auth, guard, apiKey) {
384
449
  }
385
450
  return textResult(CODING_STANDARDS);
386
451
  });
452
+ server.registerTool('get_build_standards', {
453
+ title: 'Get Build Standards',
454
+ description: 'Get AstralKit STRUCTURAL standards — how to build an app well, not just how to style it. ' +
455
+ 'Covers architecture (atomic design, separation of concerns, layers, file-size limits), the ' +
456
+ 'safe incremental refactoring process for cleaning up a messy app, the loading/empty/error/' +
457
+ 'populated state contract, error handling, responsive/mobile-menu patterns, and Radix correctness. ' +
458
+ 'This complements get_coding_standards (visual rules). Pass a topic to get one section; omit for the index. ' +
459
+ 'Use it before building app structure, or when asked to clean up / refactor vibe-coded code.',
460
+ inputSchema: {
461
+ topic: z.enum(BUILD_STANDARD_TOPICS).optional()
462
+ .describe(`Which section to return. One of: ${BUILD_STANDARD_TOPICS.join(', ')}. Omit to get the index of all topics.`),
463
+ },
464
+ }, async ({ topic }) => {
465
+ try {
466
+ await guard.check();
467
+ }
468
+ catch (err) {
469
+ if (err instanceof AuthError)
470
+ return errorResult(err.message);
471
+ throw err;
472
+ }
473
+ return textResult(buildStandardsGuide(topic));
474
+ });
475
+ server.registerTool('get_screen_blueprint', {
476
+ title: 'Get Screen Blueprint',
477
+ description: 'Get a UX blueprint for a specific screen type — structure, must-haves, anti-patterns, the ' +
478
+ 'four-state (loading/empty/error/populated) requirement, responsive/mobile notes, and exactly ' +
479
+ 'which AstralKit components to INSTALL for this screen (categories + ready search_components queries + polish_ui archetypes) — reuse and re-content them, do not freestyle. ' +
480
+ 'Call this FIRST when building a dashboard, navigation, pricing page, marketing/landing page, ' +
481
+ 'auth screen, onboarding flow, or settings page, so the screen is built right and on-library.',
482
+ inputSchema: {
483
+ screen_type: z.enum(SCREEN_TYPES)
484
+ .describe(`The kind of screen to build. One of: ${SCREEN_TYPES.join(', ')}.`),
485
+ },
486
+ }, async ({ screen_type }) => {
487
+ try {
488
+ await guard.check();
489
+ }
490
+ catch (err) {
491
+ if (err instanceof AuthError)
492
+ return errorResult(err.message);
493
+ throw err;
494
+ }
495
+ return textResult(buildScreenBlueprint(screen_type));
496
+ });
387
497
  server.registerTool('get_setup', {
388
498
  title: 'Get Setup',
389
499
  description: 'Get the exact project setup AstralKit needs to RENDER correctly: the CSS imports (theme + utilities), ' +
@@ -484,8 +594,8 @@ function createServer(auth, guard, apiKey) {
484
594
  server.registerTool('get_preview', {
485
595
  title: 'Get Preview Image',
486
596
  description: 'Get a rendered PREVIEW IMAGE of a component so you can SEE its design before building. ' +
487
- 'Use it to compare candidates from search_components and pick the most striking one that fits the brand — ' +
488
- 'then call get_component (recipe) to adapt the chosen one. Returns the image.',
597
+ 'Use it to compare candidates from search_components and pick the best fit — ' +
598
+ 'then call get_component (recipe) to install and reuse the chosen one. Returns the image.',
489
599
  inputSchema: {
490
600
  slug: z.string().max(MAX_SLUG_LENGTH).describe('Component slug to preview'),
491
601
  },
@@ -570,9 +680,12 @@ function createServer(auth, guard, apiKey) {
570
680
  fix: `Replace with the nearest AstralKit token — ${tokenSuggestions([...new Set(rawTailwindColors)])}. (Full crosswalk via get_design_tokens.)`,
571
681
  });
572
682
  }
573
- const arbitraryValues = code.match(/\b(p|px|py|pt|pb|pl|pr|m|mx|my|mt|mb|ml|mr|gap|space-[xy]|w|h|text|rounded|leading)-\[[^\]]+\]/g);
683
+ // Negative lookbehind for a word-char/hyphen so `h-[]` inside `min-h-[]`
684
+ // (and `w-[…]` inside `max-w-[…]`) is NOT falsely captured — those min-/max-
685
+ // prefixed values are legitimate (min-h touch targets, max-w width clamps).
686
+ const arbitraryValues = code.match(/(?<![\w-])(p|px|py|pt|pb|pl|pr|m|mx|my|mt|mb|ml|mr|gap|space-[xy]|w|h|text|rounded|leading)-\[[^\]]+\]/g);
574
687
  if (arbitraryValues) {
575
- const nonExempt = arbitraryValues.filter(v => !v.startsWith('min-h-[3rem]') && !v.startsWith('h-14'));
688
+ const nonExempt = arbitraryValues.filter(v => !v.startsWith('h-14'));
576
689
  if (nonExempt.length > 0) {
577
690
  issues.push({
578
691
  rule: 'arbitrary-value',
@@ -665,6 +778,36 @@ function createServer(auth, guard, apiKey) {
665
778
  fix: 'GSAP is not licensed for sold products/templates. Use Framer Motion (the MIT `motion` package) or CSS animations / the astralkit motion presets instead.',
666
779
  });
667
780
  }
781
+ // Cheap structural smells (the deep structural audit lives in review_app).
782
+ if (/catch\s*\([^)]*\)\s*\{\s*\}/.test(code)) {
783
+ issues.push({
784
+ rule: 'empty-catch',
785
+ detail: 'Empty catch block silently swallows errors.',
786
+ fix: 'Every catch must log AND update the UI (error state / recovery toast). See get_build_standards("error-handling").',
787
+ });
788
+ }
789
+ if (/(?:^|[^.\w])alert\s*\(/.test(code)) {
790
+ issues.push({
791
+ rule: 'alert-used',
792
+ detail: 'alert() used for user feedback.',
793
+ fix: 'Use a toast (react-hot-toast / Sonner / Radix Toast) — error toasts should not auto-dismiss. See get_build_standards("error-handling").',
794
+ });
795
+ }
796
+ if (/\{\s*[\w.]+\.length\s*(?:>\s*0\s*)?&&\s*[(<]/.test(code) &&
797
+ !/length\s*===\s*0|length\s*<\s*1|!\w+\??\.length|\bEmptyState\b|isEmpty/i.test(code)) {
798
+ issues.push({
799
+ rule: 'ghost-render',
800
+ detail: 'A list is rendered only when non-empty, with no empty branch (the "Ghost" — users see blank space).',
801
+ fix: 'Add an explicit empty branch before the list: if (!data?.length) return <EmptyState .../>. See get_build_standards("state-lifecycle").',
802
+ });
803
+ }
804
+ if (/return\s*\(?\s*<\w+[^>]*>\s*(No data|Nothing here|No results|Empty)\b/i.test(code)) {
805
+ issues.push({
806
+ rule: 'naked-empty-state',
807
+ detail: 'A bare "No data"-style empty state with no icon, explanation, or action.',
808
+ fix: 'Use a real empty state (icon + title + description + CTA) — reuse atoms/empty-state.tsx. See get_build_standards("empty-states").',
809
+ });
810
+ }
668
811
  return jsonResult({
669
812
  valid: issues.length === 0,
670
813
  issueCount: issues.length,
@@ -674,6 +817,36 @@ function createServer(auth, guard, apiKey) {
674
817
  : `Found ${issues.length} issue${issues.length === 1 ? '' : 's'}. Apply each fix and re-run validate_code until clean. (validate_code checks conventions, not compilation — also run a real build/typecheck.)`,
675
818
  });
676
819
  });
820
+ server.registerTool('review_app', {
821
+ title: 'Review App (structural audit)',
822
+ description: 'Audit code for STRUCTURAL "messy app" smells — the architectural counterpart to validate_code ' +
823
+ '(which checks visual/token issues). Flags a missing loading/empty/error state contract, the ' +
824
+ '"Ghost" and "Naked Empty" anti-patterns, swallowed errors (empty catch, alert(), raw errors ' +
825
+ 'shown to users), atomic-design / separation-of-concerns violations, navigation with no mobile ' +
826
+ 'menu, and Radix a11y gaps. Pass a single `code` snippet, or a `files` map (path -> contents) ' +
827
+ 'for cross-file checks (file-size limits, atoms that fetch, fat page.tsx, routes missing ' +
828
+ 'error.tsx/loading.tsx). Heuristic, not a compiler — pair it with validate_code.',
829
+ inputSchema: {
830
+ code: z.string().max(MAX_CODE_LENGTH).optional().describe('A single TSX/JSX file or snippet to audit.'),
831
+ files: z.record(z.string(), z.string().max(MAX_CODE_LENGTH)).optional()
832
+ .describe('A map of { "path/to/file.tsx": "contents" } for multi-file checks (file size, layering, route boundaries).'),
833
+ screen_type: z.enum(SCREEN_TYPES).optional()
834
+ .describe(`Optional screen type for screen-specific guidance. One of: ${SCREEN_TYPES.join(', ')}.`),
835
+ },
836
+ }, async ({ code, files, screen_type }) => {
837
+ try {
838
+ await guard.check();
839
+ }
840
+ catch (err) {
841
+ if (err instanceof AuthError)
842
+ return errorResult(err.message);
843
+ throw err;
844
+ }
845
+ if (!code && (!files || Object.keys(files).length === 0)) {
846
+ return errorResult('Provide either `code` (a snippet) or `files` (a path -> contents map) to review.');
847
+ }
848
+ return jsonResult(reviewApp({ code, files, screenType: screen_type }));
849
+ });
677
850
  // Boosters (Plus/Lifetime only)
678
851
  if (canAccessBoosters(auth.tier)) {
679
852
  server.registerTool('list_boosters', {
@@ -783,6 +956,12 @@ function createServer(auth, guard, apiKey) {
783
956
  contents: [{ uri: 'astralkit://polish', text: buildPolishGuide(), mimeType: 'text/markdown' }],
784
957
  };
785
958
  });
959
+ server.registerResource('AstralKit Build Standards', 'astralkit://standards', { description: 'Structural standards — atomic design, separation of concerns, the loading/empty/error/populated state contract, error handling, responsive/mobile-menu, Radix correctness, and the safe refactoring process (the architectural sibling of the visual coding rules)', mimeType: 'text/markdown' }, async () => {
960
+ await guard.check();
961
+ return {
962
+ contents: [{ uri: 'astralkit://standards', text: buildStandardsGuide(), mimeType: 'text/markdown' }],
963
+ };
964
+ });
786
965
  // ─── Prompts ─────────────────────────────────────────────────────────
787
966
  server.registerPrompt('build-with-astralkit', {
788
967
  title: 'Build with AstralKit',