@santi020k/lumen-mcp 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -9
- package/bin/lumen-mcp-http.mjs +20 -0
- package/bin/lumen-mcp-local.mjs +5 -5
- package/data/lumen-data.json +3983 -694
- package/dist/data.d.ts +26 -0
- package/dist/data.d.ts.map +1 -1
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +110 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +95 -3
- package/dist/tools.d.ts +52 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +192 -16
- package/package.json +11 -3
package/dist/tools.js
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
import { loadLumenData } from './data.js';
|
|
2
2
|
const normalize = (value) => value.replaceAll(/([a-z0-9])([A-Z])/g, '$1-$2').replaceAll(/[\s_]+/g, '-').toLowerCase();
|
|
3
3
|
const tokenize = (value) => normalize(value).split(/[^a-z0-9]+/).filter(Boolean);
|
|
4
|
+
const searchStopWords = new Set([
|
|
5
|
+
'a', 'accessibility', 'accessible', 'admin', 'an', 'and', 'app', 'application', 'component', 'for',
|
|
6
|
+
'in', 'lumen', 'not', 'of', 'or', 'the', 'to', 'ui', 'with'
|
|
7
|
+
]);
|
|
8
|
+
const searchTermAliases = {
|
|
9
|
+
booking: ['booking', 'schedule', 'calendar', 'appointment', 'date'],
|
|
10
|
+
dark: ['dark', 'theme'],
|
|
11
|
+
dashboard: ['dashboard', 'data-table', 'sidebar', 'chart', 'stat', 'metric'],
|
|
12
|
+
mode: ['mode', 'theme'],
|
|
13
|
+
pagination: ['pagination', 'paginated'],
|
|
14
|
+
sortable: ['sortable', 'sort']
|
|
15
|
+
};
|
|
16
|
+
const meaningfulSearchTerms = (value) => {
|
|
17
|
+
const terms = tokenize(value);
|
|
18
|
+
const meaningful = terms.filter((term) => !searchStopWords.has(term));
|
|
19
|
+
return meaningful.length > 0 ? meaningful : terms;
|
|
20
|
+
};
|
|
4
21
|
const availableFrameworks = (component) => Object.entries(component.frameworks)
|
|
5
22
|
.filter(([, available]) => available)
|
|
6
23
|
.map(([framework]) => framework);
|
|
@@ -23,22 +40,29 @@ export const resolveRecipe = (name, data = loadLumenData()) => {
|
|
|
23
40
|
const target = normalize(name);
|
|
24
41
|
return data.recipes.find((recipe) => normalize(recipe.name) === target);
|
|
25
42
|
};
|
|
26
|
-
const frameworkSearchText = (component) =>
|
|
43
|
+
const frameworkSearchText = (component, framework) => (framework
|
|
44
|
+
? [component.frameworkDetails[framework]]
|
|
45
|
+
: Object.values(component.frameworkDetails))
|
|
27
46
|
.flatMap((framework) => [
|
|
28
47
|
framework.packageName,
|
|
48
|
+
framework.behavior?.description ?? '',
|
|
49
|
+
framework.behavior?.hook ?? '',
|
|
50
|
+
framework.behavior?.mode ?? '',
|
|
51
|
+
framework.behavior?.setup ?? '',
|
|
52
|
+
framework.example,
|
|
29
53
|
framework.propsExtends ?? '',
|
|
30
54
|
framework.props.map((prop) => `${prop.name} ${prop.type}`).join(' '),
|
|
31
55
|
framework.attributes?.join(' ') ?? '',
|
|
32
56
|
framework.tagName ?? ''
|
|
33
57
|
])
|
|
34
58
|
.join(' ');
|
|
35
|
-
const componentSearchText = (component) => [
|
|
59
|
+
const componentSearchText = (component, framework) => [
|
|
36
60
|
component.name,
|
|
37
61
|
component.kebab,
|
|
38
|
-
'accessible accessibility',
|
|
39
62
|
component.description,
|
|
40
63
|
component.category,
|
|
41
64
|
component.collections.join(' '),
|
|
65
|
+
component.recipes.map((recipe) => recipe.name).join(' '),
|
|
42
66
|
component.keywords.join(' '),
|
|
43
67
|
component.dependencies.join(' '),
|
|
44
68
|
component.guidance?.when ?? '',
|
|
@@ -46,16 +70,23 @@ const componentSearchText = (component) => [
|
|
|
46
70
|
component.apiReference
|
|
47
71
|
.map((row) => `${row.attribute} ${row.values} ${row.description}`)
|
|
48
72
|
.join(' '),
|
|
49
|
-
frameworkSearchText(component)
|
|
73
|
+
frameworkSearchText(component, framework)
|
|
50
74
|
].join(' ');
|
|
51
75
|
const scoreCandidate = (query, terms, searchableText, primaryName = '') => {
|
|
52
76
|
const normalizedText = normalize(searchableText);
|
|
53
|
-
const matchedTerms = terms.filter((term) => normalizedText.includes(
|
|
54
|
-
if (matchedTerms.length
|
|
77
|
+
const matchedTerms = terms.filter((term) => (searchTermAliases[term] ?? [term]).some((candidate) => normalizedText.includes(candidate)));
|
|
78
|
+
if (matchedTerms.length === 0)
|
|
55
79
|
return { matchedTerms, score: 0 };
|
|
56
|
-
const normalizedQuery =
|
|
80
|
+
const normalizedQuery = terms.join('-');
|
|
57
81
|
const normalizedName = normalize(primaryName);
|
|
58
|
-
|
|
82
|
+
const coverage = matchedTerms.length / terms.length;
|
|
83
|
+
let score = matchedTerms.reduce((total, term) => total + ((searchTermAliases[term] ?? [term]).some((candidate) => normalizedName.includes(candidate))
|
|
84
|
+
? 20
|
|
85
|
+
: 5), 0);
|
|
86
|
+
score += Math.round(coverage * 80);
|
|
87
|
+
score -= (terms.length - matchedTerms.length) * 3;
|
|
88
|
+
if (terms.includes(normalizedName))
|
|
89
|
+
score += 50;
|
|
59
90
|
if (normalizedText.includes(normalizedQuery))
|
|
60
91
|
score += 100;
|
|
61
92
|
if (normalizedName === normalizedQuery)
|
|
@@ -63,20 +94,22 @@ const scoreCandidate = (query, terms, searchableText, primaryName = '') => {
|
|
|
63
94
|
return { matchedTerms, score };
|
|
64
95
|
};
|
|
65
96
|
const appendSearchResult = (results, candidate, query, terms, searchableText) => {
|
|
66
|
-
const match = scoreCandidate(query, terms, searchableText, candidate.name);
|
|
97
|
+
const match = scoreCandidate(query, terms, searchableText, candidate.kind === 'rule' ? '' : candidate.name);
|
|
67
98
|
if (match.score > 0)
|
|
68
99
|
results.push({ ...candidate, ...match });
|
|
69
100
|
};
|
|
70
|
-
const collectSearchResults = (query, terms, data) => {
|
|
101
|
+
const collectSearchResults = (query, terms, data, framework) => {
|
|
71
102
|
const results = [];
|
|
72
103
|
for (const component of data.components) {
|
|
104
|
+
if (framework && !component.frameworks[framework])
|
|
105
|
+
continue;
|
|
73
106
|
appendSearchResult(results, {
|
|
74
107
|
category: component.category,
|
|
75
108
|
description: component.description,
|
|
76
109
|
frameworks: availableFrameworks(component),
|
|
77
110
|
kind: 'component',
|
|
78
111
|
name: component.name
|
|
79
|
-
}, query, terms, componentSearchText(component));
|
|
112
|
+
}, query, terms, componentSearchText(component, framework));
|
|
80
113
|
}
|
|
81
114
|
for (const recipe of data.recipes) {
|
|
82
115
|
appendSearchResult(results, {
|
|
@@ -104,14 +137,17 @@ const collectSearchResults = (query, terms, data) => {
|
|
|
104
137
|
};
|
|
105
138
|
export const listComponents = (args = {}, data = loadLumenData()) => {
|
|
106
139
|
const query = args.query?.trim();
|
|
107
|
-
const queryTerms = query ?
|
|
140
|
+
const queryTerms = query ? meaningfulSearchTerms(query) : [];
|
|
141
|
+
const recipe = args.recipe ? resolveRecipe(args.recipe, data) : undefined;
|
|
142
|
+
const recipeName = recipe?.name ?? (args.recipe ? normalize(args.recipe) : undefined);
|
|
108
143
|
const matches = data.components.filter((component) => {
|
|
109
144
|
if (args.framework && !component.frameworks[args.framework])
|
|
110
145
|
return false;
|
|
111
|
-
if (
|
|
146
|
+
if (recipeName &&
|
|
147
|
+
!component.recipes.some((componentRecipe) => normalize(componentRecipe.name) === recipeName))
|
|
112
148
|
return false;
|
|
113
149
|
if (query &&
|
|
114
|
-
scoreCandidate(query, queryTerms, componentSearchText(component), component.name).score === 0)
|
|
150
|
+
scoreCandidate(query, queryTerms, componentSearchText(component, args.framework), component.name).score === 0)
|
|
115
151
|
return false;
|
|
116
152
|
return true;
|
|
117
153
|
});
|
|
@@ -181,6 +217,9 @@ const componentDetail = (component, framework, detail) => {
|
|
|
181
217
|
: usage;
|
|
182
218
|
};
|
|
183
219
|
const formatProps = (framework) => {
|
|
220
|
+
if (framework.attributes && framework.attributes.length > 0) {
|
|
221
|
+
return `Attributes:\n${framework.attributes.map((attribute) => `- ${attribute}`).join('\n')}`;
|
|
222
|
+
}
|
|
184
223
|
if (framework.props.length === 0) {
|
|
185
224
|
return framework.propsExtends
|
|
186
225
|
? `Props extend ${framework.propsExtends}; no additional local props were detected.`
|
|
@@ -211,6 +250,19 @@ const formatComponentUsage = (component, frameworkName, detail) => {
|
|
|
211
250
|
return sections.join('\n');
|
|
212
251
|
sections.push('', formatProps(framework), '', '## Example', `\`\`\`${framework.language}`);
|
|
213
252
|
sections.push(framework.example, '```');
|
|
253
|
+
if (framework.behavior) {
|
|
254
|
+
sections.push('', '## Framework behavior', framework.behavior.setup);
|
|
255
|
+
if (framework.behavior.hook)
|
|
256
|
+
sections.push(`Hook: ${framework.behavior.hook}`);
|
|
257
|
+
if (framework.behavior.description)
|
|
258
|
+
sections.push(framework.behavior.description);
|
|
259
|
+
if (framework.behavior.options && framework.behavior.options.length > 0) {
|
|
260
|
+
sections.push('', 'Hook options:', ...framework.behavior.options.map((option) => `- ${option.name}: ${option.type} — ${option.description}`));
|
|
261
|
+
}
|
|
262
|
+
if (framework.behavior.controller && framework.behavior.controller.length > 0) {
|
|
263
|
+
sections.push('', 'Controller values:', ...framework.behavior.controller.map((value) => `- ${value.name}: ${value.type} — ${value.description}`));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
214
266
|
if (component.guidance) {
|
|
215
267
|
sections.push('', '## Guidance', component.guidance.when);
|
|
216
268
|
if (component.guidance.distinction)
|
|
@@ -298,8 +350,16 @@ export const search = (args, data = loadLumenData()) => {
|
|
|
298
350
|
};
|
|
299
351
|
}
|
|
300
352
|
const limit = Math.max(1, Math.min(args.limit ?? 20, 100));
|
|
301
|
-
const results = collectSearchResults(query,
|
|
302
|
-
|
|
353
|
+
const results = collectSearchResults(query, meaningfulSearchTerms(query), data, args.framework);
|
|
354
|
+
const kindPriority = {
|
|
355
|
+
component: 0,
|
|
356
|
+
recipe: 1,
|
|
357
|
+
rule: 3,
|
|
358
|
+
token: 2
|
|
359
|
+
};
|
|
360
|
+
results.sort((left, right) => right.score - left.score ||
|
|
361
|
+
kindPriority[left.kind] - kindPriority[right.kind] ||
|
|
362
|
+
left.name.localeCompare(right.name));
|
|
303
363
|
const shown = results.slice(0, limit);
|
|
304
364
|
if (results.length === 0) {
|
|
305
365
|
return {
|
|
@@ -312,6 +372,122 @@ export const search = (args, data = loadLumenData()) => {
|
|
|
312
372
|
text: `${results.length} match(es) for "${args.query}"${results.length > shown.length ? ` (showing ${shown.length})` : ''}:\n\n${shown.map((result) => `${result.kind}: ${result.name} — ${result.description} [matched: ${result.matchedTerms.join(', ')}]`).join('\n')}`
|
|
313
373
|
};
|
|
314
374
|
};
|
|
375
|
+
export const getMeta = (data = loadLumenData()) => ({
|
|
376
|
+
data: { meta: data.meta },
|
|
377
|
+
text: [
|
|
378
|
+
'# Lumen MCP snapshot',
|
|
379
|
+
`Server version: ${data.meta.serverVersion}`,
|
|
380
|
+
`Schema version: ${data.meta.schemaVersion}`,
|
|
381
|
+
`Catalog hash: ${data.meta.catalogHash}`,
|
|
382
|
+
`Components: ${data.meta.componentCount}`,
|
|
383
|
+
'',
|
|
384
|
+
'## Package versions',
|
|
385
|
+
...Object.entries(data.meta.packageVersions).map(([name, version]) => `- ${name}: ${version}`)
|
|
386
|
+
].join('\n')
|
|
387
|
+
});
|
|
388
|
+
const diffManifestSection = (current, baseline) => {
|
|
389
|
+
const currentNames = Object.keys(current);
|
|
390
|
+
const baselineNames = Object.keys(baseline);
|
|
391
|
+
return {
|
|
392
|
+
added: currentNames.filter((name) => baseline[name] === undefined).sort(),
|
|
393
|
+
changed: currentNames
|
|
394
|
+
.filter((name) => baseline[name] !== undefined && baseline[name] !== current[name])
|
|
395
|
+
.sort(),
|
|
396
|
+
removed: baselineNames.filter((name) => current[name] === undefined).sort(),
|
|
397
|
+
unchanged: currentNames.filter((name) => baseline[name] === current[name]).sort()
|
|
398
|
+
};
|
|
399
|
+
};
|
|
400
|
+
export const getCatalogManifest = (data = loadLumenData()) => ({
|
|
401
|
+
data: {
|
|
402
|
+
catalogHash: data.meta.catalogHash,
|
|
403
|
+
manifest: data.catalogManifest,
|
|
404
|
+
schemaVersion: data.meta.schemaVersion
|
|
405
|
+
},
|
|
406
|
+
text: [
|
|
407
|
+
'# Lumen catalog manifest',
|
|
408
|
+
`Catalog hash: ${data.meta.catalogHash}`,
|
|
409
|
+
`Components: ${Object.keys(data.catalogManifest.components).length}`,
|
|
410
|
+
`Recipes: ${Object.keys(data.catalogManifest.recipes).length}`,
|
|
411
|
+
'',
|
|
412
|
+
'Keep this structured result and pass its manifest to lumen_diff_catalog after an MCP update.'
|
|
413
|
+
].join('\n')
|
|
414
|
+
});
|
|
415
|
+
export const diffCatalog = (args, data = loadLumenData()) => {
|
|
416
|
+
const components = diffManifestSection(data.catalogManifest.components, args.baseline.components);
|
|
417
|
+
const recipes = diffManifestSection(data.catalogManifest.recipes, args.baseline.recipes);
|
|
418
|
+
const changedCount = [
|
|
419
|
+
...components.added,
|
|
420
|
+
...components.changed,
|
|
421
|
+
...components.removed,
|
|
422
|
+
...recipes.added,
|
|
423
|
+
...recipes.changed,
|
|
424
|
+
...recipes.removed
|
|
425
|
+
].length;
|
|
426
|
+
const unchanged = changedCount === 0 &&
|
|
427
|
+
(args.baselineCatalogHash === undefined ||
|
|
428
|
+
args.baselineCatalogHash === data.meta.catalogHash);
|
|
429
|
+
return {
|
|
430
|
+
data: {
|
|
431
|
+
components,
|
|
432
|
+
currentCatalogHash: data.meta.catalogHash,
|
|
433
|
+
recipes,
|
|
434
|
+
unchanged
|
|
435
|
+
},
|
|
436
|
+
text: unchanged
|
|
437
|
+
? `The Lumen catalog is unchanged (${data.meta.catalogHash}).`
|
|
438
|
+
: [
|
|
439
|
+
'# Lumen catalog changes',
|
|
440
|
+
`Current catalog hash: ${data.meta.catalogHash}`,
|
|
441
|
+
`Changed entries: ${changedCount}`,
|
|
442
|
+
`Components — added: ${components.added.length}, changed: ${components.changed.length}, removed: ${components.removed.length}`,
|
|
443
|
+
`Recipes — added: ${recipes.added.length}, changed: ${recipes.changed.length}, removed: ${recipes.removed.length}`
|
|
444
|
+
].join('\n')
|
|
445
|
+
};
|
|
446
|
+
};
|
|
447
|
+
export const diagnose = (data = loadLumenData()) => {
|
|
448
|
+
const componentNames = data.components.map((component) => component.name);
|
|
449
|
+
const frameworkCoverage = {
|
|
450
|
+
astro: data.components.filter((component) => component.frameworks.astro).length,
|
|
451
|
+
elements: data.components.filter((component) => component.frameworks.elements).length,
|
|
452
|
+
react: data.components.filter((component) => component.frameworks.react).length
|
|
453
|
+
};
|
|
454
|
+
const checks = [
|
|
455
|
+
{
|
|
456
|
+
message: `${data.components.length} component records match metadata.`,
|
|
457
|
+
name: 'component-count',
|
|
458
|
+
status: data.components.length === data.meta.componentCount ? 'pass' : 'fail'
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
message: 'Component names are unique.',
|
|
462
|
+
name: 'unique-components',
|
|
463
|
+
status: new Set(componentNames).size === componentNames.length ? 'pass' : 'fail'
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
message: 'Every component has a manifest fingerprint.',
|
|
467
|
+
name: 'component-manifest',
|
|
468
|
+
status: componentNames.every((name) => data.catalogManifest.components[name]) ? 'pass' : 'fail'
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
message: 'Every available framework contract has an example and import statement.',
|
|
472
|
+
name: 'framework-contracts',
|
|
473
|
+
status: data.components.every((component) => Object.entries(component.frameworks).every(([framework, available]) => !available ||
|
|
474
|
+
Boolean(component.frameworkDetails[framework].example.trim()) &&
|
|
475
|
+
Boolean(component.frameworkDetails[framework].importStatement.trim()))) ? 'pass' : 'fail'
|
|
476
|
+
}
|
|
477
|
+
];
|
|
478
|
+
const status = checks.every((check) => check.status === 'pass') ? 'healthy' : 'issues';
|
|
479
|
+
return {
|
|
480
|
+
data: { checks, frameworkCoverage, status },
|
|
481
|
+
isError: status === 'issues',
|
|
482
|
+
text: [
|
|
483
|
+
`# Lumen MCP diagnostics: ${status}`,
|
|
484
|
+
`Catalog hash: ${data.meta.catalogHash}`,
|
|
485
|
+
`Framework coverage — Astro: ${frameworkCoverage.astro}, React: ${frameworkCoverage.react}, Elements: ${frameworkCoverage.elements}`,
|
|
486
|
+
'',
|
|
487
|
+
...checks.map((check) => `- ${check.status.toUpperCase()} ${check.name}: ${check.message}`)
|
|
488
|
+
].join('\n')
|
|
489
|
+
};
|
|
490
|
+
};
|
|
315
491
|
export const getTokens = (data = loadLumenData()) => {
|
|
316
492
|
const { tokens } = data;
|
|
317
493
|
const colorLines = Object.entries(tokens.colors).map(([name, value]) => `- ${name}: hsl(${value})`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@santi020k/lumen-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -29,20 +29,23 @@
|
|
|
29
29
|
"LICENSE"
|
|
30
30
|
],
|
|
31
31
|
"bin": {
|
|
32
|
-
"lumen-mcp": "./bin/lumen-mcp.mjs"
|
|
32
|
+
"lumen-mcp": "./bin/lumen-mcp.mjs",
|
|
33
|
+
"lumen-mcp-http": "./bin/lumen-mcp-http.mjs"
|
|
33
34
|
},
|
|
34
35
|
"dependencies": {
|
|
35
36
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
36
37
|
"zod": "^4.4.3"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|
|
40
|
+
"@astrojs/compiler": "^3.0.1",
|
|
41
|
+
"@types/express": "^5.0.6",
|
|
39
42
|
"@types/node": "^26.1.1",
|
|
40
43
|
"@vitest/coverage-v8": "^4.1.10",
|
|
41
44
|
"typescript": "^6.0.3",
|
|
42
45
|
"vitest": "^4.1.10"
|
|
43
46
|
},
|
|
44
47
|
"engines": {
|
|
45
|
-
"node": ">=
|
|
48
|
+
"node": ">=20.20.0"
|
|
46
49
|
},
|
|
47
50
|
"repository": {
|
|
48
51
|
"type": "git",
|
|
@@ -69,9 +72,14 @@
|
|
|
69
72
|
"license": "MIT",
|
|
70
73
|
"scripts": {
|
|
71
74
|
"generate": "node scripts/generate-data.mjs",
|
|
75
|
+
"check:snapshot": "node scripts/generate-data.mjs --check",
|
|
72
76
|
"build": "pnpm run generate && tsc -p tsconfig.build.json",
|
|
73
77
|
"dev": "pnpm run generate && tsc -p tsconfig.build.json --watch --preserveWatchOutput",
|
|
74
78
|
"local": "node bin/lumen-mcp-local.mjs",
|
|
79
|
+
"eval": "pnpm --filter @santi020k/lumen-react... run build && pnpm run build && node scripts/evaluate.mjs && node scripts/validate-examples.mjs",
|
|
80
|
+
"eval:examples": "pnpm --filter @santi020k/lumen-react... run build && pnpm run build && node scripts/validate-examples.mjs",
|
|
81
|
+
"eval:search": "pnpm run build && node scripts/evaluate.mjs --search-only",
|
|
82
|
+
"smoke:package": "pnpm run build && node scripts/smoke-package.mjs",
|
|
75
83
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
76
84
|
"test": "vitest run --config ../../vitest.config.ts --project mcp",
|
|
77
85
|
"test:coverage": "vitest run --config ../../vitest.config.ts --project mcp --coverage",
|