@fundamental-ngx/mcp 0.62.0-rc.100

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