@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.
package/dist/server.js ADDED
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Builds the MCP server: registers the documentation tools, resources and
3
+ * prompts over the loaded doc-index. Every tool answers from in-memory lookups.
4
+ */
5
+ import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
6
+ import { z } from 'zod';
7
+ import { importLine, resolveComponent, searchIndex, suggestIcons, } from './load-index.js';
8
+ import { validateUsage } from './validate-usage.js';
9
+ import { registerPrompts } from './prompts.js';
10
+ import { markdownSlug, renderComponentMarkdown, renderLlmsTxt } from './render.js';
11
+ function json(value) {
12
+ return { content: [{ type: 'text', text: JSON.stringify(value, null, 2) }] };
13
+ }
14
+ function errorText(text) {
15
+ return { content: [{ type: 'text', text }], isError: true };
16
+ }
17
+ export function buildServer(loaded) {
18
+ const { index } = loaded;
19
+ const server = new McpServer({
20
+ name: 'saitech-design-system',
21
+ version: index.libraryVersion,
22
+ });
23
+ // --- Tools ----------------------------------------------------------------
24
+ server.registerTool('sds_list_components', {
25
+ title: 'List saitech-design-system components',
26
+ description: 'List the saitech-design-system design-system components (and directives/services). Start here to discover what is available before generating code.',
27
+ inputSchema: {
28
+ kind: z.enum(['component', 'directive', 'service']).optional().describe('Filter by kind (default: component).'),
29
+ group: z.enum(['primitive', 'form', 'layout', 'data']).optional().describe('Filter components by group.'),
30
+ },
31
+ }, async ({ kind, group }) => {
32
+ const k = kind ?? 'component';
33
+ if (k === 'service') {
34
+ return json(index.services);
35
+ }
36
+ const source = k === 'directive' ? index.directives : index.components;
37
+ const list = source
38
+ .filter((c) => (group ? c.group === group : true))
39
+ .map((c) => ({
40
+ className: c.className,
41
+ selector: c.selector,
42
+ group: c.group,
43
+ importName: c.importName,
44
+ hasExample: c.examples.length > 0,
45
+ description: c.overviewDescription ?? c.description ?? null,
46
+ }));
47
+ return json({ count: list.length, components: list });
48
+ });
49
+ server.registerTool('sds_get_component', {
50
+ title: 'Get saitech-design-system component API',
51
+ description: 'Full API for one component: inputs (with types, defaults, transforms, required, alias), outputs (including synthesized two-way `model()` change-outputs), exported types, content-projection slots, host bindings, the import line, and whether it is a ControlValueAccessor (bind value via ngModel/formControl, not [value]). The primary lookup before writing saitech-design-system markup.',
52
+ inputSchema: {
53
+ name: z
54
+ .string()
55
+ .describe('Component class name (SdsButton), selector (sds-button), or bare name (button).'),
56
+ },
57
+ }, async ({ name }) => {
58
+ const c = resolveComponent(loaded, name);
59
+ if (!c) {
60
+ const near = searchIndex(loaded, name, 5).map((h) => h.selector);
61
+ return errorText(`No saitech-design-system component matches "${name}". Closest: ${near.join(', ') || '(none)'}. ` +
62
+ `Use sds_list_components to see all.`);
63
+ }
64
+ return json({ ...c, importLine: importLine(loaded, c) });
65
+ });
66
+ server.registerTool('sds_get_examples', {
67
+ title: 'Get saitech-design-system usage examples',
68
+ description: 'Curated, copyable usage snippets for a component, grouped by their (French) section taxonomy. The `code` field is the canonical doc snippet to copy; `runnableExample` is the live demo markup.',
69
+ inputSchema: {
70
+ name: z.string().describe('Component class name, selector, or bare name.'),
71
+ section: z.string().optional().describe('Filter to sections whose title contains this text.'),
72
+ },
73
+ }, async ({ name, section }) => {
74
+ const c = resolveComponent(loaded, name);
75
+ if (!c)
76
+ return errorText(`No saitech-design-system component matches "${name}".`);
77
+ let examples = c.examples;
78
+ if (section) {
79
+ const s = section.toLowerCase();
80
+ examples = examples.filter((e) => e.sectionTitle.toLowerCase().includes(s));
81
+ }
82
+ if (examples.length === 0) {
83
+ return json({ selector: c.selector, examples: [], note: 'No curated examples for this component.' });
84
+ }
85
+ return json({ selector: c.selector, importLine: importLine(loaded, c), examples });
86
+ });
87
+ server.registerTool('sds_search', {
88
+ title: 'Search saitech-design-system docs',
89
+ description: 'Keyword search across component names, selectors, descriptions, input names, exported types and example section titles.',
90
+ inputSchema: {
91
+ query: z.string().describe('Search terms, e.g. "date", "selection", "icon button".'),
92
+ limit: z.number().int().min(1).max(30).optional().describe('Max results (default 8).'),
93
+ },
94
+ }, async ({ query, limit }) => {
95
+ return json({ query, results: searchIndex(loaded, query, limit ?? 8) });
96
+ });
97
+ server.registerTool('sds_validate_usage', {
98
+ title: 'Validate saitech-design-system markup',
99
+ description: 'Lint a candidate Angular snippet against the saitech-design-system API: unknown selectors, invalid input names, invalid enum values, missing required inputs, unregistered icon names, and binding [value] on a ControlValueAccessor control. Run this after generating saitech-design-system markup.',
100
+ inputSchema: {
101
+ code: z.string().describe('The Angular template / component snippet to check.'),
102
+ },
103
+ }, async ({ code }) => json(validateUsage(loaded, code)));
104
+ server.registerTool('sds_validate_icon', {
105
+ title: 'Validate an sds-icon name',
106
+ description: 'Check whether an icon name is registered in SDS_ICONS (an unregistered <sds-icon name> renders nothing). Returns near-miss suggestions when invalid.',
107
+ inputSchema: {
108
+ name: z.string().describe('Icon name, e.g. "search", "calendar".'),
109
+ },
110
+ }, async ({ name }) => {
111
+ const valid = loaded.iconSet.has(name);
112
+ return json({ name, valid, suggestions: valid ? [] : suggestIcons(loaded, name) });
113
+ });
114
+ server.registerTool('sds_get_token', {
115
+ title: 'Get saitech-design-system design tokens',
116
+ description: 'Look up design tokens (colors, radii, status, layout) with their Tailwind utility and CSS variable. Omit args to list all.',
117
+ inputSchema: {
118
+ name: z.string().optional().describe('Exact or partial token name, e.g. "brand-600".'),
119
+ family: z
120
+ .enum(['brand', 'accent', 'ink', 'surface', 'radius', 'status', 'font', 'layout'])
121
+ .optional()
122
+ .describe('Filter by token family.'),
123
+ },
124
+ }, async ({ name, family }) => {
125
+ let tokens = index.tokens;
126
+ if (family)
127
+ tokens = tokens.filter((t) => t.family === family);
128
+ if (name) {
129
+ const n = name.toLowerCase();
130
+ tokens = tokens.filter((t) => t.token.toLowerCase().includes(n));
131
+ }
132
+ return json({ count: tokens.length, tokens });
133
+ });
134
+ server.registerTool('sds_get_setup', {
135
+ title: 'Get saitech-design-system install & Tailwind setup',
136
+ description: 'The install + Tailwind v4 setup checklist (registry .npmrc, install command, @import/@source/@theme, peer deps, standalone usage). The #1 source of "components render unstyled" issues.',
137
+ inputSchema: {
138
+ context: z.enum(['consumer', 'contributor']).optional().describe('Audience (default: consumer).'),
139
+ },
140
+ }, async ({ context }) => {
141
+ return json({
142
+ packageName: index.packageName,
143
+ libraryVersion: index.libraryVersion,
144
+ devImport: index.devImport,
145
+ context: context ?? 'consumer',
146
+ ...index.install,
147
+ });
148
+ });
149
+ // --- Resources ------------------------------------------------------------
150
+ server.registerResource('docs', 'sds://docs.json', {
151
+ title: 'Full saitech-design-system doc index',
152
+ description: 'The entire generated documentation index as JSON.',
153
+ mimeType: 'application/json',
154
+ }, async (uri) => ({
155
+ contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(index, null, 2) }],
156
+ }));
157
+ server.registerResource('meta', 'sds://meta', {
158
+ title: 'saitech-design-system doc index metadata',
159
+ description: 'Version + provenance of the doc index, so the agent can confirm which library version it is coding against.',
160
+ mimeType: 'application/json',
161
+ }, async (uri) => ({
162
+ contents: [
163
+ {
164
+ uri: uri.href,
165
+ mimeType: 'application/json',
166
+ text: JSON.stringify({
167
+ packageName: index.packageName,
168
+ libraryVersion: index.libraryVersion,
169
+ schemaVersion: index.schemaVersion,
170
+ generatedAt: index.generatedAt,
171
+ componentCount: index.componentCount,
172
+ }, null, 2),
173
+ },
174
+ ],
175
+ }));
176
+ server.registerResource('component', new ResourceTemplate('sds://component/{name}', {
177
+ list: async () => ({
178
+ resources: loaded.all.map((c) => ({
179
+ uri: `sds://component/${c.selector}`,
180
+ name: c.className,
181
+ mimeType: 'application/json',
182
+ })),
183
+ }),
184
+ }), {
185
+ title: 'saitech-design-system component record',
186
+ description: 'A single component doc record addressable by selector or class name.',
187
+ }, async (uri, { name }) => {
188
+ const c = resolveComponent(loaded, Array.isArray(name) ? name[0] : name);
189
+ if (!c) {
190
+ return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify({ error: `unknown component "${name}"` }) }] };
191
+ }
192
+ return {
193
+ contents: [
194
+ { uri: uri.href, mimeType: 'application/json', text: JSON.stringify({ ...c, importLine: importLine(loaded, c) }, null, 2) },
195
+ ],
196
+ };
197
+ });
198
+ server.registerResource('llms', 'sds://llms.txt', {
199
+ title: 'saitech-design-system llms.txt',
200
+ description: 'Compact llms.txt index of the whole library for tools that ingest it directly.',
201
+ mimeType: 'text/markdown',
202
+ }, async (uri) => ({
203
+ contents: [{ uri: uri.href, mimeType: 'text/markdown', text: renderLlmsTxt(index) }],
204
+ }));
205
+ server.registerResource('component-md', new ResourceTemplate('sds://components/{selector}.md', {
206
+ list: async () => ({
207
+ resources: loaded.all.map((c) => ({
208
+ uri: `sds://components/${markdownSlug(c.selector)}.md`,
209
+ name: `${c.className} (Markdown)`,
210
+ mimeType: 'text/markdown',
211
+ })),
212
+ }),
213
+ }), {
214
+ title: 'saitech-design-system component card (Markdown)',
215
+ description: 'A per-component Markdown reference card.',
216
+ }, async (uri, { selector }) => {
217
+ const sel = Array.isArray(selector) ? selector[0] : selector;
218
+ const c = resolveComponent(loaded, sel.replace(/\.md$/, ''));
219
+ const text = c ? renderComponentMarkdown(index, c) : `# Unknown component "${sel}"`;
220
+ return { contents: [{ uri: uri.href, mimeType: 'text/markdown', text }] };
221
+ });
222
+ // --- Prompts --------------------------------------------------------------
223
+ registerPrompts(server, loaded);
224
+ return server;
225
+ }
@@ -0,0 +1,162 @@
1
+ import { suggestIcons } from './load-index.js';
2
+ /** Attributes that are valid on any element and are not component inputs. */
3
+ const GLOBAL_ATTRS = new Set([
4
+ 'class', 'style', 'id', 'title', 'hidden', 'role', 'slot', 'tabindex', 'value',
5
+ 'ngclass', 'ngstyle', 'ngmodel', 'formcontrol', 'formcontrolname', 'formgroupname',
6
+ 'routerlink', 'routerlinkactive', 'routerlinkactiveoptions',
7
+ ]);
8
+ function parseAttrs(blob) {
9
+ const attrs = [];
10
+ let rest = blob;
11
+ const push = (raw, name, form, value) => {
12
+ attrs.push({ raw, name, form, value });
13
+ };
14
+ // [(x)]="..." two-way
15
+ rest = rest.replace(/\[\(([\w-]+)\)\]\s*=\s*"([^"]*)"/g, (_m, n, v) => {
16
+ push(`[(${n})]`, n, 'two-way', v);
17
+ return ' ';
18
+ });
19
+ // [x]="..." property
20
+ rest = rest.replace(/\[([\w.-]+)\]\s*=\s*"([^"]*)"/g, (_m, n, v) => {
21
+ push(`[${n}]`, n, 'prop', v);
22
+ return ' ';
23
+ });
24
+ // (x)="..." event
25
+ rest = rest.replace(/\(([\w-]+)\)\s*=\s*"([^"]*)"/g, (_m, n, v) => {
26
+ push(`(${n})`, n, 'event', v);
27
+ return ' ';
28
+ });
29
+ // x="..." plain
30
+ rest = rest.replace(/([@*#]?[\w-]+)\s*=\s*"([^"]*)"/g, (_m, n, v) => {
31
+ push(n, n, 'plain', v);
32
+ return ' ';
33
+ });
34
+ // bare boolean attributes left over
35
+ for (const m of rest.matchAll(/(?:^|\s)([a-zA-Z][\w-]*)(?=\s|$)/g)) {
36
+ push(m[1], m[1], 'bool', null);
37
+ }
38
+ return attrs;
39
+ }
40
+ /** String-literal members of an input's enum type, if it is one. */
41
+ function enumValues(component, inputType) {
42
+ // inline union: 'a' | 'b'
43
+ if (inputType.includes("'")) {
44
+ const vals = [...inputType.matchAll(/'([^']*)'/g)].map((m) => m[1]);
45
+ if (vals.length > 1)
46
+ return vals;
47
+ }
48
+ // named union exported alongside the component
49
+ const t = component.exportedTypes.find((e) => e.name === inputType.replace(/\s*\|\s*null$/, ''));
50
+ if (t && t.kind === 'union' && t.body.includes("'")) {
51
+ return [...t.body.matchAll(/'([^']*)'/g)].map((m) => m[1]);
52
+ }
53
+ return null;
54
+ }
55
+ function isIconInput(inputType) {
56
+ return inputType.replace(/\s/g, '').includes('SdsIconName');
57
+ }
58
+ function closest(name, candidates) {
59
+ const n = name.toLowerCase();
60
+ return candidates.find((c) => c.toLowerCase().includes(n) || n.includes(c.toLowerCase()));
61
+ }
62
+ export function validateUsage(loaded, code) {
63
+ const diagnostics = [];
64
+ const tagRe = /<(sds-[a-z0-9-]+)((?:\s+[^>]*?)?)\s*\/?>/g;
65
+ let m;
66
+ let sawTag = false;
67
+ while ((m = tagRe.exec(code)) !== null) {
68
+ sawTag = true;
69
+ const selector = m[1];
70
+ const blob = m[2] ?? '';
71
+ const component = loaded.bySelector.get(selector);
72
+ if (!component) {
73
+ diagnostics.push({
74
+ severity: 'error',
75
+ selector,
76
+ message: `Unknown selector <${selector}>.`,
77
+ suggestion: closest(selector.replace('sds-', ''), loaded.all.map((c) => c.selector)),
78
+ });
79
+ continue;
80
+ }
81
+ const inputByName = new Map(component.inputs.map((i) => [i.name.toLowerCase(), i]));
82
+ const outputNames = new Set(component.outputs.map((o) => o.name.toLowerCase()));
83
+ const attrs = parseAttrs(blob);
84
+ const present = new Set(attrs.map((a) => a.name.toLowerCase()));
85
+ for (const attr of attrs) {
86
+ const lname = attr.name.toLowerCase();
87
+ if (attr.form === 'event') {
88
+ // outputs / DOM events: only flag if it looks like an unknown component output
89
+ continue;
90
+ }
91
+ if (attr.name.startsWith('*') || attr.name.startsWith('#') || attr.name.startsWith('@'))
92
+ continue;
93
+ if (lname.startsWith('aria-') || lname.startsWith('data-'))
94
+ continue;
95
+ if (GLOBAL_ATTRS.has(lname)) {
96
+ // [value] bound on a CVA control is the classic mistake
97
+ if (lname === 'value' && component.cva && (attr.form === 'prop' || attr.form === 'two-way' || attr.form === 'plain')) {
98
+ diagnostics.push({
99
+ severity: 'warning',
100
+ selector,
101
+ input: 'value',
102
+ message: `<${selector}> is a ControlValueAccessor: bind its value with [(ngModel)] or formControlName, not [value].`,
103
+ });
104
+ }
105
+ continue;
106
+ }
107
+ const input = inputByName.get(lname);
108
+ if (!input) {
109
+ // not an input — maybe a model change-event already handled, otherwise unknown
110
+ if (!outputNames.has(lname)) {
111
+ diagnostics.push({
112
+ severity: 'warning',
113
+ selector,
114
+ input: attr.name,
115
+ message: `Unknown input "${attr.name}" on <${selector}>.`,
116
+ suggestion: closest(attr.name, component.inputs.map((i) => i.name)),
117
+ });
118
+ }
119
+ continue;
120
+ }
121
+ // enum validation for plain string values
122
+ if (attr.form === 'plain' && attr.value !== null) {
123
+ const values = enumValues(component, input.type);
124
+ if (values && !values.includes(attr.value)) {
125
+ diagnostics.push({
126
+ severity: 'error',
127
+ selector,
128
+ input: input.name,
129
+ message: `Invalid value "${attr.value}" for ${input.name} on <${selector}>. Allowed: ${values.map((v) => `'${v}'`).join(', ')}.`,
130
+ });
131
+ }
132
+ if (isIconInput(input.type) && !loaded.iconSet.has(attr.value)) {
133
+ diagnostics.push({
134
+ severity: 'error',
135
+ selector,
136
+ input: input.name,
137
+ message: `Icon "${attr.value}" is not registered (SDS_ICONS).`,
138
+ suggestion: suggestIcons(loaded, attr.value, 3).join(', '),
139
+ });
140
+ }
141
+ }
142
+ }
143
+ // missing required inputs
144
+ for (const input of component.inputs) {
145
+ if (input.required && !present.has(input.name.toLowerCase())) {
146
+ diagnostics.push({
147
+ severity: 'error',
148
+ selector,
149
+ input: input.name,
150
+ message: `Missing required input "${input.name}" on <${selector}>.`,
151
+ });
152
+ }
153
+ }
154
+ }
155
+ if (!sawTag) {
156
+ diagnostics.push({
157
+ severity: 'warning',
158
+ message: 'No <sds-*> elements found in the snippet — nothing to validate.',
159
+ });
160
+ }
161
+ return { ok: !diagnostics.some((d) => d.severity === 'error'), diagnostics };
162
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@besaitech/ng-design-system-mcp",
3
+ "version": "0.0.5",
4
+ "description": "MCP server that exposes the @besaitech/ng-design-system Angular component library documentation (API, examples, tokens, icons, setup) to AI coding tools.",
5
+ "type": "module",
6
+ "bin": {
7
+ "saitech-design-system-mcp": "dist/index.js"
8
+ },
9
+ "main": "dist/index.js",
10
+ "files": [
11
+ "dist",
12
+ "data/docs.json",
13
+ "README.md"
14
+ ],
15
+ "engines": {
16
+ "node": ">=20"
17
+ },
18
+ "scripts": {
19
+ "build": "tsc -p tsconfig.json"
20
+ },
21
+ "keywords": [
22
+ "mcp",
23
+ "model-context-protocol",
24
+ "angular",
25
+ "design-system",
26
+ "saitech-design-system"
27
+ ],
28
+ "license": "UNLICENSED",
29
+ "dependencies": {
30
+ "@modelcontextprotocol/sdk": "^1.26.0",
31
+ "zod": "^4.0.0"
32
+ },
33
+ "publishConfig": {
34
+ "registry": "https://registry.npmjs.org/",
35
+ "access": "public"
36
+ }
37
+ }