@fundamental-ngx/mcp 0.62.0-rc.67

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 (33) hide show
  1. package/README.md +109 -0
  2. package/package.json +21 -0
  3. package/src/data/components.json +61370 -0
  4. package/src/extractors/build-metadata.d.ts +8 -0
  5. package/src/extractors/build-metadata.js +178 -0
  6. package/src/extractors/build-metadata.js.map +1 -0
  7. package/src/extractors/cem-extractor.d.ts +17 -0
  8. package/src/extractors/cem-extractor.js +430 -0
  9. package/src/extractors/cem-extractor.js.map +1 -0
  10. package/src/extractors/changelog-extractor.d.ts +6 -0
  11. package/src/extractors/changelog-extractor.js +115 -0
  12. package/src/extractors/changelog-extractor.js.map +1 -0
  13. package/src/extractors/description-extractor.d.ts +11 -0
  14. package/src/extractors/description-extractor.js +58 -0
  15. package/src/extractors/description-extractor.js.map +1 -0
  16. package/src/extractors/example-extractor.d.ts +19 -0
  17. package/src/extractors/example-extractor.js +67 -0
  18. package/src/extractors/example-extractor.js.map +1 -0
  19. package/src/extractors/token-extractor.d.ts +6 -0
  20. package/src/extractors/token-extractor.js +345 -0
  21. package/src/extractors/token-extractor.js.map +1 -0
  22. package/src/extractors/typedoc-extractor.d.ts +16 -0
  23. package/src/extractors/typedoc-extractor.js +576 -0
  24. package/src/extractors/typedoc-extractor.js.map +1 -0
  25. package/src/index.d.ts +2 -0
  26. package/src/index.js +3 -0
  27. package/src/index.js.map +1 -0
  28. package/src/server.d.ts +1 -0
  29. package/src/server.js +794 -0
  30. package/src/server.js.map +1 -0
  31. package/src/types/component-metadata.d.ts +177 -0
  32. package/src/types/component-metadata.js +21 -0
  33. package/src/types/component-metadata.js.map +1 -0
