@lumeo-ui/mcp-server 3.5.0 → 3.5.1

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 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
- return byName.get(name.toLowerCase());
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). Best matches first.`,
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
- const results = searchCatalog(query).map(toListPayload);
504
- return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
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.0",
3
+ "version": "3.5.1",
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",