@besaitech/ng-design-system-mcp 0.0.5

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.
@@ -0,0 +1,143 @@
1
+ /**
2
+ * The doc-index schema — the single source of truth for the shape of `docs.json`.
3
+ *
4
+ * Imported by BOTH the build-time generator (`scripts/`) and the runtime MCP
5
+ * server (`packages/mcp-server/`). The server re-validates the bundled artifact
6
+ * with this schema at startup, so the two can never disagree on shape.
7
+ *
8
+ * Uses zod v4 (already a workspace dependency).
9
+ */
10
+ import { z } from 'zod';
11
+ /** How a signal input's raw value is coerced by Angular. */
12
+ export const TransformKind = z.enum([
13
+ 'booleanAttribute',
14
+ 'numberAttribute',
15
+ 'toIconPx',
16
+ ]);
17
+ export const InputSchema = z.object({
18
+ /** Public name (the alias when one is set, e.g. `sdsCell`). */
19
+ name: z.string(),
20
+ /** Resolved public type, e.g. `SdsButtonVariant`, `boolean`, `Date | null`. */
21
+ type: z.string(),
22
+ /** Default literal as written in source, or null for required/computed defaults. */
23
+ default: z.string().nullable(),
24
+ required: z.boolean(),
25
+ /** Coercion applied via `transform:` (widens the accepted type), or null. */
26
+ transform: TransformKind.nullable(),
27
+ /** Set when the public input name differs from the class property name. */
28
+ alias: z.string().nullable(),
29
+ /** Doc-comment on the input property, if any. */
30
+ jsdoc: z.string().nullable(),
31
+ });
32
+ export const OutputSchema = z.object({
33
+ name: z.string(),
34
+ /** Event payload type. */
35
+ type: z.string(),
36
+ /** True when synthesized from a `model()` two-way binding (`<name>Change`). */
37
+ fromModel: z.boolean(),
38
+ });
39
+ export const ExportedTypeSchema = z.object({
40
+ name: z.string(),
41
+ kind: z.enum(['union', 'interface', 'type', 'const']),
42
+ /** The declaration body / right-hand side, trimmed for display. */
43
+ body: z.string(),
44
+ });
45
+ export const ExampleSchema = z.object({
46
+ sectionTitle: z.string(),
47
+ sectionDescription: z.string().nullable(),
48
+ /** The curated, copyable snippet (the `C`-map value) — the source of truth. */
49
+ code: z.string(),
50
+ /** The live projected markup from the showcase `<app-demo>`, if captured. */
51
+ runnableExample: z.string().nullable(),
52
+ stack: z.boolean(),
53
+ dark: z.boolean(),
54
+ });
55
+ export const ComponentGroup = z.enum([
56
+ 'primitive',
57
+ 'form',
58
+ 'layout',
59
+ 'data',
60
+ ]);
61
+ export const ComponentSchema = z.object({
62
+ className: z.string(),
63
+ selector: z.string(),
64
+ kind: z.enum(['component', 'directive']),
65
+ group: ComponentGroup,
66
+ /** Class name(s) a consumer must import, comma-joined for compound widgets. */
67
+ importName: z.string(),
68
+ /** Curated FR description from the showcase `<app-doc-page description>`. */
69
+ description: z.string().nullable(),
70
+ /** Shorter FR one-liner from the overview catalogue. */
71
+ overviewDescription: z.string().nullable(),
72
+ /** Class-level doc-comment text. */
73
+ jsdoc: z.string().nullable(),
74
+ /** Class-level `@example` snippet, if present. */
75
+ example: z.string().nullable(),
76
+ /** Implements ControlValueAccessor (its value is bound via ngModel/formControl, NOT [value]). */
77
+ cva: z.boolean(),
78
+ usesNgModel: z.boolean(),
79
+ inputs: InputSchema.array(),
80
+ outputs: OutputSchema.array(),
81
+ exportedTypes: ExportedTypeSchema.array(),
82
+ /** Named content-projection slots, e.g. `['[sdsIconLeading]','[sdsIconTrailing]']`. */
83
+ projectionSlots: z.string().array(),
84
+ /** `host: {}` bindings (class/attr/event), for documenting host behaviour. */
85
+ hostBindings: z.record(z.string(), z.string()),
86
+ examples: ExampleSchema.array(),
87
+ });
88
+ export const ServiceSchema = z.object({
89
+ className: z.string(),
90
+ providedIn: z.enum(['root', 'component']),
91
+ /** Public signals/methods/types the store exposes. */
92
+ exports: z.string().array(),
93
+ jsdoc: z.string().nullable(),
94
+ });
95
+ export const TokenSchema = z.object({
96
+ /** The `@theme` token name, e.g. `color-brand-600`. */
97
+ token: z.string(),
98
+ family: z.enum([
99
+ 'brand',
100
+ 'accent',
101
+ 'ink',
102
+ 'surface',
103
+ 'radius',
104
+ 'status',
105
+ 'font',
106
+ 'layout',
107
+ ]),
108
+ /** Example Tailwind utility, e.g. `bg-brand-600`. */
109
+ tailwindUtility: z.string().nullable(),
110
+ /** CSS variable reference, e.g. `var(--color-brand-600)`. */
111
+ cssVar: z.string(),
112
+ semanticNote: z.string().nullable(),
113
+ });
114
+ export const InstallSchema = z.object({
115
+ npmrc: z.string(),
116
+ installCommand: z.string(),
117
+ tailwindImport: z.string(),
118
+ sourceDirective: z.string(),
119
+ themeBlock: z.string(),
120
+ peerDeps: z.string().array(),
121
+ standaloneNote: z.string(),
122
+ });
123
+ export const DocIndexSchema = z.object({
124
+ /** Bumped when the shape below changes (independent of library version). */
125
+ schemaVersion: z.string(),
126
+ /** Read from projects/saitech-design-system/package.json — the library version these docs describe. */
127
+ libraryVersion: z.string(),
128
+ packageName: z.literal('@besaitech/ng-design-system'),
129
+ /** Bare specifier used inside this monorepo (tsconfig path alias). */
130
+ devImport: z.literal('saitech-design-system'),
131
+ generatedAt: z.string(),
132
+ componentCount: z.number(),
133
+ components: ComponentSchema.array(),
134
+ directives: ComponentSchema.array(),
135
+ services: ServiceSchema.array(),
136
+ icons: z.object({
137
+ names: z.string().array(),
138
+ /** Registry entries that deviate from the inner-SVG convention (e.g. `hand`). */
139
+ outliers: z.string().array(),
140
+ }),
141
+ tokens: TokenSchema.array(),
142
+ install: InstallSchema,
143
+ });
package/dist/index.js ADDED
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * saitech-design-system MCP server entry point.
4
+ *
5
+ * Default: load the bundled, version-stamped data/docs.json and serve over stdio
6
+ * (zero-config for Claude Code / Cursor / Windsurf, fully offline).
7
+ *
8
+ * Flags:
9
+ * --source <repoRoot> Contributor mode: regenerate the index from a working
10
+ * tree (runs scripts/generate-doc-index.ts if present)
11
+ * and serve that, so library edits show up build-free.
12
+ * --data <path> Serve a specific docs.json instead of the bundled one.
13
+ * --http Reserved for a future shared HTTP instance (not yet
14
+ * operated; falls back to stdio with a notice).
15
+ */
16
+ import { execFileSync } from 'node:child_process';
17
+ import { existsSync } from 'node:fs';
18
+ import { resolve } from 'node:path';
19
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
20
+ import { buildServer } from './server.js';
21
+ import { loadIndex } from './load-index.js';
22
+ function parseArgs(argv) {
23
+ const args = { http: false };
24
+ for (let i = 0; i < argv.length; i++) {
25
+ const a = argv[i];
26
+ if (a === '--source')
27
+ args.source = argv[++i];
28
+ else if (a === '--data')
29
+ args.data = argv[++i];
30
+ else if (a === '--http')
31
+ args.http = true;
32
+ }
33
+ return args;
34
+ }
35
+ function resolveDataPath(args) {
36
+ if (args.data)
37
+ return resolve(args.data);
38
+ if (args.source) {
39
+ const repo = resolve(args.source);
40
+ const generator = resolve(repo, 'scripts/generate-doc-index.ts');
41
+ const data = resolve(repo, 'packages/mcp-server/data/docs.json');
42
+ if (existsSync(generator)) {
43
+ try {
44
+ // Send generator output to our stderr so it never corrupts the stdio JSON-RPC stream.
45
+ execFileSync(process.execPath, [generator], { stdio: ['ignore', 2, 2] });
46
+ }
47
+ catch (err) {
48
+ console.error(`[saitech-design-system-mcp] --source regeneration failed; using existing index. ${String(err)}`);
49
+ }
50
+ }
51
+ if (existsSync(data))
52
+ return data;
53
+ console.error(`[saitech-design-system-mcp] --source index not found at ${data}; falling back to bundled.`);
54
+ }
55
+ return undefined; // bundled default
56
+ }
57
+ async function main() {
58
+ const args = parseArgs(process.argv.slice(2));
59
+ if (args.http) {
60
+ console.error('[saitech-design-system-mcp] --http is not operated yet; serving over stdio.');
61
+ }
62
+ const loaded = loadIndex(resolveDataPath(args));
63
+ const server = buildServer(loaded);
64
+ console.error(`[saitech-design-system-mcp] serving ${loaded.index.packageName}@${loaded.index.libraryVersion} ` +
65
+ `(${loaded.index.componentCount} components) over stdio`);
66
+ const transport = new StdioServerTransport();
67
+ await server.connect(transport);
68
+ }
69
+ main().catch((err) => {
70
+ console.error('[saitech-design-system-mcp] fatal:', err);
71
+ process.exit(1);
72
+ });
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Loads + zod-validates the bundled `data/docs.json` and builds the in-memory
3
+ * lookup structures the tools query. No AST or network at runtime: the heavy
4
+ * extraction already happened at build time.
5
+ */
6
+ import { readFileSync } from 'node:fs';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { dirname, resolve } from 'node:path';
9
+ import { DocIndexSchema } from './doc-index.schema.js';
10
+ /** Path to the bundled artifact, relative to the compiled dist/ directory. */
11
+ function defaultDataPath() {
12
+ const here = dirname(fileURLToPath(import.meta.url));
13
+ return resolve(here, '../data/docs.json');
14
+ }
15
+ export function loadIndex(dataPath = defaultDataPath()) {
16
+ const raw = JSON.parse(readFileSync(dataPath, 'utf8'));
17
+ const index = DocIndexSchema.parse(raw);
18
+ const all = [...index.components, ...index.directives];
19
+ const byClass = new Map();
20
+ const bySelector = new Map();
21
+ for (const c of all) {
22
+ byClass.set(c.className.toLowerCase(), c);
23
+ bySelector.set(c.selector.toLowerCase(), c);
24
+ }
25
+ return {
26
+ index,
27
+ byClass,
28
+ bySelector,
29
+ all,
30
+ iconSet: new Set(index.icons.names),
31
+ };
32
+ }
33
+ /** Resolve a component by class name (`SdsButton`), selector (`sds-button`), or bare (`button`). */
34
+ export function resolveComponent(loaded, query) {
35
+ const q = query.trim().toLowerCase();
36
+ return (loaded.bySelector.get(q) ??
37
+ loaded.byClass.get(q) ??
38
+ loaded.bySelector.get(`sds-${q}`) ??
39
+ loaded.byClass.get(`sds${q.replace(/-/g, '')}`) ??
40
+ loaded.all.find((c) => c.selector.includes(q) || c.className.toLowerCase().includes(q)) ??
41
+ null);
42
+ }
43
+ /** The published import line for a component. */
44
+ export function importLine(loaded, c) {
45
+ return `import { ${c.importName} } from '${loaded.index.packageName}';`;
46
+ }
47
+ export function searchIndex(loaded, query, limit = 8) {
48
+ const q = query.trim().toLowerCase();
49
+ if (!q)
50
+ return [];
51
+ const hits = [];
52
+ for (const c of loaded.all) {
53
+ let best = null;
54
+ const consider = (field, value, weight) => {
55
+ if (!value)
56
+ return;
57
+ const idx = value.toLowerCase().indexOf(q);
58
+ if (idx === -1)
59
+ return;
60
+ // exact field equality scores highest, then prefix, then substring.
61
+ const score = weight + (value.toLowerCase() === q ? 100 : idx === 0 ? 40 : 20) - Math.min(idx, 19);
62
+ if (!best || score > best.score) {
63
+ best = { field, snippet: value.slice(0, 140), score };
64
+ }
65
+ };
66
+ consider('className', c.className, 60);
67
+ consider('selector', c.selector, 60);
68
+ consider('description', c.description, 30);
69
+ consider('overviewDescription', c.overviewDescription, 30);
70
+ for (const i of c.inputs)
71
+ consider(`input:${i.name}`, i.name, 25);
72
+ for (const t of c.exportedTypes)
73
+ consider(`type:${t.name}`, t.name, 20);
74
+ for (const e of c.examples)
75
+ consider(`example:${e.sectionTitle}`, e.sectionTitle, 15);
76
+ if (best) {
77
+ const b = best;
78
+ hits.push({ className: c.className, selector: c.selector, matchedField: b.field, snippet: b.snippet, score: b.score });
79
+ }
80
+ }
81
+ hits.sort((a, b) => b.score - a.score);
82
+ return hits.slice(0, limit);
83
+ }
84
+ function editDistance(a, b) {
85
+ const dp = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
86
+ for (let j = 0; j <= b.length; j++)
87
+ dp[0][j] = j;
88
+ for (let i = 1; i <= a.length; i++) {
89
+ for (let j = 1; j <= b.length; j++) {
90
+ dp[i][j] =
91
+ a[i - 1] === b[j - 1]
92
+ ? dp[i - 1][j - 1]
93
+ : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
94
+ }
95
+ }
96
+ return dp[a.length][b.length];
97
+ }
98
+ /** Closest registered icon names for a (possibly mistyped) query. */
99
+ export function suggestIcons(loaded, query, limit = 5) {
100
+ const q = query.trim().toLowerCase();
101
+ return [...loaded.iconSet]
102
+ .map((name) => ({
103
+ name,
104
+ d: name.includes(q) || q.includes(name) ? 0 : editDistance(q, name),
105
+ }))
106
+ .sort((a, b) => a.d - b.d)
107
+ .slice(0, limit)
108
+ .map((x) => x.name);
109
+ }
@@ -0,0 +1,47 @@
1
+ import { z } from 'zod';
2
+ function userMessage(text) {
3
+ return { messages: [{ role: 'user', content: { type: 'text', text } }] };
4
+ }
5
+ export function registerPrompts(server, loaded) {
6
+ const pkg = loaded.index.packageName;
7
+ server.registerPrompt('sds_generate_component_usage', {
8
+ title: 'Generate saitech-design-system component usage',
9
+ description: 'Produce a correct, validated Angular 20 snippet that uses an saitech-design-system component.',
10
+ argsSchema: {
11
+ component: z.string().describe('Component class name, selector, or bare name (e.g. SdsButton).'),
12
+ intent: z.string().optional().describe('What the snippet should do, e.g. "a save button with a leading icon".'),
13
+ },
14
+ }, ({ component, intent }) => userMessage(`Generate an Angular 20 (standalone, signals) snippet that uses the saitech-design-system component "${component}"` +
15
+ (intent ? ` to: ${intent}.` : '.') +
16
+ `\n\nSteps:\n` +
17
+ `1. Call sds_get_component with name="${component}" to get its inputs, outputs, enum types, projection slots and whether it is a ControlValueAccessor.\n` +
18
+ `2. Call sds_get_examples with name="${component}" for a copyable starting point.\n` +
19
+ `3. Emit a self-contained component: import { ... } from '${pkg}', add it to the standalone component's imports array, and use only valid selectors, input names and enum values. For any <sds-icon name="…">, only use names you confirmed with sds_validate_icon.\n` +
20
+ `4. Finally, call sds_validate_usage on your generated template and fix every error it reports.`));
21
+ server.registerPrompt('sds_scaffold_form', {
22
+ title: 'Scaffold an saitech-design-system reactive form',
23
+ description: 'Wire saitech-design-system form controls with Angular reactive forms, respecting their ControlValueAccessor contract.',
24
+ argsSchema: {
25
+ fields: z.string().describe('Comma-separated fields, e.g. "name:input, birthDate:date-picker, role:select".'),
26
+ },
27
+ }, ({ fields }) => userMessage(`Scaffold an Angular 20 reactive form using saitech-design-system form controls for these fields: ${fields}.\n\n` +
28
+ `Rules:\n` +
29
+ `- Use ReactiveFormsModule + a FormGroup; bind each control with formControlName.\n` +
30
+ `- saitech-design-system form controls (SdsInput, SdsTextarea, SdsSelect, SdsMultiSelect, SdsDatePicker, SdsCheckbox, SdsToggleSwitch, SdsPhoneInput) implement ControlValueAccessor — their value flows through the form control, NEVER via a [value] binding.\n` +
31
+ `- Call sds_get_component for each control to get correct inputs (label, hint, error, size, options, etc.) and import them from '${pkg}'.\n` +
32
+ `- Validate the final template with sds_validate_usage.`));
33
+ server.registerPrompt('sds_build_data_table', {
34
+ title: 'Build an saitech-design-system data table',
35
+ description: 'Assemble SdsTable with columns, custom cell templates, selection and pagination.',
36
+ argsSchema: {
37
+ columns: z.string().describe('Comma-separated columns, e.g. "name, service, status".'),
38
+ features: z.string().optional().describe('Optional features, e.g. "selection, pagination, custom status cell".'),
39
+ },
40
+ }, ({ columns, features }) => userMessage(`Build an SdsTable for columns: ${columns}.` +
41
+ (features ? ` Features: ${features}.` : '') +
42
+ `\n\nSteps:\n` +
43
+ `1. Call sds_get_component for SdsTable (note its inputs like [value], [columns], selectionMode, and its two-way [(selection)] model) and for SdsTableCell (the structural directive, public input alias "sdsCell") and SdsPaginator if pagination is requested.\n` +
44
+ `2. Call sds_get_examples for sds-table to see the column-definition and <ng-template sdsCell> patterns.\n` +
45
+ `3. Import the needed classes from '${pkg}'. Use <ng-template sdsCell="field"> for custom cells.\n` +
46
+ `4. Validate the final template with sds_validate_usage.`));
47
+ }
package/dist/render.js ADDED
@@ -0,0 +1,102 @@
1
+ /** Escape `|` so union types don't break Markdown table cells. */
2
+ function cell(text) {
3
+ return text.replace(/\|/g, '\\|');
4
+ }
5
+ function inputRow(i) {
6
+ const flags = [
7
+ i.required ? 'required' : '',
8
+ i.transform ? `transform: ${i.transform}` : '',
9
+ i.alias ? `alias: ${i.alias}` : '',
10
+ ]
11
+ .filter(Boolean)
12
+ .join('; ');
13
+ const notes = [i.jsdoc ?? '', flags].filter(Boolean).join(' — ');
14
+ return `| \`${cell(i.name)}\` | \`${cell(i.type)}\` | ${i.default === null ? '—' : `\`${cell(i.default)}\``} | ${cell(notes || '')} |`;
15
+ }
16
+ export function renderComponentMarkdown(index, c) {
17
+ const out = [];
18
+ out.push(`# ${c.className} \`<${c.selector}>\``);
19
+ out.push('');
20
+ if (c.description)
21
+ out.push(c.description, '');
22
+ out.push('```ts', `import { ${c.importName} } from '${index.packageName}';`, '```', '');
23
+ if (c.cva) {
24
+ out.push('> ControlValueAccessor: bind the value with `[(ngModel)]` or `formControlName`, never `[value]`.', '');
25
+ }
26
+ if (c.inputs.length) {
27
+ out.push('## Inputs', '', '| name | type | default | notes |', '| --- | --- | --- | --- |');
28
+ for (const i of c.inputs)
29
+ out.push(inputRow(i));
30
+ out.push('');
31
+ }
32
+ if (c.outputs.length) {
33
+ out.push('## Outputs', '', '| name | payload | source |', '| --- | --- | --- |');
34
+ for (const o of c.outputs)
35
+ out.push(`| \`${cell(o.name)}\` | \`${cell(o.type)}\` | ${o.fromModel ? 'model two-way' : 'output'} |`);
36
+ out.push('');
37
+ }
38
+ if (c.projectionSlots.length) {
39
+ out.push('## Content projection', '', c.projectionSlots.map((s) => `- \`${s}\``).join('\n'), '');
40
+ }
41
+ if (c.exportedTypes.length) {
42
+ out.push('## Exported types', '');
43
+ for (const t of c.exportedTypes)
44
+ out.push(`- \`${t.name}\` (${t.kind}): \`${t.body}\``);
45
+ out.push('');
46
+ }
47
+ if (c.examples.length) {
48
+ out.push('## Examples', '');
49
+ for (const e of c.examples) {
50
+ out.push(`### ${e.sectionTitle}`);
51
+ if (e.sectionDescription)
52
+ out.push('', e.sectionDescription);
53
+ out.push('', '```html', e.code, '```', '');
54
+ }
55
+ }
56
+ return out.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n';
57
+ }
58
+ export function renderLlmsTxt(index) {
59
+ const out = [];
60
+ out.push(`# saitech-design-system (${index.packageName}) v${index.libraryVersion}`);
61
+ out.push('');
62
+ out.push('> Angular 20 standalone component library (StaffKit / Cliniques universitaires Saitech), ' +
63
+ 'styled with Tailwind CSS v4. Components are `Sds*` classes with `sds-*` selectors. ' +
64
+ 'Generated from source; do not hand-edit.');
65
+ out.push('');
66
+ out.push('## Setup');
67
+ out.push(`- Install: \`${index.install.installCommand}\` (private GitLab registry — see .npmrc below)`);
68
+ out.push(`- Import: \`import { SdsButton } from '${index.packageName}';\``);
69
+ out.push('- Tailwind v4 is required in the consuming app: `@import "tailwindcss";`, the `@source` directive for the package, and the full `@theme` token block — otherwise components render unstyled.');
70
+ out.push('');
71
+ const groups = ['primitive', 'form', 'layout', 'data'];
72
+ out.push('## Components');
73
+ for (const g of groups) {
74
+ const items = index.components.filter((c) => c.group === g);
75
+ if (!items.length)
76
+ continue;
77
+ out.push('', `### ${g}`);
78
+ for (const c of items) {
79
+ const desc = c.overviewDescription ?? c.description ?? '';
80
+ out.push(`- [${c.className}](components/${c.selector}.md) \`<${c.selector}>\`${desc ? ` — ${desc}` : ''}`);
81
+ }
82
+ }
83
+ out.push('');
84
+ if (index.directives.length) {
85
+ out.push('## Directives');
86
+ for (const d of index.directives)
87
+ out.push(`- [${d.className}](components/${d.selector.replace(/[^a-z0-9-]/gi, '-')}.md) \`${d.selector}\``);
88
+ out.push('');
89
+ }
90
+ out.push('## Design tokens');
91
+ out.push('Defined in `@theme {}` (Tailwind utilities + CSS variables). Families: ' +
92
+ [...new Set(index.tokens.map((t) => t.family))].join(', ') + '.');
93
+ out.push('');
94
+ out.push('## Icons');
95
+ out.push(`\`<sds-icon name="…">\` — ${index.icons.names.length} registered names: ` + index.icons.names.join(', ') + '.');
96
+ out.push('');
97
+ return out.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n';
98
+ }
99
+ /** Filesystem-safe markdown filename for a selector (directives have `[...]`). */
100
+ export function markdownSlug(selector) {
101
+ return selector.replace(/[^a-z0-9-]/gi, '-');
102
+ }