@ng-solid/mcp 22.0.0

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 (47) hide show
  1. package/GUIDE.md +292 -0
  2. package/README.md +159 -0
  3. package/data/design-system.json +24204 -0
  4. package/data/icons.json +1 -0
  5. package/data/skills/ns-solid-components/SKILL.md +140 -0
  6. package/data/skills/ns-solid-forms/SKILL.md +183 -0
  7. package/data/skills/ns-solid-icons/SKILL.md +95 -0
  8. package/data/skills/ns-solid-install/SKILL.md +120 -0
  9. package/data/skills/ns-solid-style-contract/SKILL.md +84 -0
  10. package/data/skills/ns-solid-theming/SKILL.md +123 -0
  11. package/index.d.ts +3 -0
  12. package/index.js +38 -0
  13. package/lib/data.d.ts +26 -0
  14. package/lib/data.js +51 -0
  15. package/lib/install-skills.d.ts +1 -0
  16. package/lib/install-skills.js +159 -0
  17. package/lib/preset-validator.d.ts +44 -0
  18. package/lib/preset-validator.js +172 -0
  19. package/lib/prompts.d.ts +2 -0
  20. package/lib/prompts.js +173 -0
  21. package/lib/resources.d.ts +2 -0
  22. package/lib/resources.js +328 -0
  23. package/lib/server.d.ts +3 -0
  24. package/lib/server.js +31 -0
  25. package/lib/style-contract.d.ts +37 -0
  26. package/lib/style-contract.js +167 -0
  27. package/lib/template-parser.d.ts +29 -0
  28. package/lib/template-parser.js +230 -0
  29. package/lib/tools.components.d.ts +44 -0
  30. package/lib/tools.components.js +836 -0
  31. package/lib/tools.d.ts +9 -0
  32. package/lib/tools.guides.d.ts +15 -0
  33. package/lib/tools.guides.js +103 -0
  34. package/lib/tools.icons.d.ts +7 -0
  35. package/lib/tools.icons.js +106 -0
  36. package/lib/tools.js +18 -0
  37. package/lib/tools.tokens.d.ts +7 -0
  38. package/lib/tools.tokens.js +278 -0
  39. package/lib/types.d.ts +349 -0
  40. package/lib/types.js +11 -0
  41. package/package.json +52 -0
  42. package/skills/ns-solid-components/SKILL.md +77 -0
  43. package/skills/ns-solid-forms/SKILL.md +166 -0
  44. package/skills/ns-solid-icons/SKILL.md +95 -0
  45. package/skills/ns-solid-install/SKILL.md +119 -0
  46. package/skills/ns-solid-style-contract/SKILL.md +84 -0
  47. package/skills/ns-solid-theming/SKILL.md +123 -0
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Pure port of `tools/scripts/check-design-system-styles.mjs`.
3
+ *
4
+ * The original script walks the filesystem and calls `process.exit`. This module extracts
5
+ * the same rules into a pure function an MCP tool can run against a snippet an agent pastes,
6
+ * without touching disk. The three canonical regexes (BEM class shape, banned Tailwind
7
+ * utilities, raw CSS values) are NOT re-declared here -- they are read from
8
+ * `snapshot.styleContract` (`design-system.json`) so this module can never drift from the
9
+ * regexes the real `pnpm quality:styles` gate enforces.
10
+ */
11
+ /** Parses a stringified `RegExp` literal (e.g. `"/^ns-.../"` or `"/.../i"`) back into a RegExp. */
12
+ export function parseRegexLiteral(literal) {
13
+ const match = /^\/(.*)\/([a-z]*)$/.exec(literal);
14
+ if (!match) {
15
+ throw new Error(`Not a serialized RegExp literal: ${literal}`);
16
+ }
17
+ return new RegExp(match[1], match[2]);
18
+ }
19
+ function lineAt(code, index) {
20
+ let line = 1;
21
+ for (let i = 0; i < index && i < code.length; i++) {
22
+ if (code[i] === '\n')
23
+ line++;
24
+ }
25
+ return line;
26
+ }
27
+ function ruleFrom(contract, id) {
28
+ const rule = contract.rules.find((candidate) => candidate.id === id);
29
+ if (!rule) {
30
+ throw new Error(`style contract snapshot is missing rule "${id}"`);
31
+ }
32
+ return rule;
33
+ }
34
+ const CLASS_ATTRIBUTE = /(?:^|\s)class="([^"]+)"/g;
35
+ const CLASS_BINDING = /\[class\.([^\]]+)\]/g;
36
+ const NG_CLASS_ATTRIBUTE = /\[ngClass\]="([\s\S]*?)"/g;
37
+ const OBJECT_CLASS_KEY = /(?:'([^']+)'|([a-zA-Z][\w-]*))\s*:/g;
38
+ const INLINE_STYLES = /\bstyles\s*:/;
39
+ const SCSS_USE_IMPORT = /@use\b|@import\b/;
40
+ function validateTemplateClasses(code, contract, violations) {
41
+ const bemPattern = parseRegexLiteral(contract.bemClassPattern);
42
+ const utilityPattern = parseRegexLiteral(contract.utilityClassPattern);
43
+ const bemRule = ruleFrom(contract, 'bem-naming');
44
+ const utilityRule = ruleFrom(contract, 'banned-utility');
45
+ const checkClass = (className, index) => {
46
+ const trimmed = className.trim();
47
+ if (!trimmed)
48
+ return;
49
+ if (!bemPattern.test(trimmed)) {
50
+ violations.push({
51
+ ruleId: bemRule.id,
52
+ severity: bemRule.severity,
53
+ message: `${bemRule.message} -- got "${trimmed}".`,
54
+ snippet: trimmed,
55
+ line: lineAt(code, index)
56
+ });
57
+ }
58
+ if (utilityPattern.test(trimmed)) {
59
+ violations.push({
60
+ ruleId: utilityRule.id,
61
+ severity: utilityRule.severity,
62
+ message: `${utilityRule.message} -- got "${trimmed}".`,
63
+ snippet: trimmed,
64
+ line: lineAt(code, index)
65
+ });
66
+ }
67
+ };
68
+ for (const match of code.matchAll(CLASS_ATTRIBUTE)) {
69
+ const listStart = match.index + match[0].indexOf(match[1]);
70
+ let offset = 0;
71
+ for (const className of match[1].split(/\s+/)) {
72
+ const at = match[1].indexOf(className, offset);
73
+ offset = at + className.length;
74
+ checkClass(className, listStart + at);
75
+ }
76
+ }
77
+ for (const match of code.matchAll(CLASS_BINDING)) {
78
+ checkClass(match[1], match.index);
79
+ }
80
+ for (const match of code.matchAll(NG_CLASS_ATTRIBUTE)) {
81
+ const bodyStart = match.index + match[0].indexOf(match[1]);
82
+ for (const key of match[1].matchAll(OBJECT_CLASS_KEY)) {
83
+ const name = key[1] ?? key[2];
84
+ checkClass(name, bodyStart + (key.index ?? 0));
85
+ }
86
+ }
87
+ }
88
+ function validateScss(code, contract, violations) {
89
+ if (!code.includes('@reference')) {
90
+ const rule = ruleFrom(contract, 'scss-reference-required');
91
+ violations.push({
92
+ ruleId: rule.id,
93
+ severity: rule.severity,
94
+ message: rule.message,
95
+ snippet: code.slice(0, 60).trim()
96
+ });
97
+ }
98
+ const useImportMatch = SCSS_USE_IMPORT.exec(code);
99
+ if (useImportMatch) {
100
+ const rule = ruleFrom(contract, 'no-use-import');
101
+ violations.push({
102
+ ruleId: rule.id,
103
+ severity: rule.severity,
104
+ message: rule.message,
105
+ snippet: useImportMatch[0],
106
+ line: lineAt(code, useImportMatch.index)
107
+ });
108
+ }
109
+ const rawValuePattern = parseRegexLiteral(contract.rawStyleValuePattern);
110
+ const globalRawValuePattern = new RegExp(rawValuePattern.source, rawValuePattern.flags.includes('g')
111
+ ? rawValuePattern.flags
112
+ : `${rawValuePattern.flags}g`);
113
+ const rawRule = ruleFrom(contract, 'no-raw-css-values');
114
+ for (const match of code.matchAll(globalRawValuePattern)) {
115
+ violations.push({
116
+ ruleId: rawRule.id,
117
+ severity: rawRule.severity,
118
+ message: `${rawRule.message} -- got "${match[0]}".`,
119
+ snippet: match[0],
120
+ line: lineAt(code, match.index)
121
+ });
122
+ }
123
+ }
124
+ function validateComponentTs(code, contract, violations) {
125
+ const match = INLINE_STYLES.exec(code);
126
+ if (match) {
127
+ const rule = ruleFrom(contract, 'no-inline-styles');
128
+ violations.push({
129
+ ruleId: rule.id,
130
+ severity: rule.severity,
131
+ message: rule.message,
132
+ snippet: code.slice(match.index, match.index + 40).trim(),
133
+ line: lineAt(code, match.index)
134
+ });
135
+ }
136
+ // Inline templates inside a `template: \`...\`` literal are still markup -- apply the same
137
+ // class rules a `.html` file would get.
138
+ validateTemplateClasses(code, contract, violations);
139
+ }
140
+ /**
141
+ * Validates a code snippet against the ng-solid style-architecture contract.
142
+ *
143
+ * @param code the raw source text to validate (a template, a component SCSS file, or a
144
+ * component `.ts` source)
145
+ * @param kind which contract rules apply -- `'template'` runs the BEM/Tailwind-utility checks,
146
+ * `'scss'` runs the `@reference`/`@use`/raw-value checks, `'component-ts'` runs the
147
+ * inline-`styles:` check plus the template checks (component `.ts` files can carry an inline
148
+ * template)
149
+ * @param contract `snapshot.styleContract` -- the canonical rule/pattern set, never duplicated here
150
+ */
151
+ export function validateStyleContract(code, kind, contract) {
152
+ const violations = [];
153
+ if (kind === 'template') {
154
+ validateTemplateClasses(code, contract, violations);
155
+ }
156
+ else if (kind === 'scss') {
157
+ validateScss(code, contract, violations);
158
+ }
159
+ else {
160
+ validateComponentTs(code, contract, violations);
161
+ }
162
+ return {
163
+ valid: violations.every((violation) => violation.severity !== 'error'),
164
+ violations
165
+ };
166
+ }
167
+ //# sourceMappingURL=style-contract.js.map
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Near-literal port of BHD's `tools.template-parser.ts` -- a small, well-tested,
3
+ * hostile-input-bounded scanner over a template string that picks out the narrow set of
4
+ * binding forms `validate_component_usage` needs to check.
5
+ *
6
+ * Two changes from the BHD original:
7
+ * 1. The `ns` prefix replaces `bhd` wherever the caller filters out selector-prefixed noise
8
+ * (that filtering lives in `tools.components.ts`, not here -- this module has no
9
+ * hardcoded prefix of its own).
10
+ * 2. Banana-in-a-box `[(x)]="y"` bindings are recognized as a third candidate kind, `model`,
11
+ * distinct from a plain property binding `[x]="y"`. BHD had no `model()` inputs; ng-solid
12
+ * has 15+ (`ns-input`, `ns-drawer`, `ns-modal`, ...).
13
+ */
14
+ export interface TemplateCandidates {
15
+ /** Plain attribute-form and property-binding-form names, e.g. `variant`, `disabled`. */
16
+ attributes: string[];
17
+ /** Event-binding names, e.g. `dismissed` from `(dismissed)="...". */
18
+ events: string[];
19
+ /** Banana-in-a-box model-binding names, e.g. `value` from `[(value)]="...". */
20
+ models: string[];
21
+ transitions: number;
22
+ }
23
+ /**
24
+ * Extracts the narrow binding forms supported by `validate_component_usage`.
25
+ *
26
+ * This intentionally treats quotes as ordinary characters and consumes a malformed
27
+ * candidate fragment through its next `=` so parsing cannot restart inside it.
28
+ */
29
+ export declare function parseTemplateCandidates(template: string): TemplateCandidates;
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Near-literal port of BHD's `tools.template-parser.ts` -- a small, well-tested,
3
+ * hostile-input-bounded scanner over a template string that picks out the narrow set of
4
+ * binding forms `validate_component_usage` needs to check.
5
+ *
6
+ * Two changes from the BHD original:
7
+ * 1. The `ns` prefix replaces `bhd` wherever the caller filters out selector-prefixed noise
8
+ * (that filtering lives in `tools.components.ts`, not here -- this module has no
9
+ * hardcoded prefix of its own).
10
+ * 2. Banana-in-a-box `[(x)]="y"` bindings are recognized as a third candidate kind, `model`,
11
+ * distinct from a plain property binding `[x]="y"`. BHD had no `model()` inputs; ng-solid
12
+ * has 15+ (`ns-input`, `ns-drawer`, `ns-modal`, ...).
13
+ */
14
+ function isAsciiLetter(value) {
15
+ const code = value.codePointAt(0) ?? -1;
16
+ return (code >= 65 && code <= 90) || (code >= 97 && code <= 122);
17
+ }
18
+ function isAsciiDigit(value) {
19
+ const code = value.codePointAt(0) ?? -1;
20
+ return code >= 48 && code <= 57;
21
+ }
22
+ function isWhitespace(value) {
23
+ return (value === ' ' ||
24
+ value === '\n' ||
25
+ value === '\r' ||
26
+ value === '\t' ||
27
+ value === '\f');
28
+ }
29
+ function isIdentifierContinuation(value) {
30
+ return (isAsciiLetter(value) ||
31
+ isAsciiDigit(value) ||
32
+ value === '_' ||
33
+ value === '-');
34
+ }
35
+ function candidateKind(start, next) {
36
+ if (start === '[' && next === '(')
37
+ return 'model';
38
+ if (start === '[')
39
+ return 'property';
40
+ if (start === '(')
41
+ return 'event';
42
+ if (isAsciiLetter(start))
43
+ return 'attribute';
44
+ return 'malformed';
45
+ }
46
+ function consume(template, state) {
47
+ const character = template[state.index];
48
+ state.index += 1;
49
+ state.transitions += 1;
50
+ return character;
51
+ }
52
+ function consumeMalformedFragment(template, state) {
53
+ while (state.index < template.length &&
54
+ !isWhitespace(template[state.index])) {
55
+ if (consume(template, state) === '=')
56
+ return;
57
+ }
58
+ }
59
+ function parseAttributeCandidate(template, state, fragmentStart) {
60
+ let invalid = false;
61
+ while (state.index < template.length &&
62
+ !isWhitespace(template[state.index])) {
63
+ const characterIndex = state.index;
64
+ const character = consume(template, state);
65
+ if (character === '=') {
66
+ return invalid
67
+ ? undefined
68
+ : template.slice(fragmentStart, characterIndex);
69
+ }
70
+ if (characterIndex > fragmentStart && !isIdentifierContinuation(character))
71
+ invalid = true;
72
+ }
73
+ return undefined;
74
+ }
75
+ function closingDelimiter(kind) {
76
+ return kind === 'property' ? ']' : ')';
77
+ }
78
+ function bindingNameStart(character, characterIndex) {
79
+ return isAsciiLetter(character) ? characterIndex : -1;
80
+ }
81
+ function updateBindingCandidate(binding, character, characterIndex, delimiter) {
82
+ if (character === delimiter && binding.nameStart !== -1 && !binding.closed) {
83
+ binding.nameEnd = characterIndex;
84
+ binding.closed = true;
85
+ return;
86
+ }
87
+ if (binding.closed || !isIdentifierContinuation(character))
88
+ binding.invalid = true;
89
+ }
90
+ function completeBindingCandidate(template, binding) {
91
+ if (binding.invalid || !binding.closed)
92
+ return undefined;
93
+ return template.slice(binding.nameStart, binding.nameEnd);
94
+ }
95
+ function parseBindingCandidate(template, state, fragmentStart, kind) {
96
+ const binding = {
97
+ invalid: false,
98
+ nameStart: -1,
99
+ nameEnd: -1,
100
+ closed: false
101
+ };
102
+ const delimiter = closingDelimiter(kind);
103
+ while (state.index < template.length &&
104
+ !isWhitespace(template[state.index])) {
105
+ const characterIndex = state.index;
106
+ const character = consume(template, state);
107
+ if (character === '=') {
108
+ return completeBindingCandidate(template, binding);
109
+ }
110
+ const offset = characterIndex - fragmentStart;
111
+ if (offset === 0)
112
+ continue;
113
+ if (offset === 1) {
114
+ binding.nameStart = bindingNameStart(character, characterIndex);
115
+ if (binding.nameStart === -1)
116
+ binding.invalid = true;
117
+ continue;
118
+ }
119
+ updateBindingCandidate(binding, character, characterIndex, delimiter);
120
+ }
121
+ return undefined;
122
+ }
123
+ /**
124
+ * Parses `[(name)]=` starting at `fragmentStart` (which points at the `[`; the caller has
125
+ * already confirmed the next character is `(` via `candidateKind`).
126
+ */
127
+ function parseModelCandidate(template, state, fragmentStart) {
128
+ const model = {
129
+ invalid: false,
130
+ nameStart: -1,
131
+ nameEnd: -1,
132
+ stage: 'name'
133
+ };
134
+ while (state.index < template.length &&
135
+ !isWhitespace(template[state.index])) {
136
+ const characterIndex = state.index;
137
+ const character = consume(template, state);
138
+ const offset = characterIndex - fragmentStart;
139
+ if (character === '=') {
140
+ return model.invalid || model.stage !== 'closed'
141
+ ? undefined
142
+ : template.slice(model.nameStart, model.nameEnd);
143
+ }
144
+ if (offset <= 1)
145
+ continue; // consumes '[' then '(', already validated by the caller
146
+ if (model.stage === 'name') {
147
+ if (model.nameStart === -1) {
148
+ model.nameStart = bindingNameStart(character, characterIndex);
149
+ if (model.nameStart === -1)
150
+ model.invalid = true;
151
+ continue;
152
+ }
153
+ if (character === ')') {
154
+ model.nameEnd = characterIndex;
155
+ model.stage = 'awaiting-bracket';
156
+ continue;
157
+ }
158
+ if (!isIdentifierContinuation(character))
159
+ model.invalid = true;
160
+ continue;
161
+ }
162
+ if (model.stage === 'awaiting-bracket') {
163
+ if (character === ']') {
164
+ model.stage = 'closed';
165
+ }
166
+ else {
167
+ model.invalid = true;
168
+ }
169
+ continue;
170
+ }
171
+ // stage === 'closed': anything other than '=' before whitespace/end is invalid
172
+ model.invalid = true;
173
+ }
174
+ return undefined;
175
+ }
176
+ /**
177
+ * Extracts the narrow binding forms supported by `validate_component_usage`.
178
+ *
179
+ * This intentionally treats quotes as ordinary characters and consumes a malformed
180
+ * candidate fragment through its next `=` so parsing cannot restart inside it.
181
+ */
182
+ export function parseTemplateCandidates(template) {
183
+ const attributes = [];
184
+ const events = [];
185
+ const models = [];
186
+ const state = { index: 0, transitions: 0 };
187
+ while (state.index < template.length) {
188
+ const start = template[state.index];
189
+ if (isWhitespace(start)) {
190
+ consume(template, state);
191
+ continue;
192
+ }
193
+ // A quote or other punctuation is not a candidate start. Advancing one character
194
+ // retains the prior scan's behavior of finding simple attributes within quotes.
195
+ if (!isAsciiLetter(start) &&
196
+ !isAsciiDigit(start) &&
197
+ start !== '[' &&
198
+ start !== '(') {
199
+ consume(template, state);
200
+ continue;
201
+ }
202
+ const fragmentStart = state.index;
203
+ const kind = candidateKind(start, template[state.index + 1]);
204
+ if (kind === 'malformed') {
205
+ consumeMalformedFragment(template, state);
206
+ continue;
207
+ }
208
+ let name;
209
+ if (kind === 'attribute') {
210
+ name = parseAttributeCandidate(template, state, fragmentStart);
211
+ }
212
+ else if (kind === 'model') {
213
+ name = parseModelCandidate(template, state, fragmentStart);
214
+ }
215
+ else {
216
+ name = parseBindingCandidate(template, state, fragmentStart, kind);
217
+ }
218
+ if (name === undefined)
219
+ continue;
220
+ if (kind === 'model') {
221
+ models.push(name);
222
+ continue;
223
+ }
224
+ attributes.push(name);
225
+ if (kind === 'event')
226
+ events.push(name);
227
+ }
228
+ return { attributes, events, models, transitions: state.transitions };
229
+ }
230
+ //# sourceMappingURL=template-parser.js.map
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Discovery, API, form-control, service/provider and style-validation tools -- the bulk of
3
+ * the ng-solid MCP surface. Split out of `tools.ts` to keep that file a thin aggregator.
4
+ */
5
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
+ import type { ComponentDoc, NsSnapshot } from './types.js';
7
+ export declare function jsonError(payload: Record<string, unknown>): {
8
+ content: {
9
+ type: "text";
10
+ text: string;
11
+ }[];
12
+ isError: boolean;
13
+ };
14
+ /** Every response embedding markup MUST carry this trio -- `<ns-x>` does not work without it. */
15
+ export interface ComponentTrio {
16
+ selector: string;
17
+ importStatement: string;
18
+ imports: string[];
19
+ }
20
+ export declare function importTrio(component: ComponentDoc): ComponentTrio;
21
+ /** Plain Levenshtein edit distance, used to rank "did you mean" suggestions. */
22
+ export declare function levenshtein(a: string, b: string): number;
23
+ export declare function suggestComponents(snapshot: NsSnapshot, query: string, limit?: number): string[];
24
+ export interface ComponentResolution {
25
+ component: ComponentDoc | null;
26
+ error?: string;
27
+ suggestions?: string[];
28
+ }
29
+ /**
30
+ * Resolves a user-supplied name to a `ComponentDoc`. Accepts a selector (`ns-button`), a class
31
+ * name (`NsButtonComponent`), a slug (`button`) or an attribute-directive selector
32
+ * (`[nsTooltip]`), case-insensitively. Never returns a bare "not found" -- always attaches
33
+ * distance-ranked suggestions.
34
+ */
35
+ export declare function resolveComponent(snapshot: NsSnapshot, input: string): ComponentResolution;
36
+ /** For an internal (non-exported) subcomponent, finds the public component that renders it. */
37
+ export declare function findPublicSiblingFor(snapshot: NsSnapshot, doc: ComponentDoc): ComponentDoc | null;
38
+ /** Builds a minimal-but-valid markup snippet for a component, honoring required inputs/models. */
39
+ export declare function buildTemplateSnippet(doc: ComponentDoc, options?: {
40
+ includeOptional?: boolean;
41
+ }): string;
42
+ /** Renders a full Markdown doc for a component -- shared by `export_component_docs` and the `ns://component/{selector}/markdown` resource. */
43
+ export declare function renderComponentMarkdown(snapshot: NsSnapshot, doc: ComponentDoc): string;
44
+ export declare function registerComponentTools(server: McpServer): void;