@lumeo-ui/mcp-server 3.4.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) {
@@ -132,6 +167,16 @@ function toComponentMarkdown(c) {
132
167
  sections.push("## Enums", "", enumRows, "");
133
168
  if (c.events.length)
134
169
  sections.push("## Events", "", eventRows, "");
170
+ // Aggregate gotchas from the root component AND its sub-components — a
171
+ // gotcha is often declared on a sub-component (e.g. SheetContent carries
172
+ // the Sheet gotcha), so the root resource must surface those too.
173
+ const gotchaLines = [
174
+ ...(c.gotchas ?? []).map((g) => `- ${g}`),
175
+ ...Object.values(c.subComponents).flatMap((s) => (s.gotchas ?? []).map((g) => `- **${s.componentName}**: ${g}`)),
176
+ ];
177
+ if (gotchaLines.length) {
178
+ sections.push("## Gotchas", "", gotchaLines.join("\n"), "");
179
+ }
135
180
  if (Object.keys(c.subComponents).length)
136
181
  sections.push("## Sub-components", "", subRows, "");
137
182
  if (example)
@@ -159,6 +204,86 @@ function toCategoryMarkdown(category) {
159
204
  "",
160
205
  ].join("\n");
161
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
+ }
162
287
  function toListPayload(c) {
163
288
  return {
164
289
  name: c.name,
@@ -180,6 +305,7 @@ function toGetPayload(c) {
180
305
  events: s.events,
181
306
  enums: s.enums,
182
307
  records: s.records,
308
+ gotchas: s.gotchas ?? [],
183
309
  }));
184
310
  return {
185
311
  name: c.name,
@@ -195,6 +321,7 @@ function toGetPayload(c) {
195
321
  enums: c.enums,
196
322
  records: c.records,
197
323
  cssVars: c.cssVars,
324
+ gotchas: c.gotchas ?? [],
198
325
  files: c.files,
199
326
  subComponents,
200
327
  examples: c.examples ?? [],
@@ -377,9 +504,37 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
377
504
  },
378
505
  },
379
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
+ },
380
534
  {
381
535
  name: "lumeo_search",
382
- 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.`,
383
538
  inputSchema: {
384
539
  type: "object",
385
540
  required: ["query"],
@@ -486,10 +641,41 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
486
641
  }
487
642
  return { content: [{ type: "text", text: JSON.stringify(toGetPayload(c), null, 2) }] };
488
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
+ }
489
662
  case "lumeo_search": {
490
663
  const query = typeof a.query === "string" ? a.query : "";
491
- const results = searchCatalog(query).map(toListPayload);
492
- 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
+ };
493
679
  }
494
680
  case "lumeo_get_example": {
495
681
  const wanted = typeof a.name === "string" ? a.name : "";
@@ -580,6 +766,12 @@ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
580
766
  description: `Overview of all Lumeo components in the ${cat} category.`,
581
767
  mimeType: "text/markdown",
582
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
+ })),
583
775
  ],
584
776
  }));
585
777
  server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({
@@ -596,6 +788,12 @@ server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({
596
788
  description: "Markdown overview of all components in a Lumeo category.",
597
789
  mimeType: "text/markdown",
598
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
+ },
599
797
  ],
600
798
  }));
601
799
  server.setRequestHandler(ReadResourceRequestSchema, async (req) => {
@@ -613,13 +811,21 @@ server.setRequestHandler(ReadResourceRequestSchema, async (req) => {
613
811
  const cat = decodeURIComponent(categoryMatch[1]);
614
812
  return { contents: [{ uri, mimeType: "text/markdown", text: toCategoryMarkdown(cat) }] };
615
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
+ }
616
822
  throw new Error(`Unsupported resource URI: ${uri}`);
617
823
  });
618
824
  // ───── Start ─────
619
825
  async function main() {
620
826
  const transport = new StdioServerTransport();
621
827
  await server.connect(transport);
622
- 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, ` +
623
829
  `${api.stats.totalParameters} params, ${api.stats.totalEnums} enums, ` +
624
830
  `${themeTokens.length} theme tokens, ${patterns.length} patterns, ` +
625
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.4.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",