package/src/server.js ADDED
@@ -0,0 +1,794 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { readFileSync } from 'fs';
4
+ import { resolve } from 'path';
5
+ import { z } from 'zod';
6
+ import { extractChangelogs } from './extractors/changelog-extractor';
7
+ import { extractDesignTokens } from './extractors/token-extractor';
8
+ import { LIBRARY_ALIAS_MAP } from './types/component-metadata';
9
+ // Load component catalog from pre-built JSON
10
+ let catalog;
11
+ try {
12
+ const dataPath = resolve(__dirname, 'data', 'components.json');
13
+ catalog = JSON.parse(readFileSync(dataPath, 'utf-8'));
14
+ }
15
+ catch {
16
+ catalog = { generatedAt: new Date().toISOString(), version: 'unknown', components: [] };
17
+ console.error('Warning: components.json not found. Run `nx run mcp-server:extract-metadata` first.');
18
+ }
19
+ // Design tokens and changelog loaded async at startup
20
+ let designTokens = [];
21
+ let changelogEntries = [];
22
+ const server = new McpServer({
23
+ name: 'fundamental-ngx',
24
+ version: catalog.version
25
+ });
26
+ // ---------------------------------------------------------------------------
27
+ // Tool: list_components
28
+ // ---------------------------------------------------------------------------
29
+ server.tool('list_components', `List all Fundamental NGX components. Returns name, selector, library, and description.
30
+ Optionally filter by library (core, platform, btp, cx, cdk, ui5, ui5-fiori, ui5-ai)
31
+ or category (Action, Form, Layout, Display, Navigation, etc.).
32
+ Use this to discover what components are available.`, {
33
+ library: z
34
+ .enum(['core', 'platform', 'btp', 'cx', 'cdk', 'i18n', 'ui5', 'ui5-fiori', 'ui5-ai'])
35
+ .optional()
36
+ .describe('Filter by library'),
37
+ category: z.string().optional().describe('Filter by category')
38
+ }, async ({ library, category }) => {
39
+ let components = catalog.components;
40
+ if (library) {
41
+ const fullLibrary = LIBRARY_ALIAS_MAP[library];
42
+ if (fullLibrary) {
43
+ components = components.filter((c) => c.library === fullLibrary);
44
+ }
45
+ }
46
+ if (category) {
47
+ const lowerCategory = category.toLowerCase();
48
+ components = components.filter((c) => c.category.toLowerCase().includes(lowerCategory));
49
+ }
50
+ const summary = components.map((c) => ({
51
+ name: c.name,
52
+ selector: c.selector,
53
+ library: c.library,
54
+ description: truncate(c.description, 120)
55
+ }));
56
+ return {
57
+ content: [
58
+ {
59
+ type: 'text',
60
+ text: JSON.stringify({ count: summary.length, components: summary }, null, 2)
61
+ }
62
+ ]
63
+ };
64
+ });
65
+ // ---------------------------------------------------------------------------
66
+ // Tool: search_components
67
+ // ---------------------------------------------------------------------------
68
+ server.tool('search_components', `Search Fundamental NGX components by keyword. Searches across component names,
69
+ selectors, descriptions, and input/output property names.
70
+ Use this when you need to find a component by a partial name or feature keyword.`, {
71
+ query: z.string().describe('Search keyword (e.g., "button", "table", "date", "navigation")'),
72
+ library: z
73
+ .enum(['core', 'platform', 'btp', 'cx', 'cdk', 'ui5', 'ui5-fiori', 'ui5-ai'])
74
+ .optional()
75
+ .describe('Restrict search to a specific library')
76
+ }, async ({ query, library }) => {
77
+ const lowerQuery = query.toLowerCase();
78
+ let components = catalog.components;
79
+ if (library) {
80
+ const fullLibrary = LIBRARY_ALIAS_MAP[library];
81
+ if (fullLibrary) {
82
+ components = components.filter((c) => c.library === fullLibrary);
83
+ }
84
+ }
85
+ const scored = components
86
+ .map((c) => ({ component: c, score: scoreMatch(c, lowerQuery) }))
87
+ .filter((s) => s.score > 0)
88
+ .sort((a, b) => b.score - a.score)
89
+ .slice(0, 20);
90
+ const results = scored.map((s) => ({
91
+ name: s.component.name,
92
+ selector: s.component.selector,
93
+ library: s.component.library,
94
+ category: s.component.category,
95
+ description: truncate(s.component.description, 150),
96
+ relevance: s.score
97
+ }));
98
+ return {
99
+ content: [
100
+ {
101
+ type: 'text',
102
+ text: JSON.stringify({ query, count: results.length, results }, null, 2)
103
+ }
104
+ ]
105
+ };
106
+ });
107
+ // ---------------------------------------------------------------------------
108
+ // Tool: get_component_api
109
+ // ---------------------------------------------------------------------------
110
+ server.tool('get_component_api', `Get full API details for a specific Fundamental NGX component.
111
+ Accepts component name (e.g., "ButtonComponent"), selector (e.g., "fd-button", "ui5-button"),
112
+ or partial match. Returns all inputs, outputs, slots, methods, and enum values.
113
+ Use this when you need to know how to use a specific component.`, {
114
+ name: z.string().describe('Component name, selector, or search term')
115
+ }, async ({ name }) => {
116
+ const component = findComponent(name);
117
+ if (!component) {
118
+ return {
119
+ content: [
120
+ {
121
+ type: 'text',
122
+ text: `Component "${name}" not found. Use search_components to find available components.`
123
+ }
124
+ ]
125
+ };
126
+ }
127
+ const result = { ...component };
128
+ if (component.deprecated) {
129
+ result.deprecationWarning = `This component is deprecated: ${component.deprecated}`;
130
+ }
131
+ return {
132
+ content: [
133
+ {
134
+ type: 'text',
135
+ text: JSON.stringify(result, null, 2)
136
+ }
137
+ ]
138
+ };
139
+ });
140
+ // ---------------------------------------------------------------------------
141
+ // Tool: get_component_examples
142
+ // ---------------------------------------------------------------------------
143
+ server.tool('get_component_examples', `Get working code examples for a Fundamental NGX component.
144
+ Returns TypeScript and HTML snippets from the documentation examples.
145
+ Use this when you need real usage patterns for a component.`, {
146
+ name: z.string().describe('Component name or selector')
147
+ }, async ({ name }) => {
148
+ const component = findComponent(name);
149
+ if (!component) {
150
+ return {
151
+ content: [
152
+ {
153
+ type: 'text',
154
+ text: `Component "${name}" not found. Use search_components to find available components.`
155
+ }
156
+ ]
157
+ };
158
+ }
159
+ if (!component.examples || component.examples.length === 0) {
160
+ return {
161
+ content: [
162
+ {
163
+ type: 'text',
164
+ text: JSON.stringify({
165
+ component: component.name,
166
+ selector: component.selector,
167
+ docsUrl: component.docsUrl || 'https://sap.github.io/fundamental-ngx',
168
+ note: 'No examples found for this component. Check the docs site for usage guidance.'
169
+ }, null, 2)
170
+ }
171
+ ]
172
+ };
173
+ }
174
+ const formatted = component.examples.map((ex) => {
175
+ let code = `// --- ${ex.description} ---\n\n`;
176
+ code += ex.typescript;
177
+ if (ex.html) {
178
+ code += `\n\n<!-- Template: ${ex.name}.component.html -->\n\n${ex.html}`;
179
+ }
180
+ return { name: ex.description, code };
181
+ });
182
+ return {
183
+ content: [
184
+ {
185
+ type: 'text',
186
+ text: JSON.stringify({
187
+ component: component.name,
188
+ selector: component.selector,
189
+ exampleCount: formatted.length,
190
+ examples: formatted
191
+ }, null, 2)
192
+ }
193
+ ]
194
+ };
195
+ });
196
+ // ---------------------------------------------------------------------------
197
+ // Tool: recommend_components
198
+ // ---------------------------------------------------------------------------
199
+ server.tool('recommend_components', `Given a UI description or use case, recommend which Fundamental NGX components to use.
200
+ Describe what you want to build (e.g., "a filterable data table", "a login form",
201
+ "a master-detail layout") and get a list of recommended components with their selectors,
202
+ libraries, and how they compose together.`, {
203
+ description: z.string().describe('What you want to build (e.g., "a form with date picker and validation")')
204
+ }, async ({ description }) => {
205
+ const lowerDesc = description.toLowerCase();
206
+ const recommendations = [];
207
+ // Pattern-based recommendations
208
+ for (const [pattern, componentSelectors] of Object.entries(UI_PATTERNS)) {
209
+ const keywords = pattern.split('|');
210
+ if (keywords.some((kw) => lowerDesc.includes(kw))) {
211
+ for (const sel of componentSelectors) {
212
+ const comp = catalog.components.find((c) => c.selector === sel);
213
+ if (comp && !recommendations.some((r) => r.selector === sel)) {
214
+ recommendations.push({
215
+ component: comp.name,
216
+ selector: comp.selector,
217
+ library: comp.library,
218
+ reason: `Matches "${pattern.split('|')[0]}" pattern`
219
+ });
220
+ }
221
+ }
222
+ }
223
+ }
224
+ // Also do a keyword search for anything not caught by patterns
225
+ if (recommendations.length < 3) {
226
+ const words = lowerDesc.split(/\s+/).filter((w) => w.length > 3);
227
+ for (const word of words) {
228
+ const matches = catalog.components
229
+ .filter((c) => c.selector.includes(word) ||
230
+ c.name.toLowerCase().includes(word) ||
231
+ c.category.toLowerCase().includes(word))
232
+ .slice(0, 3);
233
+ for (const comp of matches) {
234
+ if (!recommendations.some((r) => r.selector === comp.selector)) {
235
+ recommendations.push({
236
+ component: comp.name,
237
+ selector: comp.selector,
238
+ library: comp.library,
239
+ reason: `Name/category matches "${word}"`
240
+ });
241
+ }
242
+ }
243
+ }
244
+ }
245
+ return {
246
+ content: [
247
+ {
248
+ type: 'text',
249
+ text: JSON.stringify({
250
+ description,
251
+ recommendations: recommendations.slice(0, 15),
252
+ note: 'Use get_component_api for full details on each component.'
253
+ }, null, 2)
254
+ }
255
+ ]
256
+ };
257
+ });
258
+ // ---------------------------------------------------------------------------
259
+ // Tool: get_migration_guide
260
+ // ---------------------------------------------------------------------------
261
+ server.tool('get_migration_guide', `Get migration guidance for upgrading Fundamental NGX.
262
+ Returns breaking changes, deprecated APIs, and migration steps.
263
+ Use this when helping users upgrade between versions.`, {
264
+ component: z.string().optional().describe('Specific component to get migration info for'),
265
+ from_version: z.string().optional().describe('Version migrating from (e.g., "0.58.0")'),
266
+ to_version: z.string().optional().describe('Version migrating to (defaults to latest)')
267
+ }, async ({ component, from_version, to_version }) => {
268
+ let entries = changelogEntries;
269
+ if (component) {
270
+ const lowerComp = component.toLowerCase();
271
+ entries = entries.filter((e) => e.component?.toLowerCase().includes(lowerComp) || e.description.toLowerCase().includes(lowerComp));
272
+ }
273
+ if (from_version) {
274
+ entries = entries.filter((e) => compareVersions(e.version, from_version) >= 0);
275
+ }
276
+ if (to_version) {
277
+ entries = entries.filter((e) => compareVersions(e.version, to_version) <= 0);
278
+ }
279
+ if (entries.length === 0) {
280
+ return {
281
+ content: [
282
+ {
283
+ type: 'text',
284
+ text: JSON.stringify({
285
+ filters: { component, from_version, to_version },
286
+ matches: 0,
287
+ note: 'No changelog entries found for the given filters. Try broader version ranges or omit the component filter.'
288
+ }, null, 2)
289
+ }
290
+ ]
291
+ };
292
+ }
293
+ // Deduplicate: the same commit appears in multiple library CHANGELOGs
294
+ // (fixed-version monorepo). Keep one entry per unique description+type+version.
295
+ const seen = new Set();
296
+ entries = entries.filter((e) => {
297
+ const key = `${e.version}|${e.type}|${e.description}`;
298
+ if (seen.has(key)) {
299
+ return false;
300
+ }
301
+ seen.add(key);
302
+ return true;
303
+ });
304
+ // Collapse RC entries into their stable release. Stable releases (e.g. 0.61.0)
305
+ // aggregate all changes from their RCs (e.g. 0.61.0-rc.2), producing duplicates.
306
+ // If the same type+description exists in both RC and stable, keep only the stable entry.
307
+ const stableKeys = new Set();
308
+ for (const e of entries) {
309
+ if (!e.version.includes('-')) {
310
+ stableKeys.add(`${baseVersion(e.version)}|${e.type}|${e.description}`);
311
+ }
312
+ }
313
+ entries = entries.filter((e) => {
314
+ if (!e.version.includes('-')) {
315
+ return true;
316
+ } // keep all stable entries
317
+ const key = `${baseVersion(e.version)}|${e.type}|${e.description}`;
318
+ return !stableKeys.has(key); // drop RC entry if stable has same change
319
+ });
320
+ // Group by version
321
+ const byVersion = {};
322
+ for (const entry of entries) {
323
+ (byVersion[entry.version] ??= []).push(entry);
324
+ }
325
+ // Sort versions descending, limit output
326
+ const sortedVersions = Object.keys(byVersion).sort((a, b) => compareVersions(b, a));
327
+ const limitedVersions = sortedVersions.slice(0, 20);
328
+ const result = limitedVersions.map((version) => ({
329
+ version,
330
+ changes: byVersion[version].map((e) => ({
331
+ type: e.type,
332
+ library: e.library,
333
+ component: e.component,
334
+ description: e.description
335
+ }))
336
+ }));
337
+ return {
338
+ content: [
339
+ {
340
+ type: 'text',
341
+ text: JSON.stringify({
342
+ filters: { component, from_version, to_version },
343
+ totalEntries: entries.length,
344
+ versions: result
345
+ }, null, 2)
346
+ }
347
+ ]
348
+ };
349
+ });
350
+ // ---------------------------------------------------------------------------
351
+ // Tool: get_design_tokens
352
+ // ---------------------------------------------------------------------------
353
+ server.tool('get_design_tokens', `Look up SAP design tokens (CSS custom properties) and utility classes.
354
+ Search for colors, spacing (margins/paddings), typography, elevation, and border tokens.
355
+ Returns CSS variable names, descriptions, and usage examples.
356
+ Use this when styling Fundamental NGX components or building custom layouts.`, {
357
+ query: z.string().describe('What to look for (e.g., "background color", "margin", "font size")'),
358
+ category: z
359
+ .enum(['color', 'spacing', 'typography', 'elevation', 'border', 'size'])
360
+ .optional()
361
+ .describe('Filter by token category')
362
+ }, async ({ query, category }) => {
363
+ const lowerQuery = query.toLowerCase();
364
+ let tokens = designTokens;
365
+ if (category) {
366
+ tokens = tokens.filter((t) => t.category === category);
367
+ }
368
+ const matches = tokens
369
+ .filter((t) => t.name.toLowerCase().includes(lowerQuery) || t.description.toLowerCase().includes(lowerQuery))
370
+ .slice(0, 50);
371
+ if (matches.length === 0) {
372
+ return {
373
+ content: [
374
+ {
375
+ type: 'text',
376
+ text: JSON.stringify({
377
+ query,
378
+ category,
379
+ matches: 0,
380
+ note: 'No tokens found. Try broader terms like "color", "spacing", "font", or "margin".'
381
+ }, null, 2)
382
+ }
383
+ ]
384
+ };
385
+ }
386
+ return {
387
+ content: [
388
+ {
389
+ type: 'text',
390
+ text: JSON.stringify({
391
+ query,
392
+ category,
393
+ matches: matches.length,
394
+ tokens: matches
395
+ }, null, 2)
396
+ }
397
+ ]
398
+ };
399
+ });
400
+ // ---------------------------------------------------------------------------
401
+ // Tool: get_accessibility_guide
402
+ // ---------------------------------------------------------------------------
403
+ server.tool('get_accessibility_guide', `Get accessibility guidance for a Fundamental NGX component.
404
+ Returns ARIA-related inputs (ariaLabel, ariaDescribedBy, role, etc.),
405
+ keyboard handling notes, and accessibility code examples when available.
406
+ Use this when building accessible UIs or auditing existing components.`, {
407
+ name: z.string().describe('Component name or selector (e.g., "fd-button", "ui5-dialog")')
408
+ }, async ({ name }) => {
409
+ const component = findComponent(name);
410
+ if (!component) {
411
+ return {
412
+ content: [
413
+ {
414
+ type: 'text',
415
+ text: `Component "${name}" not found. Use search_components to find available components.`
416
+ }
417
+ ]
418
+ };
419
+ }
420
+ // Extract ARIA-related inputs
421
+ const ariaInputs = component.inputs.filter((i) => i.name.toLowerCase().startsWith('aria') ||
422
+ i.name.toLowerCase() === 'role' ||
423
+ i.name.toLowerCase().startsWith('accessible'));
424
+ // Extract a11y examples
425
+ const a11yExamples = (component.examples ?? []).filter((ex) => ex.name.toLowerCase().includes('a11y') ||
426
+ ex.name.toLowerCase().includes('accessibility') ||
427
+ ex.name.toLowerCase().includes('accessible'));
428
+ // Build general guidance based on component features
429
+ const tips = [];
430
+ if (ariaInputs.length > 0) {
431
+ const labelInput = ariaInputs.find((i) => i.name === 'ariaLabel' || i.name === 'accessibleName');
432
+ if (labelInput) {
433
+ tips.push(`Provide a descriptive "${labelInput.name}" for screen readers, especially when the component has no visible text label.`);
434
+ }
435
+ const describedByInput = ariaInputs.find((i) => i.name === 'ariaDescribedBy' || i.name === 'accessibleNameRef');
436
+ if (describedByInput) {
437
+ tips.push(`Use "${describedByInput.name}" to reference IDs of elements that describe this component.`);
438
+ }
439
+ const roleInput = ariaInputs.find((i) => i.name === 'role' || i.name === 'accessibleRole');
440
+ if (roleInput) {
441
+ tips.push(`The "${roleInput.name}" input overrides the default ARIA role. Only change it when the component is used in a non-standard context.`);
442
+ }
443
+ }
444
+ else {
445
+ tips.push('This component has no explicit ARIA inputs. It likely handles accessibility internally or relies on native HTML semantics.');
446
+ }
447
+ if (component.keyboardHandling) {
448
+ tips.push('This component has documented keyboard interactions — see keyboardHandling below.');
449
+ }
450
+ if (component.slots && component.slots.length > 0) {
451
+ tips.push('When projecting content via slots, ensure projected elements have appropriate ARIA attributes.');
452
+ }
453
+ const result = {
454
+ component: component.name,
455
+ selector: component.selector,
456
+ library: component.library,
457
+ ariaInputs: ariaInputs.map((i) => ({
458
+ name: i.name,
459
+ type: i.type,
460
+ description: i.description,
461
+ defaultValue: i.defaultValue
462
+ })),
463
+ keyboardHandling: component.keyboardHandling || null,
464
+ a11yExamples: a11yExamples.map((ex) => ({
465
+ name: ex.name,
466
+ description: ex.description,
467
+ typescript: ex.typescript,
468
+ html: ex.html
469
+ })),
470
+ tips
471
+ };
472
+ return {
473
+ content: [
474
+ {
475
+ type: 'text',
476
+ text: JSON.stringify(result, null, 2)
477
+ }
478
+ ]
479
+ };
480
+ });
481
+ // ---------------------------------------------------------------------------
482
+ // Tool: compare_components
483
+ // ---------------------------------------------------------------------------
484
+ server.tool('compare_components', `Compare two Fundamental NGX components side by side.
485
+ Returns shared and unique inputs, outputs, slots, and methods.
486
+ Useful for choosing between core (fd-) and UI5 (ui5-) variants,
487
+ or comparing alternative components for the same use case.`, {
488
+ component_a: z.string().describe('First component name or selector (e.g., "fd-button")'),
489
+ component_b: z.string().describe('Second component name or selector (e.g., "ui5-button")')
490
+ }, async ({ component_a, component_b }) => {
491
+ const compA = findComponent(component_a);
492
+ const compB = findComponent(component_b);
493
+ if (!compA || !compB) {
494
+ const missing = [...(!compA ? [component_a] : []), ...(!compB ? [component_b] : [])];
495
+ return {
496
+ content: [
497
+ {
498
+ type: 'text',
499
+ text: `Component(s) not found: ${missing.map((m) => `"${m}"`).join(', ')}. Use search_components to find available components.`
500
+ }
501
+ ]
502
+ };
503
+ }
504
+ // Compare inputs by name
505
+ const inputNamesA = new Set(compA.inputs.map((i) => i.name));
506
+ const inputNamesB = new Set(compB.inputs.map((i) => i.name));
507
+ const sharedInputs = compA.inputs
508
+ .filter((i) => inputNamesB.has(i.name))
509
+ .map((i) => {
510
+ const bInput = compB.inputs.find((bi) => bi.name === i.name);
511
+ return {
512
+ name: i.name,
513
+ typeA: i.type,
514
+ typeB: bInput.type,
515
+ defaultA: i.defaultValue,
516
+ defaultB: bInput.defaultValue
517
+ };
518
+ });
519
+ const onlyInA_inputs = compA.inputs
520
+ .filter((i) => !inputNamesB.has(i.name))
521
+ .map((i) => ({ name: i.name, type: i.type, defaultValue: i.defaultValue }));
522
+ const onlyInB_inputs = compB.inputs
523
+ .filter((i) => !inputNamesA.has(i.name))
524
+ .map((i) => ({ name: i.name, type: i.type, defaultValue: i.defaultValue }));
525
+ // Compare outputs by name
526
+ const outputNamesA = new Set(compA.outputs.map((o) => o.name));
527
+ const outputNamesB = new Set(compB.outputs.map((o) => o.name));
528
+ const sharedOutputs = compA.outputs
529
+ .filter((o) => outputNamesB.has(o.name))
530
+ .map((o) => {
531
+ const bOutput = compB.outputs.find((bo) => bo.name === o.name);
532
+ return { name: o.name, typeA: o.type, typeB: bOutput.type };
533
+ });
534
+ const onlyInA_outputs = compA.outputs
535
+ .filter((o) => !outputNamesB.has(o.name))
536
+ .map((o) => ({ name: o.name, type: o.type }));
537
+ const onlyInB_outputs = compB.outputs
538
+ .filter((o) => !outputNamesA.has(o.name))
539
+ .map((o) => ({ name: o.name, type: o.type }));
540
+ // Build summary
541
+ const summaryParts = [];
542
+ summaryParts.push(`${compA.selector} is from ${compA.library}; ${compB.selector} is from ${compB.library}.`);
543
+ summaryParts.push(`Inputs: ${compA.inputs.length} vs ${compB.inputs.length} (${sharedInputs.length} shared).`);
544
+ summaryParts.push(`Outputs: ${compA.outputs.length} vs ${compB.outputs.length} (${sharedOutputs.length} shared).`);
545
+ if (compA.slots.length > 0 || compB.slots.length > 0) {
546
+ const slotsA = compA.slots.map((s) => s.name).join(', ') || 'none';
547
+ const slotsB = compB.slots.map((s) => s.name).join(', ') || 'none';
548
+ summaryParts.push(`Slots: ${compA.selector} [${slotsA}], ${compB.selector} [${slotsB}].`);
549
+ }
550
+ if (compA.deprecated) {
551
+ summaryParts.push(`${compA.selector} is deprecated: ${compA.deprecated}`);
552
+ }
553
+ if (compB.deprecated) {
554
+ summaryParts.push(`${compB.selector} is deprecated: ${compB.deprecated}`);
555
+ }
556
+ // Find alternatives from UI_PATTERNS
557
+ const alternatives = findAlternatives(compA.selector, compB.selector);
558
+ const result = {
559
+ componentA: {
560
+ name: compA.name,
561
+ selector: compA.selector,
562
+ library: compA.library,
563
+ category: compA.category,
564
+ description: truncate(compA.description, 200)
565
+ },
566
+ componentB: {
567
+ name: compB.name,
568
+ selector: compB.selector,
569
+ library: compB.library,
570
+ category: compB.category,
571
+ description: truncate(compB.description, 200)
572
+ },
573
+ comparison: {
574
+ sharedInputs,
575
+ onlyInA: onlyInA_inputs,
576
+ onlyInB: onlyInB_inputs,
577
+ sharedOutputs,
578
+ onlyOutputsInA: onlyInA_outputs,
579
+ onlyOutputsInB: onlyInB_outputs,
580
+ slotsA: compA.slots.map((s) => ({ name: s.name, description: truncate(s.description, 80) })),
581
+ slotsB: compB.slots.map((s) => ({ name: s.name, description: truncate(s.description, 80) })),
582
+ methodsA: compA.methods.map((m) => m.name),
583
+ methodsB: compB.methods.map((m) => m.name),
584
+ summary: summaryParts.join(' ')
585
+ },
586
+ alternatives
587
+ };
588
+ return {
589
+ content: [
590
+ {
591
+ type: 'text',
592
+ text: JSON.stringify(result, null, 2)
593
+ }
594
+ ]
595
+ };
596
+ });
597
+ // ---------------------------------------------------------------------------
598
+ // Resource: component catalog
599
+ // ---------------------------------------------------------------------------
600
+ server.resource('component-catalog', 'fundamental-ngx://components/catalog', async (uri) => ({
601
+ contents: [
602
+ {
603
+ uri: uri.href,
604
+ mimeType: 'application/json',
605
+ text: JSON.stringify(catalog, null, 2)
606
+ }
607
+ ]
608
+ }));
609
+ // ---------------------------------------------------------------------------
610
+ // Helpers
611
+ // ---------------------------------------------------------------------------
612
+ function findComponent(nameOrSelector) {
613
+ const lower = nameOrSelector.toLowerCase();
614
+ // Exact match on selector
615
+ const bySelector = catalog.components.find((c) => c.selector.toLowerCase() === lower);
616
+ if (bySelector) {
617
+ return bySelector;
618
+ }
619
+ // Exact match on name
620
+ const byName = catalog.components.find((c) => c.name.toLowerCase() === lower);
621
+ if (byName) {
622
+ return byName;
623
+ }
624
+ // Partial match on selector
625
+ const byPartialSelector = catalog.components.find((c) => c.selector.toLowerCase().includes(lower));
626
+ if (byPartialSelector) {
627
+ return byPartialSelector;
628
+ }
629
+ // Partial match on name
630
+ const byPartialName = catalog.components.find((c) => c.name.toLowerCase().includes(lower));
631
+ if (byPartialName) {
632
+ return byPartialName;
633
+ }
634
+ return undefined;
635
+ }
636
+ function scoreMatch(component, query) {
637
+ let score = 0;
638
+ if (component.selector.toLowerCase() === query) {
639
+ score += 100;
640
+ }
641
+ else if (component.selector.toLowerCase().includes(query)) {
642
+ score += 50;
643
+ }
644
+ if (component.name.toLowerCase() === query) {
645
+ score += 90;
646
+ }
647
+ else if (component.name.toLowerCase().includes(query)) {
648
+ score += 40;
649
+ }
650
+ if (component.description.toLowerCase().includes(query)) {
651
+ score += 20;
652
+ }
653
+ if (component.category.toLowerCase().includes(query)) {
654
+ score += 30;
655
+ }
656
+ // Search in input/output names
657
+ for (const input of component.inputs) {
658
+ if (input.name.toLowerCase().includes(query)) {
659
+ score += 15;
660
+ break;
661
+ }
662
+ }
663
+ for (const output of component.outputs) {
664
+ if (output.name.toLowerCase().includes(query)) {
665
+ score += 15;
666
+ break;
667
+ }
668
+ }
669
+ return score;
670
+ }
671
+ function truncate(text, maxLength) {
672
+ if (!text) {
673
+ return '';
674
+ }
675
+ // Take only the first line/paragraph for summaries
676
+ const firstLine = text.split('\n')[0];
677
+ if (firstLine.length <= maxLength) {
678
+ return firstLine;
679
+ }
680
+ return firstLine.slice(0, maxLength - 3) + '...';
681
+ }
682
+ /** Find alternative components from UI_PATTERNS that share a category with either selector. */
683
+ function findAlternatives(selectorA, selectorB) {
684
+ const alternatives = new Set();
685
+ for (const componentSelectors of Object.values(UI_PATTERNS)) {
686
+ const hasA = componentSelectors.includes(selectorA);
687
+ const hasB = componentSelectors.includes(selectorB);
688
+ if (hasA || hasB) {
689
+ for (const sel of componentSelectors) {
690
+ if (sel !== selectorA && sel !== selectorB) {
691
+ alternatives.add(sel);
692
+ }
693
+ }
694
+ }
695
+ }
696
+ return [...alternatives];
697
+ }
698
+ /** Simple version comparison: "0.60.0" vs "0.61.0". Returns negative/0/positive. */
699
+ function compareVersions(a, b) {
700
+ const [aBase, aPre] = a.split('-');
701
+ const [bBase, bPre] = b.split('-');
702
+ const pa = aBase.split('.').map(Number);
703
+ const pb = bBase.split('.').map(Number);
704
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
705
+ const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
706
+ if (diff !== 0) {
707
+ return diff;
708
+ }
709
+ }
710
+ // Same base version: stable (no pre-release) > RC
711
+ if (!aPre && bPre) {
712
+ return 1;
713
+ }
714
+ if (aPre && !bPre) {
715
+ return -1;
716
+ }
717
+ if (aPre && bPre) {
718
+ // Compare RC numbers: rc.2 vs rc.10
719
+ const aRc = parseInt(aPre.replace(/\D+/g, ''), 10) || 0;
720
+ const bRc = parseInt(bPre.replace(/\D+/g, ''), 10) || 0;
721
+ return aRc - bRc;
722
+ }
723
+ return 0;
724
+ }
725
+ /**
726
+ * Get the base version from a version string (strips -rc.N suffix).
727
+ */
728
+ function baseVersion(version) {
729
+ return version.replace(/-.*$/, '');
730
+ }
731
+ // ---------------------------------------------------------------------------
732
+ // UI Pattern recommendations
733
+ // ---------------------------------------------------------------------------
734
+ const UI_PATTERNS = {
735
+ 'table|data table|grid': ['fd-table', 'fdp-table', 'ui5-table'],
736
+ 'form|input form': ['fd-form', 'fd-input-group', 'fd-checkbox', 'fd-radio', 'fd-select', 'fd-switch'],
737
+ 'dialog|modal|popup': ['fd-dialog', 'ui5-dialog', 'fd-message-box'],
738
+ 'date|calendar|date picker': ['fd-date-picker', 'fd-calendar', 'ui5-date-picker', 'ui5-calendar'],
739
+ 'navigation|nav|sidebar': ['fd-side-navigation', 'fd-vertical-navigation', 'ui5-side-navigation'],
740
+ 'button|action': ['fd-button', 'ui5-button', 'fd-split-button', 'fd-segmented-button'],
741
+ 'list|items': ['fd-list', 'ui5-list', 'fd-grid-list'],
742
+ 'menu|dropdown': ['fd-menu', 'fd-popover', 'ui5-menu'],
743
+ 'tabs|tab': ['fd-tabs', 'ui5-tab-container'],
744
+ 'wizard|stepper|step': ['fd-wizard', 'ui5-wizard'],
745
+ 'card|tile': ['fd-card', 'fd-tile', 'ui5-card'],
746
+ 'tree|hierarchy': ['fd-tree', 'ui5-tree'],
747
+ 'upload|file': ['fd-file-uploader', 'ui5-file-uploader', 'fd-upload-collection'],
748
+ 'search|filter': ['fdb-search-field', 'fdp-search-field', 'fdp-smart-filter-bar'],
749
+ 'toolbar|header bar': ['fd-toolbar', 'fd-bar', 'ui5-toolbar'],
750
+ 'notification|alert|message': ['fd-message-strip', 'fd-message-toast', 'fd-notification', 'ui5-notification-list'],
751
+ 'avatar|user|profile': ['fd-avatar', 'fd-avatar-group', 'ui5-avatar'],
752
+ 'pagination|paging': ['fd-pagination'],
753
+ 'master detail|split|column': ['fd-flexible-column-layout', 'ui5-flexible-column-layout'],
754
+ breadcrumb: ['fd-breadcrumb', 'ui5-breadcrumbs'],
755
+ 'progress|loading|busy': ['fd-busy-indicator', 'fd-progress-indicator', 'ui5-busy-indicator'],
756
+ 'timeline|feed': ['fd-timeline', 'fd-feed-list-item', 'ui5-timeline'],
757
+ 'approval|workflow': ['fdp-approval-flow'],
758
+ 'shell|app header': ['fdb-tool-header', 'ui5-shell-bar'],
759
+ 'slider|range': ['fd-slider', 'fdp-slider', 'ui5-slider', 'ui5-range-slider'],
760
+ 'token|tag|chip': ['fd-token', 'ui5-token', 'ui5-tag'],
761
+ 'combobox|autocomplete': ['fd-combobox', 'fd-multi-combobox', 'ui5-combo-box', 'ui5-multi-combo-box'],
762
+ 'rating|star': ['fd-rating-indicator', 'ui5-rating-indicator'],
763
+ 'icon|symbol': ['fd-icon', 'ui5-icon'],
764
+ 'skeleton|placeholder': ['fd-skeleton'],
765
+ 'panel|section': ['fd-panel', 'fd-layout-panel', 'ui5-panel'],
766
+ 'dynamic page|object page': ['fd-dynamic-page', 'fdp-dynamic-page', 'ui5-dynamic-page'],
767
+ 'splitter|resizable': ['fdb-splitter'],
768
+ 'ai|artificial intelligence|prompt': [
769
+ 'ui5-ai-button',
770
+ 'ui5-ai-prompt-input',
771
+ 'ui5-ai-text-area',
772
+ 'ui5-ai-writing-assistant'
773
+ ]
774
+ };
775
+ // ---------------------------------------------------------------------------
776
+ // Start server
777
+ // ---------------------------------------------------------------------------
778
+ async function main() {
779
+ // Load design tokens and changelog entries in parallel
780
+ const basePath = resolve(__dirname, '..', '..', '..');
781
+ const [tokens, changelog] = await Promise.all([
782
+ extractDesignTokens(basePath).catch(() => []),
783
+ extractChangelogs(basePath).catch(() => [])
784
+ ]);
785
+ designTokens = tokens;
786
+ changelogEntries = changelog;
787
+ const transport = new StdioServerTransport();
788
+ await server.connect(transport);
789
+ }
790
+ main().catch((error) => {
791
+ console.error('MCP server failed to start:', error);
792
+ process.exit(1);
793
+ });
794
+ //# sourceMappingURL=server.js.map