@lumeo-ui/mcp-server 3.5.0 → 3.5.2
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/index.js +199 -5
- package/package.json +1 -1
- package/src/components-api.json +3870 -634
- package/src/registry.json +18 -17
package/dist/index.js
CHANGED
|
@@ -40,6 +40,26 @@ const api = loadComponentsApi() ?? {
|
|
|
40
40
|
};
|
|
41
41
|
const components = Object.values(api.components).sort((a, b) => a.name.localeCompare(b.name));
|
|
42
42
|
const byName = new Map(components.map((c) => [c.name.toLowerCase(), c]));
|
|
43
|
+
// Sub-component → parent map. Sub-components (SheetContent, SelectTrigger,
|
|
44
|
+
// CardHeader, DialogContent, TabsTrigger, AccordionItem, …) are real,
|
|
45
|
+
// usable elements but only exist nested under their parent in the schema.
|
|
46
|
+
// Without this, an agent looking one up by name got "not found" even
|
|
47
|
+
// though the element exists. Resolving to the parent surfaces the full
|
|
48
|
+
// composition (the parent payload lists all its sub-components).
|
|
49
|
+
const bySubName = new Map();
|
|
50
|
+
for (const c of components) {
|
|
51
|
+
for (const s of Object.values(c.subComponents)) {
|
|
52
|
+
const key = s.componentName.toLowerCase();
|
|
53
|
+
if (!byName.has(key) && !bySubName.has(key))
|
|
54
|
+
bySubName.set(key, c);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// Service-layer public API (OverlayService, ThemeBuilder, IResponsiveService,
|
|
58
|
+
// global enums, …). These live in plain C#, not .razor, so they were
|
|
59
|
+
// previously invisible to agents. Indexed here so they're discoverable the
|
|
60
|
+
// same way components are.
|
|
61
|
+
const services = (api.services ?? []).slice().sort((a, b) => a.name.localeCompare(b.name));
|
|
62
|
+
const serviceByName = new Map(services.map((s) => [s.name.toLowerCase(), s]));
|
|
43
63
|
const CATEGORIES = Array.from(new Set(components.map((c) => c.category))).sort();
|
|
44
64
|
const themeTokens = api.themeTokens ?? [];
|
|
45
65
|
const patterns = api.patterns ?? [];
|
|
@@ -50,7 +70,9 @@ const patternByKey = new Map(patterns.map((p) => [p.title.toLowerCase().replace(
|
|
|
50
70
|
const curatedExampleByName = new Map(curatedExamples.map((c) => [c.name.toLowerCase(), c.example]));
|
|
51
71
|
// ───────────────── Helpers ─────────────────
|
|
52
72
|
function findComponent(name) {
|
|
53
|
-
|
|
73
|
+
// Top-level match first; fall back to the parent of a matching sub-component
|
|
74
|
+
// so e.g. "SheetContent" / "CardHeader" / "SelectTrigger" resolve.
|
|
75
|
+
return byName.get(name.toLowerCase()) ?? bySubName.get(name.toLowerCase());
|
|
54
76
|
}
|
|
55
77
|
function score(c, q) {
|
|
56
78
|
const needle = q.toLowerCase();
|
|
@@ -67,6 +89,19 @@ function score(c, q) {
|
|
|
67
89
|
s += 10;
|
|
68
90
|
if (c.description.toLowerCase().includes(needle))
|
|
69
91
|
s += 5;
|
|
92
|
+
// Sub-component name matches surface the parent — so searching a nested
|
|
93
|
+
// element ("SheetContent", "TabsTrigger") finds the component that owns it.
|
|
94
|
+
for (const sub of Object.values(c.subComponents)) {
|
|
95
|
+
const sn = sub.componentName.toLowerCase();
|
|
96
|
+
if (sn === needle) {
|
|
97
|
+
s += 80;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
if (sn.includes(needle)) {
|
|
101
|
+
s += 20;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
70
105
|
return s;
|
|
71
106
|
}
|
|
72
107
|
function searchCatalog(query, category) {
|
|
@@ -169,6 +204,86 @@ function toCategoryMarkdown(category) {
|
|
|
169
204
|
"",
|
|
170
205
|
].join("\n");
|
|
171
206
|
}
|
|
207
|
+
// ───────────────── Services ─────────────────
|
|
208
|
+
function findService(name) {
|
|
209
|
+
return serviceByName.get(name.toLowerCase());
|
|
210
|
+
}
|
|
211
|
+
function toServiceMarkdown(s) {
|
|
212
|
+
const propRows = s.properties
|
|
213
|
+
.map((p) => `| \`${p.name}\` | \`${p.type}\` | \`${p.default ?? "—"}\` | ${p.summary ?? ""} |`)
|
|
214
|
+
.join("\n");
|
|
215
|
+
const methodRows = s.methods
|
|
216
|
+
.map((m) => `- \`${m.signature}\` → \`${m.returnType}\`${m.summary ? ` — ${m.summary}` : ""}`)
|
|
217
|
+
.join("\n");
|
|
218
|
+
const enumRows = s.enumValues
|
|
219
|
+
.map((e) => `- **${e.name}**${e.summary ? ` — ${e.summary}` : ""}`)
|
|
220
|
+
.join("\n");
|
|
221
|
+
const eventRows = s.events
|
|
222
|
+
.map((e) => `- **${e.name}** \`${e.type}\`${e.summary ? ` — ${e.summary}` : ""}`)
|
|
223
|
+
.join("\n");
|
|
224
|
+
const sections = [
|
|
225
|
+
`# ${s.name}`,
|
|
226
|
+
"",
|
|
227
|
+
`**Kind:** ${s.kind}`,
|
|
228
|
+
`**Namespace:** \`${s.namespace ?? "Lumeo"}\``,
|
|
229
|
+
"",
|
|
230
|
+
s.summary ?? "_(no summary)_",
|
|
231
|
+
"",
|
|
232
|
+
];
|
|
233
|
+
if (s.enumValues.length)
|
|
234
|
+
sections.push("## Values", "", enumRows, "");
|
|
235
|
+
if (s.properties.length) {
|
|
236
|
+
sections.push("## Properties", "", "| Name | Type | Default | Summary |", "|---|---|---|---|", propRows, "");
|
|
237
|
+
}
|
|
238
|
+
if (s.methods.length)
|
|
239
|
+
sections.push("## Methods", "", methodRows, "");
|
|
240
|
+
if (s.events.length)
|
|
241
|
+
sections.push("## Events", "", eventRows, "");
|
|
242
|
+
return sections.join("\n");
|
|
243
|
+
}
|
|
244
|
+
function toServiceListPayload(s) {
|
|
245
|
+
return { name: s.name, kind: s.kind, summary: s.summary };
|
|
246
|
+
}
|
|
247
|
+
function searchServices(query) {
|
|
248
|
+
const needle = query.toLowerCase();
|
|
249
|
+
if (!needle)
|
|
250
|
+
return services;
|
|
251
|
+
return services
|
|
252
|
+
.map((s) => {
|
|
253
|
+
let score = 0;
|
|
254
|
+
if (s.name.toLowerCase() === needle)
|
|
255
|
+
score += 100;
|
|
256
|
+
if (s.name.toLowerCase().includes(needle))
|
|
257
|
+
score += 30;
|
|
258
|
+
if ((s.summary ?? "").toLowerCase().includes(needle))
|
|
259
|
+
score += 5;
|
|
260
|
+
if (s.enumValues.some((e) => e.name.toLowerCase() === needle))
|
|
261
|
+
score += 20;
|
|
262
|
+
if (s.properties.some((p) => p.name.toLowerCase() === needle))
|
|
263
|
+
score += 10;
|
|
264
|
+
if (s.methods.some((m) => m.name.toLowerCase() === needle))
|
|
265
|
+
score += 10;
|
|
266
|
+
// Events too — so an event-only query (ViewportChanged, OnThemeChanged,
|
|
267
|
+
// OnShow, OnClose) surfaces the owning service/interface.
|
|
268
|
+
if (s.events.some((e) => e.name.toLowerCase() === needle))
|
|
269
|
+
score += 10;
|
|
270
|
+
// Substring (fuzzy) member matching so partial queries discover the
|
|
271
|
+
// service: "scrollable" → OverlayOptions.ScrollableBody, "showdialog" →
|
|
272
|
+
// OverlayService.ShowDialogAsync, "mobile" → MobileFullscreen, etc.
|
|
273
|
+
if (s.properties.some((p) => p.name.toLowerCase().includes(needle)))
|
|
274
|
+
score += 6;
|
|
275
|
+
if (s.methods.some((m) => m.name.toLowerCase().includes(needle)))
|
|
276
|
+
score += 6;
|
|
277
|
+
if (s.events.some((e) => e.name.toLowerCase().includes(needle)))
|
|
278
|
+
score += 6;
|
|
279
|
+
if (s.enumValues.some((e) => e.name.toLowerCase().includes(needle)))
|
|
280
|
+
score += 6;
|
|
281
|
+
return { s, score };
|
|
282
|
+
})
|
|
283
|
+
.filter((x) => x.score > 0)
|
|
284
|
+
.sort((a, b) => b.score - a.score)
|
|
285
|
+
.map((x) => x.s);
|
|
286
|
+
}
|
|
172
287
|
function toListPayload(c) {
|
|
173
288
|
return {
|
|
174
289
|
name: c.name,
|
|
@@ -389,9 +504,37 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
389
504
|
},
|
|
390
505
|
},
|
|
391
506
|
},
|
|
507
|
+
{
|
|
508
|
+
name: "lumeo_list_services",
|
|
509
|
+
description: `List all ${services.length} Lumeo SERVICE-LAYER public API types — services (OverlayService, ` +
|
|
510
|
+
"ThemeService, ComponentInteropService), options records (OverlayOptions, AlertDialogOptions), " +
|
|
511
|
+
"interfaces (IResponsiveService, IThemeService), the fluent ThemeBuilder, and global enums " +
|
|
512
|
+
"(Size, Density, Side, Align, Orientation, Breakpoint, ThemeMode, …). These live in plain C#, " +
|
|
513
|
+
"not Razor. Returns { name, kind, summary } per type. Use lumeo_get_service for the full API.",
|
|
514
|
+
inputSchema: { type: "object", properties: {} },
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
name: "lumeo_get_service",
|
|
518
|
+
description: "Get the COMPLETE public API for a Lumeo service-layer type: its summary, public properties " +
|
|
519
|
+
"(name, type, default, XML doc), public methods (signature, return type, doc), and enum values. " +
|
|
520
|
+
"Covers OverlayService (ShowDialogAsync/ShowSheetAsync/ShowAlertDialogAsync), OverlayOptions " +
|
|
521
|
+
"(ScrollableBody, MobileFullscreen, …), ThemeBuilder, IResponsiveService, AlertDialogOptions, and " +
|
|
522
|
+
"the global enums. Sourced from the actual C# source via Roslyn.",
|
|
523
|
+
inputSchema: {
|
|
524
|
+
type: "object",
|
|
525
|
+
required: ["name"],
|
|
526
|
+
properties: {
|
|
527
|
+
name: {
|
|
528
|
+
type: "string",
|
|
529
|
+
description: "Service/type name (e.g. \"OverlayService\", \"OverlayOptions\", \"ThemeBuilder\", \"Size\"). Case-insensitive.",
|
|
530
|
+
},
|
|
531
|
+
},
|
|
532
|
+
},
|
|
533
|
+
},
|
|
392
534
|
{
|
|
393
535
|
name: "lumeo_search",
|
|
394
|
-
description: `Fuzzy search across all ${components.length} Lumeo components (name, category, description)
|
|
536
|
+
description: `Fuzzy search across all ${components.length} Lumeo components (name, category, description) ` +
|
|
537
|
+
`and ${services.length} service-layer types. Best matches first.`,
|
|
395
538
|
inputSchema: {
|
|
396
539
|
type: "object",
|
|
397
540
|
required: ["query"],
|
|
@@ -498,10 +641,41 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
498
641
|
}
|
|
499
642
|
return { content: [{ type: "text", text: JSON.stringify(toGetPayload(c), null, 2) }] };
|
|
500
643
|
}
|
|
644
|
+
case "lumeo_list_services": {
|
|
645
|
+
const results = services.map(toServiceListPayload);
|
|
646
|
+
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
|
|
647
|
+
}
|
|
648
|
+
case "lumeo_get_service": {
|
|
649
|
+
const wanted = typeof a.name === "string" ? a.name : "";
|
|
650
|
+
const s = findService(wanted);
|
|
651
|
+
if (!s) {
|
|
652
|
+
return {
|
|
653
|
+
isError: true,
|
|
654
|
+
content: [{
|
|
655
|
+
type: "text",
|
|
656
|
+
text: `Service "${wanted}" not found. Use lumeo_list_services to discover available service-layer types.`,
|
|
657
|
+
}],
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
return { content: [{ type: "text", text: JSON.stringify(s, null, 2) }] };
|
|
661
|
+
}
|
|
501
662
|
case "lumeo_search": {
|
|
502
663
|
const query = typeof a.query === "string" ? a.query : "";
|
|
503
|
-
|
|
504
|
-
|
|
664
|
+
// Flat array (the original lumeo_search shape) so existing clients that
|
|
665
|
+
// iterate the result list keep working. Component matches come first;
|
|
666
|
+
// service matches are appended. Each item carries a `resultType`
|
|
667
|
+
// discriminator ("component" | "service") for clients that want to
|
|
668
|
+
// distinguish — a harmless extra field for those that don't.
|
|
669
|
+
const results = [
|
|
670
|
+
...searchCatalog(query).map((c) => ({ resultType: "component", ...toListPayload(c) })),
|
|
671
|
+
...searchServices(query).map((s) => ({ resultType: "service", ...toServiceListPayload(s) })),
|
|
672
|
+
];
|
|
673
|
+
return {
|
|
674
|
+
content: [{
|
|
675
|
+
type: "text",
|
|
676
|
+
text: JSON.stringify(results, null, 2),
|
|
677
|
+
}],
|
|
678
|
+
};
|
|
505
679
|
}
|
|
506
680
|
case "lumeo_get_example": {
|
|
507
681
|
const wanted = typeof a.name === "string" ? a.name : "";
|
|
@@ -592,6 +766,12 @@ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
|
592
766
|
description: `Overview of all Lumeo components in the ${cat} category.`,
|
|
593
767
|
mimeType: "text/markdown",
|
|
594
768
|
})),
|
|
769
|
+
...services.map((s) => ({
|
|
770
|
+
uri: `lumeo://service/${s.name}`,
|
|
771
|
+
name: `${s.name} (Lumeo ${s.kind})`,
|
|
772
|
+
description: s.summary ?? `Lumeo service-layer ${s.kind}.`,
|
|
773
|
+
mimeType: "text/markdown",
|
|
774
|
+
})),
|
|
595
775
|
],
|
|
596
776
|
}));
|
|
597
777
|
server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({
|
|
@@ -608,6 +788,12 @@ server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({
|
|
|
608
788
|
description: "Markdown overview of all components in a Lumeo category.",
|
|
609
789
|
mimeType: "text/markdown",
|
|
610
790
|
},
|
|
791
|
+
{
|
|
792
|
+
uriTemplate: "lumeo://service/{name}",
|
|
793
|
+
name: "Lumeo service-layer reference",
|
|
794
|
+
description: "Markdown reference for a single Lumeo service-layer type, generated from C# source.",
|
|
795
|
+
mimeType: "text/markdown",
|
|
796
|
+
},
|
|
611
797
|
],
|
|
612
798
|
}));
|
|
613
799
|
server.setRequestHandler(ReadResourceRequestSchema, async (req) => {
|
|
@@ -625,13 +811,21 @@ server.setRequestHandler(ReadResourceRequestSchema, async (req) => {
|
|
|
625
811
|
const cat = decodeURIComponent(categoryMatch[1]);
|
|
626
812
|
return { contents: [{ uri, mimeType: "text/markdown", text: toCategoryMarkdown(cat) }] };
|
|
627
813
|
}
|
|
814
|
+
const serviceMatch = /^lumeo:\/\/service\/(.+)$/i.exec(uri);
|
|
815
|
+
if (serviceMatch) {
|
|
816
|
+
const wanted = decodeURIComponent(serviceMatch[1]);
|
|
817
|
+
const s = findService(wanted);
|
|
818
|
+
if (!s)
|
|
819
|
+
throw new Error(`Unknown Lumeo service: ${wanted}`);
|
|
820
|
+
return { contents: [{ uri, mimeType: "text/markdown", text: toServiceMarkdown(s) }] };
|
|
821
|
+
}
|
|
628
822
|
throw new Error(`Unsupported resource URI: ${uri}`);
|
|
629
823
|
});
|
|
630
824
|
// ───── Start ─────
|
|
631
825
|
async function main() {
|
|
632
826
|
const transport = new StdioServerTransport();
|
|
633
827
|
await server.connect(transport);
|
|
634
|
-
process.stderr.write(`[lumeo-mcp] ready — ${components.length} components, ${CATEGORIES.length} categories, ` +
|
|
828
|
+
process.stderr.write(`[lumeo-mcp] ready — ${components.length} components, ${services.length} services, ${CATEGORIES.length} categories, ` +
|
|
635
829
|
`${api.stats.totalParameters} params, ${api.stats.totalEnums} enums, ` +
|
|
636
830
|
`${themeTokens.length} theme tokens, ${patterns.length} patterns, ` +
|
|
637
831
|
`api v${api.version}, generated ${api.generated}\n`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lumeo-ui/mcp-server",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.2",
|
|
4
4
|
"description": "Model Context Protocol server for the Lumeo Blazor component library. Lets LLMs (Claude, Copilot, Cursor) author correct Lumeo markup.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|