@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.
- package/GUIDE.md +292 -0
- package/README.md +159 -0
- package/data/design-system.json +24204 -0
- package/data/icons.json +1 -0
- package/data/skills/ns-solid-components/SKILL.md +140 -0
- package/data/skills/ns-solid-forms/SKILL.md +183 -0
- package/data/skills/ns-solid-icons/SKILL.md +95 -0
- package/data/skills/ns-solid-install/SKILL.md +120 -0
- package/data/skills/ns-solid-style-contract/SKILL.md +84 -0
- package/data/skills/ns-solid-theming/SKILL.md +123 -0
- package/index.d.ts +3 -0
- package/index.js +38 -0
- package/lib/data.d.ts +26 -0
- package/lib/data.js +51 -0
- package/lib/install-skills.d.ts +1 -0
- package/lib/install-skills.js +159 -0
- package/lib/preset-validator.d.ts +44 -0
- package/lib/preset-validator.js +172 -0
- package/lib/prompts.d.ts +2 -0
- package/lib/prompts.js +173 -0
- package/lib/resources.d.ts +2 -0
- package/lib/resources.js +328 -0
- package/lib/server.d.ts +3 -0
- package/lib/server.js +31 -0
- package/lib/style-contract.d.ts +37 -0
- package/lib/style-contract.js +167 -0
- package/lib/template-parser.d.ts +29 -0
- package/lib/template-parser.js +230 -0
- package/lib/tools.components.d.ts +44 -0
- package/lib/tools.components.js +836 -0
- package/lib/tools.d.ts +9 -0
- package/lib/tools.guides.d.ts +15 -0
- package/lib/tools.guides.js +103 -0
- package/lib/tools.icons.d.ts +7 -0
- package/lib/tools.icons.js +106 -0
- package/lib/tools.js +18 -0
- package/lib/tools.tokens.d.ts +7 -0
- package/lib/tools.tokens.js +278 -0
- package/lib/types.d.ts +349 -0
- package/lib/types.js +11 -0
- package/package.json +52 -0
- package/skills/ns-solid-components/SKILL.md +77 -0
- package/skills/ns-solid-forms/SKILL.md +166 -0
- package/skills/ns-solid-icons/SKILL.md +95 -0
- package/skills/ns-solid-install/SKILL.md +119 -0
- package/skills/ns-solid-style-contract/SKILL.md +84 -0
- package/skills/ns-solid-theming/SKILL.md +123 -0
package/lib/prompts.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const INVARIANTS = `Non-negotiable ng-solid invariants:
|
|
3
|
+
1. Single entrypoint: every import comes from "@ng-solid/design-system" (or "@ng-solid/icons" for provideIcons/icon symbols). There are no secondary entrypoints.
|
|
4
|
+
2. Standalone components MUST declare every ng-solid class they use in their own \`imports: [...]\` array -- a component like <ns-button> silently fails to render otherwise. Call get_component_import (or get_component) for the exact class name before writing markup.
|
|
5
|
+
3. Templates use ONLY ns-* BEM classes (ns-block__element--modifier). Never use a Tailwind utility class (flex, gap-2, text-sm, ...) directly in markup -- Tailwind is composed in component SCSS via @apply, never in templates.
|
|
6
|
+
4. Never invent an input value. Closed unions (variant, size, placement, ...) only accept the values returned by get_component_api -- e.g. ns-button's \`variant\` accepts exactly primary/secondary/tertiary/success/danger, nothing else.
|
|
7
|
+
5. Component SCSS reads only \`--ns-component-*\` custom properties, starts with \`@reference '../theme/tailwind.css'\`, and never uses raw hex/rgb()/px/rem/ms literals or \`@use\`/\`@import\`.
|
|
8
|
+
6. \`provideNsTheme(...)\` is called exactly once, in the app's root providers (\`appConfig.providers\`) -- never per-component or per-route.
|
|
9
|
+
7. Before finishing, run validate_component_usage on the markup you wrote and validate_style_contract on any new template/SCSS -- both are cheap and catch exactly the mistakes above.`;
|
|
10
|
+
export function registerPrompts(server) {
|
|
11
|
+
server.registerPrompt('build_ns_ui', {
|
|
12
|
+
title: 'Build a UI with ng-solid',
|
|
13
|
+
description: 'Scaffolds a plan for building a piece of UI using ng-solid components, front-loaded with the invariants that most often get skipped.',
|
|
14
|
+
argsSchema: {
|
|
15
|
+
request: z
|
|
16
|
+
.string()
|
|
17
|
+
.describe('What to build, e.g. "a settings page with a theme switch and a save button".')
|
|
18
|
+
}
|
|
19
|
+
}, ({ request }) => ({
|
|
20
|
+
messages: [
|
|
21
|
+
{
|
|
22
|
+
role: 'user',
|
|
23
|
+
content: {
|
|
24
|
+
type: 'text',
|
|
25
|
+
text: `Build the following UI using the ng-solid (Arvisfy) design system: ${request}
|
|
26
|
+
|
|
27
|
+
${INVARIANTS}
|
|
28
|
+
|
|
29
|
+
Workflow:
|
|
30
|
+
1. Call suggest_component or search_components for each UI need to find the right ng-solid component(s) -- do not guess a selector.
|
|
31
|
+
2. For each component you plan to use, call get_component_api to get its real inputs/outputs/models/values, and get_component_import for its exact import.
|
|
32
|
+
3. Write the standalone component: declare every used ng-solid class in \`imports: [...]\`, bind only documented inputs with only documented values.
|
|
33
|
+
4. Validate the markup with validate_component_usage before presenting it as done.`
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}));
|
|
38
|
+
server.registerPrompt('review_ns_usage', {
|
|
39
|
+
title: 'Review ng-solid usage',
|
|
40
|
+
description: 'Reviews a piece of Angular code that uses ng-solid components for API misuse and style-contract violations.',
|
|
41
|
+
argsSchema: {
|
|
42
|
+
code: z
|
|
43
|
+
.string()
|
|
44
|
+
.describe('The component template and/or TypeScript source to review.')
|
|
45
|
+
}
|
|
46
|
+
}, ({ code }) => ({
|
|
47
|
+
messages: [
|
|
48
|
+
{
|
|
49
|
+
role: 'user',
|
|
50
|
+
content: {
|
|
51
|
+
type: 'text',
|
|
52
|
+
text: `Review this ng-solid usage for correctness:
|
|
53
|
+
|
|
54
|
+
\`\`\`
|
|
55
|
+
${code}
|
|
56
|
+
\`\`\`
|
|
57
|
+
|
|
58
|
+
${INVARIANTS}
|
|
59
|
+
|
|
60
|
+
For every ng-solid selector you find in the markup:
|
|
61
|
+
1. Call get_component to confirm the selector is real and public (not an internal subcomponent).
|
|
62
|
+
2. Call validate_component_usage with the surrounding template (and the file's \`imports: [...]\` array, if visible) to catch unknown inputs/outputs/models, invalid enum values, invented BEM classes, and a missing import.
|
|
63
|
+
3. Call validate_style_contract on any inline template/SCSS for Tailwind-in-markup or raw-value violations.
|
|
64
|
+
Report every finding with the exact fix, not just the failure.`
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
}));
|
|
69
|
+
server.registerPrompt('choose_ns_component', {
|
|
70
|
+
title: 'Choose an ng-solid component',
|
|
71
|
+
description: 'Helps pick the right ng-solid component(s) for a UI need, disambiguating close options.',
|
|
72
|
+
argsSchema: {
|
|
73
|
+
need: z
|
|
74
|
+
.string()
|
|
75
|
+
.describe('The UI need, e.g. "let the user pick a date range" or "show a non-blocking success message".')
|
|
76
|
+
}
|
|
77
|
+
}, ({ need }) => ({
|
|
78
|
+
messages: [
|
|
79
|
+
{
|
|
80
|
+
role: 'user',
|
|
81
|
+
content: {
|
|
82
|
+
type: 'text',
|
|
83
|
+
text: `Which ng-solid component(s) best fit this need: "${need}"?
|
|
84
|
+
|
|
85
|
+
Call suggest_component with this need, then get_component_api on the top 1-2 candidates to compare their real inputs/outputs before recommending one. If the need involves showing something transiently (a toast, a confirmation), check list_services / get_service first -- several ng-solid surfaces (toast, modal, drawer) are opened imperatively via a service, not placed in markup directly. Explain why the chosen component fits, including its import trio.`
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
}));
|
|
90
|
+
server.registerPrompt('build_ns_form', {
|
|
91
|
+
title: 'Build a reactive form with ng-solid',
|
|
92
|
+
description: 'Scaffolds a reactive form using ng-solid form controls, respecting the shared ControlValueAccessor contract.',
|
|
93
|
+
argsSchema: {
|
|
94
|
+
fields: z
|
|
95
|
+
.string()
|
|
96
|
+
.describe('The fields the form needs, e.g. "email, password, accept-terms switch".')
|
|
97
|
+
}
|
|
98
|
+
}, ({ fields }) => ({
|
|
99
|
+
messages: [
|
|
100
|
+
{
|
|
101
|
+
role: 'user',
|
|
102
|
+
content: {
|
|
103
|
+
type: 'text',
|
|
104
|
+
text: `Build a reactive form with these fields: ${fields}
|
|
105
|
+
|
|
106
|
+
${INVARIANTS}
|
|
107
|
+
|
|
108
|
+
Form-specific rules:
|
|
109
|
+
- Call list_form_controls to see every ng-solid control that implements ControlValueAccessor, then get_form_control_contract for each one you use -- most of their API (label, disabled, ariaLabel, ...) is INHERITED from a shared base class and won't show up if you only look at the component's own source.
|
|
110
|
+
- Bind with \`formControlName\` inside a \`[formGroup]\`, or \`[(value)]\`/\`[(disabled)]\` model bindings for standalone usage -- never invent a plain \`[value]\` input on a control that models value as \`model()\`.
|
|
111
|
+
- Wrap label/control/error groups with \`[nsField]\` where the design system expects it (check get_component_api's \`a11y\` section for aria-describedby wiring).
|
|
112
|
+
- Finish by calling validate_component_usage on the assembled template.`
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
]
|
|
116
|
+
}));
|
|
117
|
+
server.registerPrompt('theme_ns_app', {
|
|
118
|
+
title: 'Theme an ng-solid app',
|
|
119
|
+
description: 'Guides setting up or customizing the Arvisfy theme for an application.',
|
|
120
|
+
argsSchema: {
|
|
121
|
+
brand: z
|
|
122
|
+
.string()
|
|
123
|
+
.optional()
|
|
124
|
+
.describe('Optional brand description or target colors to reflect in the theme.')
|
|
125
|
+
}
|
|
126
|
+
}, ({ brand }) => ({
|
|
127
|
+
messages: [
|
|
128
|
+
{
|
|
129
|
+
role: 'user',
|
|
130
|
+
content: {
|
|
131
|
+
type: 'text',
|
|
132
|
+
text: `Set up ng-solid theming${brand ? ` for this brand: ${brand}` : ''}.
|
|
133
|
+
|
|
134
|
+
Rules:
|
|
135
|
+
- \`provideNsTheme({ preset: Arvisfy, colorScheme: 'system' })\` (or 'light'/'dark') is called exactly once, in the app's root providers -- get_provider("provideNsTheme") has the exact signature.
|
|
136
|
+
- To customize, use \`defineNsPreset(Arvisfy, { ... })\` -- never copy/fork the Arvisfy preset object. Overrides follow the same 3-tier shape: \`primitive\` (raw values only), \`semantic.common\`/\`semantic.colorScheme.{light,dark}\` (reference \`{primitive.path}\`), \`components\` (reference \`{semantic.path}\`).
|
|
137
|
+
- Before proposing an override tree, call validate_theme_preset with \`mode: "overrides"\` on it -- it enforces the same rules \`defineNsPreset\` enforces at runtime (safe primitive values, resolvable references, both-color-scheme coverage) so a bad override fails fast instead of rendering a broken value.
|
|
138
|
+
- Use get_component_tokens / list_theme_tokens / resolve_token to find the exact token(s) that control the part of the UI you're changing, and get_token_drift to sanity-check you're not touching a token component SCSS doesn't actually read.`
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
]
|
|
142
|
+
}));
|
|
143
|
+
server.registerPrompt('migrate_to_ns', {
|
|
144
|
+
title: 'Migrate to ng-solid',
|
|
145
|
+
description: 'Guides migrating UI code from another component library to ng-solid.',
|
|
146
|
+
argsSchema: {
|
|
147
|
+
library: z
|
|
148
|
+
.string()
|
|
149
|
+
.optional()
|
|
150
|
+
.describe('The library being migrated from, e.g. "Angular Material" or "PrimeNG".')
|
|
151
|
+
}
|
|
152
|
+
}, ({ library }) => ({
|
|
153
|
+
messages: [
|
|
154
|
+
{
|
|
155
|
+
role: 'user',
|
|
156
|
+
content: {
|
|
157
|
+
type: 'text',
|
|
158
|
+
text: `Migrate this UI${library ? ` from ${library}` : ''} to the ng-solid (Arvisfy) design system.
|
|
159
|
+
|
|
160
|
+
${INVARIANTS}
|
|
161
|
+
|
|
162
|
+
Workflow:
|
|
163
|
+
1. For each source component, call search_all or suggest_component to find its closest ng-solid equivalent -- do not assume prop names carry over (e.g. ng-solid variants are a closed 5-value union, colors/themes are not free-form).
|
|
164
|
+
2. Call get_component_api on the target to see its real inputs/outputs/models before rewriting bindings, and get_form_control_contract for anything used inside a form.
|
|
165
|
+
3. Replace the source library's module imports and providers with the ng-solid equivalents (get_component_import, get_provider) -- remove the old library's module/theme providers entirely, don't leave both wired up.
|
|
166
|
+
4. Re-theme with get_provider("provideNsTheme") / theme_ns_app instead of porting the source library's theme config.
|
|
167
|
+
5. Validate every rewritten template with validate_component_usage and validate_style_contract.`
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
]
|
|
171
|
+
}));
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=prompts.js.map
|
package/lib/resources.js
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
4
|
+
import { loadIcons, loadSnapshot } from './data.js';
|
|
5
|
+
import { importTrio, renderComponentMarkdown, resolveComponent } from './tools.components.js';
|
|
6
|
+
import { renderGuideMarkdown, resolveGuide } from './tools.guides.js';
|
|
7
|
+
function variable(value) {
|
|
8
|
+
return Array.isArray(value) ? (value[0] ?? '') : value;
|
|
9
|
+
}
|
|
10
|
+
function json(value) {
|
|
11
|
+
return JSON.stringify(value, null, 2);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Resolves a skill's raw `SKILL.md` on disk, mirroring `data.ts`'s `resolveSnapshotPath`
|
|
15
|
+
* fallback chain (published layout vs. dev-with-tsx layout), but returning `null` instead of
|
|
16
|
+
* throwing -- skills are a Fase 5 deliverable and may not exist on disk yet.
|
|
17
|
+
*/
|
|
18
|
+
function resolveSkillFile(relativePath) {
|
|
19
|
+
const candidates = [
|
|
20
|
+
fileURLToPath(new URL(`../${relativePath}`, import.meta.url)),
|
|
21
|
+
fileURLToPath(new URL(`../../${relativePath}`, import.meta.url))
|
|
22
|
+
];
|
|
23
|
+
for (const candidate of candidates) {
|
|
24
|
+
if (existsSync(candidate))
|
|
25
|
+
return candidate;
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
export function registerResources(server) {
|
|
30
|
+
server.registerResource('ns-component', new ResourceTemplate('ns://component/{selector}', {
|
|
31
|
+
list: async () => ({
|
|
32
|
+
resources: loadSnapshot()
|
|
33
|
+
.components.filter((c) => c.visibility === 'public')
|
|
34
|
+
.map((c) => ({
|
|
35
|
+
uri: `ns://component/${c.selector}`,
|
|
36
|
+
name: c.selector,
|
|
37
|
+
title: c.className,
|
|
38
|
+
description: c.summary,
|
|
39
|
+
mimeType: 'application/json'
|
|
40
|
+
}))
|
|
41
|
+
}),
|
|
42
|
+
complete: {
|
|
43
|
+
selector: (value) => loadSnapshot()
|
|
44
|
+
.components.map((c) => c.selector)
|
|
45
|
+
.filter((selector) => selector.startsWith(value))
|
|
46
|
+
}
|
|
47
|
+
}), {
|
|
48
|
+
title: 'ng-solid component doc',
|
|
49
|
+
description: 'Full ComponentDoc as JSON, including the import trio.',
|
|
50
|
+
mimeType: 'application/json'
|
|
51
|
+
}, async (uri, variables) => {
|
|
52
|
+
const snapshot = loadSnapshot();
|
|
53
|
+
const resolution = resolveComponent(snapshot, variable(variables.selector));
|
|
54
|
+
if (!resolution.component) {
|
|
55
|
+
return {
|
|
56
|
+
contents: [
|
|
57
|
+
{
|
|
58
|
+
uri: uri.href,
|
|
59
|
+
mimeType: 'application/json',
|
|
60
|
+
text: json({
|
|
61
|
+
error: resolution.error,
|
|
62
|
+
suggestions: resolution.suggestions
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
contents: [
|
|
70
|
+
{
|
|
71
|
+
uri: uri.href,
|
|
72
|
+
mimeType: 'application/json',
|
|
73
|
+
text: json({
|
|
74
|
+
...importTrio(resolution.component),
|
|
75
|
+
component: resolution.component
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
server.registerResource('ns-component-markdown', new ResourceTemplate('ns://component/{selector}/markdown', {
|
|
82
|
+
list: async () => ({
|
|
83
|
+
resources: loadSnapshot()
|
|
84
|
+
.components.filter((c) => c.visibility === 'public')
|
|
85
|
+
.map((c) => ({
|
|
86
|
+
uri: `ns://component/${c.selector}/markdown`,
|
|
87
|
+
name: c.selector,
|
|
88
|
+
title: `${c.className} (Markdown)`,
|
|
89
|
+
mimeType: 'text/markdown'
|
|
90
|
+
}))
|
|
91
|
+
}),
|
|
92
|
+
complete: {
|
|
93
|
+
selector: (value) => loadSnapshot()
|
|
94
|
+
.components.map((c) => c.selector)
|
|
95
|
+
.filter((selector) => selector.startsWith(value))
|
|
96
|
+
}
|
|
97
|
+
}), {
|
|
98
|
+
title: 'ng-solid component doc (Markdown)',
|
|
99
|
+
description: 'Human-readable component documentation.',
|
|
100
|
+
mimeType: 'text/markdown'
|
|
101
|
+
}, async (uri, variables) => {
|
|
102
|
+
const snapshot = loadSnapshot();
|
|
103
|
+
const resolution = resolveComponent(snapshot, variable(variables.selector));
|
|
104
|
+
const text = resolution.component
|
|
105
|
+
? renderComponentMarkdown(snapshot, resolution.component)
|
|
106
|
+
: `No component matches "${variable(variables.selector)}". Suggestions: ${(resolution.suggestions ?? []).join(', ')}`;
|
|
107
|
+
return { contents: [{ uri: uri.href, mimeType: 'text/markdown', text }] };
|
|
108
|
+
});
|
|
109
|
+
server.registerResource('ns-guide', new ResourceTemplate('ns://guide/{slug}', {
|
|
110
|
+
list: async () => ({
|
|
111
|
+
resources: loadSnapshot().guides.map((g) => ({
|
|
112
|
+
uri: `ns://guide/${g.name}`,
|
|
113
|
+
name: g.name,
|
|
114
|
+
title: g.title,
|
|
115
|
+
description: g.description,
|
|
116
|
+
mimeType: 'text/markdown'
|
|
117
|
+
}))
|
|
118
|
+
}),
|
|
119
|
+
complete: {
|
|
120
|
+
slug: (value) => loadSnapshot()
|
|
121
|
+
.guides.map((g) => g.name)
|
|
122
|
+
.filter((name) => name.startsWith(value))
|
|
123
|
+
}
|
|
124
|
+
}), {
|
|
125
|
+
title: 'ng-solid guide',
|
|
126
|
+
description: 'Guide content generated from packages/mcp/skills/*/SKILL.md.',
|
|
127
|
+
mimeType: 'text/markdown'
|
|
128
|
+
}, async (uri, variables) => {
|
|
129
|
+
const snapshot = loadSnapshot();
|
|
130
|
+
if (snapshot.guides.length === 0) {
|
|
131
|
+
return {
|
|
132
|
+
contents: [
|
|
133
|
+
{
|
|
134
|
+
uri: uri.href,
|
|
135
|
+
mimeType: 'text/markdown',
|
|
136
|
+
text: 'No guides in this snapshot yet.'
|
|
137
|
+
}
|
|
138
|
+
]
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
const resolution = resolveGuide(snapshot.guides, variable(variables.slug));
|
|
142
|
+
const text = resolution.guide
|
|
143
|
+
? renderGuideMarkdown(resolution.guide)
|
|
144
|
+
: `No guide matches "${variable(variables.slug)}". Suggestions: ${(resolution.suggestions ?? []).join(', ')}`;
|
|
145
|
+
return { contents: [{ uri: uri.href, mimeType: 'text/markdown', text }] };
|
|
146
|
+
});
|
|
147
|
+
server.registerResource('ns-skill', new ResourceTemplate('ns://skill/{slug}', {
|
|
148
|
+
list: async () => ({
|
|
149
|
+
resources: loadSnapshot().guides.map((g) => ({
|
|
150
|
+
uri: `ns://skill/${g.name}`,
|
|
151
|
+
name: g.name,
|
|
152
|
+
title: `${g.title} (raw SKILL.md)`,
|
|
153
|
+
mimeType: 'text/markdown'
|
|
154
|
+
}))
|
|
155
|
+
}),
|
|
156
|
+
complete: {
|
|
157
|
+
slug: (value) => loadSnapshot()
|
|
158
|
+
.guides.map((g) => g.name)
|
|
159
|
+
.filter((name) => name.startsWith(value))
|
|
160
|
+
}
|
|
161
|
+
}), {
|
|
162
|
+
title: 'Raw skill file',
|
|
163
|
+
description: 'The raw, front-matter-included SKILL.md a consumer would copy via `install-skills`.',
|
|
164
|
+
mimeType: 'text/markdown'
|
|
165
|
+
}, async (uri, variables) => {
|
|
166
|
+
const snapshot = loadSnapshot();
|
|
167
|
+
const slug = variable(variables.slug);
|
|
168
|
+
const guide = snapshot.guides.find((g) => g.name === slug);
|
|
169
|
+
const relativePath = guide?.skillPath ?? `skills/${slug}/SKILL.md`;
|
|
170
|
+
const filePath = resolveSkillFile(relativePath);
|
|
171
|
+
const text = filePath
|
|
172
|
+
? readFileSync(filePath, 'utf8')
|
|
173
|
+
: `No SKILL.md found for "${slug}" (tried ${relativePath}). Skills ship in Fase 5.`;
|
|
174
|
+
return { contents: [{ uri: uri.href, mimeType: 'text/markdown', text }] };
|
|
175
|
+
});
|
|
176
|
+
server.registerResource('ns-tokens-tier', new ResourceTemplate('ns://tokens/{tier}', {
|
|
177
|
+
list: async () => ({
|
|
178
|
+
resources: ['primitive', 'semantic', 'component'].map((tier) => ({
|
|
179
|
+
uri: `ns://tokens/${tier}`,
|
|
180
|
+
name: tier,
|
|
181
|
+
title: `${tier} tokens`,
|
|
182
|
+
mimeType: 'application/json'
|
|
183
|
+
}))
|
|
184
|
+
}),
|
|
185
|
+
complete: {
|
|
186
|
+
tier: (value) => ['primitive', 'semantic', 'component'].filter((tier) => tier.startsWith(value))
|
|
187
|
+
}
|
|
188
|
+
}), {
|
|
189
|
+
title: 'Theme tokens by tier',
|
|
190
|
+
description: 'Flattened tokens for one tier of the Arvisfy 3-tier preset.',
|
|
191
|
+
mimeType: 'application/json'
|
|
192
|
+
}, async (uri, variables) => {
|
|
193
|
+
const snapshot = loadSnapshot();
|
|
194
|
+
const tier = variable(variables.tier);
|
|
195
|
+
if (!snapshot.theme) {
|
|
196
|
+
return {
|
|
197
|
+
contents: [
|
|
198
|
+
{
|
|
199
|
+
uri: uri.href,
|
|
200
|
+
mimeType: 'application/json',
|
|
201
|
+
text: json({ error: 'theme is missing from the snapshot.' })
|
|
202
|
+
}
|
|
203
|
+
]
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
const payload = tier === 'primitive'
|
|
207
|
+
? snapshot.theme.tiers.primitive
|
|
208
|
+
: tier === 'semantic'
|
|
209
|
+
? snapshot.theme.tiers.semantic
|
|
210
|
+
: tier === 'component'
|
|
211
|
+
? snapshot.theme.tiers.component
|
|
212
|
+
: {
|
|
213
|
+
error: `Unknown tier "${tier}". Expected primitive, semantic or component.`
|
|
214
|
+
};
|
|
215
|
+
return {
|
|
216
|
+
contents: [
|
|
217
|
+
{ uri: uri.href, mimeType: 'application/json', text: json(payload) }
|
|
218
|
+
]
|
|
219
|
+
};
|
|
220
|
+
});
|
|
221
|
+
server.registerResource('ns-tokens-component-group', new ResourceTemplate('ns://tokens/component/{group}', {
|
|
222
|
+
list: async () => ({
|
|
223
|
+
resources: (loadSnapshot().theme?.componentGroups ?? []).map((group) => ({
|
|
224
|
+
uri: `ns://tokens/component/${group}`,
|
|
225
|
+
name: group,
|
|
226
|
+
title: `${group} tokens`,
|
|
227
|
+
mimeType: 'application/json'
|
|
228
|
+
}))
|
|
229
|
+
}),
|
|
230
|
+
complete: {
|
|
231
|
+
group: (value) => (loadSnapshot().theme?.componentGroups ?? []).filter((group) => group.startsWith(value))
|
|
232
|
+
}
|
|
233
|
+
}), {
|
|
234
|
+
title: 'Theme tokens by component group',
|
|
235
|
+
description: 'Component-tier tokens scoped to one group, e.g. "button".',
|
|
236
|
+
mimeType: 'application/json'
|
|
237
|
+
}, async (uri, variables) => {
|
|
238
|
+
const snapshot = loadSnapshot();
|
|
239
|
+
const group = variable(variables.group);
|
|
240
|
+
if (!snapshot.theme) {
|
|
241
|
+
return {
|
|
242
|
+
contents: [
|
|
243
|
+
{
|
|
244
|
+
uri: uri.href,
|
|
245
|
+
mimeType: 'application/json',
|
|
246
|
+
text: json({ error: 'theme is missing from the snapshot.' })
|
|
247
|
+
}
|
|
248
|
+
]
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
const tokens = snapshot.theme.tiers.component.filter((t) => t.path === group || t.path.startsWith(`${group}.`));
|
|
252
|
+
return {
|
|
253
|
+
contents: [
|
|
254
|
+
{ uri: uri.href, mimeType: 'application/json', text: json(tokens) }
|
|
255
|
+
]
|
|
256
|
+
};
|
|
257
|
+
});
|
|
258
|
+
server.registerResource('ns-preset-arvisfy', 'ns://preset/arvisfy', {
|
|
259
|
+
title: 'The Arvisfy preset',
|
|
260
|
+
description: 'The full 3-tier Arvisfy theme preset, drift report and validation rules.',
|
|
261
|
+
mimeType: 'application/json'
|
|
262
|
+
}, async (uri) => {
|
|
263
|
+
const snapshot = loadSnapshot();
|
|
264
|
+
return {
|
|
265
|
+
contents: [
|
|
266
|
+
{
|
|
267
|
+
uri: uri.href,
|
|
268
|
+
mimeType: 'application/json',
|
|
269
|
+
text: json(snapshot.theme ?? { error: 'theme is missing from the snapshot.' })
|
|
270
|
+
}
|
|
271
|
+
]
|
|
272
|
+
};
|
|
273
|
+
});
|
|
274
|
+
server.registerResource('ns-icons-catalog', 'ns://icons/catalog', {
|
|
275
|
+
title: 'Icon catalog',
|
|
276
|
+
description: 'The full 1790-icon Lucide catalog (name <-> symbol <-> keywords, no SVG bodies).',
|
|
277
|
+
mimeType: 'application/json'
|
|
278
|
+
}, async (uri) => {
|
|
279
|
+
const icons = loadIcons();
|
|
280
|
+
return {
|
|
281
|
+
contents: [
|
|
282
|
+
{ uri: uri.href, mimeType: 'application/json', text: json(icons) }
|
|
283
|
+
]
|
|
284
|
+
};
|
|
285
|
+
});
|
|
286
|
+
server.registerResource('ns-style-contract', 'ns://contract/style', {
|
|
287
|
+
title: 'Style contract',
|
|
288
|
+
description: 'The style-architecture rules enforced by `pnpm quality:styles`.',
|
|
289
|
+
mimeType: 'application/json'
|
|
290
|
+
}, async (uri) => {
|
|
291
|
+
const snapshot = loadSnapshot();
|
|
292
|
+
return {
|
|
293
|
+
contents: [
|
|
294
|
+
{
|
|
295
|
+
uri: uri.href,
|
|
296
|
+
mimeType: 'application/json',
|
|
297
|
+
text: json(snapshot.styleContract ?? {
|
|
298
|
+
error: 'styleContract is missing from the snapshot.'
|
|
299
|
+
})
|
|
300
|
+
}
|
|
301
|
+
]
|
|
302
|
+
};
|
|
303
|
+
});
|
|
304
|
+
server.registerResource('ns-snapshot-stats', 'ns://snapshot/stats', {
|
|
305
|
+
title: 'Snapshot stats',
|
|
306
|
+
description: 'Component/service/token/icon counts and generation metadata for this snapshot.',
|
|
307
|
+
mimeType: 'application/json'
|
|
308
|
+
}, async (uri) => {
|
|
309
|
+
const snapshot = loadSnapshot();
|
|
310
|
+
return {
|
|
311
|
+
contents: [
|
|
312
|
+
{
|
|
313
|
+
uri: uri.href,
|
|
314
|
+
mimeType: 'application/json',
|
|
315
|
+
text: json({
|
|
316
|
+
schemaVersion: snapshot.schemaVersion,
|
|
317
|
+
version: snapshot.version,
|
|
318
|
+
generatedAt: snapshot.generatedAt,
|
|
319
|
+
framework: snapshot.framework,
|
|
320
|
+
packages: snapshot.packages,
|
|
321
|
+
stats: snapshot.stats
|
|
322
|
+
})
|
|
323
|
+
}
|
|
324
|
+
]
|
|
325
|
+
};
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
//# sourceMappingURL=resources.js.map
|
package/lib/server.d.ts
ADDED
package/lib/server.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { loadSnapshot } from './data.js';
|
|
4
|
+
import { registerPrompts } from './prompts.js';
|
|
5
|
+
import { registerResources } from './resources.js';
|
|
6
|
+
import { registerTools } from './tools.js';
|
|
7
|
+
export function createNsSolidMcpServer() {
|
|
8
|
+
const snapshot = loadSnapshot();
|
|
9
|
+
const server = new McpServer({
|
|
10
|
+
name: 'ng-solid',
|
|
11
|
+
version: snapshot.version,
|
|
12
|
+
title: 'ng-solid Arvisfy design system'
|
|
13
|
+
}, {
|
|
14
|
+
capabilities: {
|
|
15
|
+
tools: {},
|
|
16
|
+
resources: {},
|
|
17
|
+
prompts: {}
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
registerTools(server);
|
|
21
|
+
registerResources(server);
|
|
22
|
+
registerPrompts(server);
|
|
23
|
+
return server;
|
|
24
|
+
}
|
|
25
|
+
export async function runNsSolidMcpServer() {
|
|
26
|
+
const server = createNsSolidMcpServer();
|
|
27
|
+
const transport = new StdioServerTransport();
|
|
28
|
+
await server.connect(transport);
|
|
29
|
+
console.error('ng-solid MCP server running on stdio');
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
import type { StyleContractDoc } from './types.js';
|
|
12
|
+
export type StyleContractKind = 'template' | 'scss' | 'component-ts';
|
|
13
|
+
export interface StyleContractViolation {
|
|
14
|
+
ruleId: string;
|
|
15
|
+
severity: 'error' | 'warning';
|
|
16
|
+
message: string;
|
|
17
|
+
snippet: string;
|
|
18
|
+
line?: number;
|
|
19
|
+
}
|
|
20
|
+
export interface StyleContractResult {
|
|
21
|
+
valid: boolean;
|
|
22
|
+
violations: StyleContractViolation[];
|
|
23
|
+
}
|
|
24
|
+
/** Parses a stringified `RegExp` literal (e.g. `"/^ns-.../"` or `"/.../i"`) back into a RegExp. */
|
|
25
|
+
export declare function parseRegexLiteral(literal: string): RegExp;
|
|
26
|
+
/**
|
|
27
|
+
* Validates a code snippet against the ng-solid style-architecture contract.
|
|
28
|
+
*
|
|
29
|
+
* @param code the raw source text to validate (a template, a component SCSS file, or a
|
|
30
|
+
* component `.ts` source)
|
|
31
|
+
* @param kind which contract rules apply -- `'template'` runs the BEM/Tailwind-utility checks,
|
|
32
|
+
* `'scss'` runs the `@reference`/`@use`/raw-value checks, `'component-ts'` runs the
|
|
33
|
+
* inline-`styles:` check plus the template checks (component `.ts` files can carry an inline
|
|
34
|
+
* template)
|
|
35
|
+
* @param contract `snapshot.styleContract` -- the canonical rule/pattern set, never duplicated here
|
|
36
|
+
*/
|
|
37
|
+
export declare function validateStyleContract(code: string, kind: StyleContractKind, contract: StyleContractDoc): StyleContractResult;
|