@lumeo-ui/mcp-server 2.0.0-rc.18 → 2.0.0-rc.19

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.
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Loads the auto-generated `components-api.json` produced by
3
+ * `tools/Lumeo.RegistryGen` (Roslyn-based scan of every `[Parameter]` /
4
+ * `[CascadingParameter]` property across every Razor component in the repo).
5
+ *
6
+ * This is the source-of-truth schema for ALL 131 Lumeo components. The
7
+ * legacy hand-curated `components.ts` is kept as an OPTIONAL example overlay:
8
+ * when it has an entry for a component we surface its `example` Razor snippet
9
+ * verbatim alongside the auto-generated parameter list.
10
+ */
11
+ import { existsSync, readFileSync } from "node:fs";
12
+ import { dirname, resolve } from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+ function findPath() {
15
+ try {
16
+ const here = dirname(fileURLToPath(import.meta.url));
17
+ // dist/ -> ../src/components-api.json
18
+ // src/ -> ./components-api.json
19
+ const candidates = [
20
+ resolve(here, "../src/components-api.json"),
21
+ resolve(here, "./components-api.json"),
22
+ // monorepo fallback when running uninstalled
23
+ resolve(here, "../..", "components-api.json"),
24
+ ];
25
+ for (const c of candidates)
26
+ if (existsSync(c))
27
+ return c;
28
+ }
29
+ catch { /* ignore */ }
30
+ return null;
31
+ }
32
+ export function loadComponentsApi() {
33
+ const path = findPath();
34
+ if (!path)
35
+ return null;
36
+ try {
37
+ const raw = readFileSync(path, "utf8");
38
+ const parsed = JSON.parse(raw);
39
+ if (!parsed?.components)
40
+ return null;
41
+ return parsed;
42
+ }
43
+ catch {
44
+ return null;
45
+ }
46
+ }
package/dist/index.js CHANGED
@@ -1,29 +1,44 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Lumeo MCP Server
3
+ * Lumeo MCP Server v2.0.0
4
4
  *
5
- * Exposes Lumeo's full 125-component catalog (sourced from the generated
6
- * registry.json, enriched by hand-curated rich entries for the top 35)
7
- * to MCP-compatible LLM clients (Claude Desktop, Cursor, Copilot, etc.):
5
+ * Source-of-truth schema for ALL 131 Lumeo components, generated at build time
6
+ * by `tools/Lumeo.RegistryGen` from the actual Razor source via Roslyn. Every
7
+ * component now ships full parameter / enum / event / sub-component metadata —
8
+ * no thin/rich split, no manual catalog drift.
8
9
  *
9
10
  * Tools:
10
- * - lumeo_list_components — list/filter the full catalog
11
- * - lumeo_get_component — rich schema when curated, thin otherwise
12
- * - lumeo_search — fuzzy text search across all 125
11
+ * - lumeo_list_components — list/filter all 131 components (name+category+description)
12
+ * - lumeo_get_component — full schema (params, enums, events, sub-components, files)
13
+ * - lumeo_search — fuzzy search across name/category/description
13
14
  *
14
15
  * Resources (URI template):
15
16
  * - lumeo://component/{name} — markdown reference per component
16
- * - lumeo://category/{name} — all components in a category
17
+ * - lumeo://category/{name} — overview of all components in a category
17
18
  *
18
19
  * Transport: stdio (the standard for spawned MCP servers).
19
20
  */
20
21
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
21
22
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
22
23
  import { CallToolRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
23
- import { catalog, CATEGORIES, registry, } from "./components.js";
24
+ import { loadComponentsApi, } from "./componentsApi.js";
25
+ import { components as curatedExamples } from "./components.js";
24
26
  const DOCS_BASE = "https://lumeo.nativ.sh";
27
+ // ───────────────── Load source-of-truth schema ─────────────────
28
+ const api = loadComponentsApi() ?? {
29
+ version: "0.0.0",
30
+ generated: "",
31
+ stats: { componentCount: 0, totalParameters: 0, totalEnums: 0, totalRecords: 0, thinFallbacks: [] },
32
+ components: {},
33
+ };
34
+ const components = Object.values(api.components).sort((a, b) => a.name.localeCompare(b.name));
35
+ const byName = new Map(components.map((c) => [c.name.toLowerCase(), c]));
36
+ const CATEGORIES = Array.from(new Set(components.map((c) => c.category))).sort();
37
+ // Hand-curated examples overlay (~30 components). Auto-gen schema wins for
38
+ // parameters/enums/events; the curated `example` Razor snippet is preserved
39
+ // as a documentation aid because LLMs benefit from seeing real usage.
40
+ const curatedExampleByName = new Map(curatedExamples.map((c) => [c.name.toLowerCase(), c.example]));
25
41
  // ───────────────── Helpers ─────────────────
26
- const byName = new Map(catalog.map((c) => [c.name.toLowerCase(), c]));
27
42
  function findComponent(name) {
28
43
  return byName.get(name.toLowerCase());
29
44
  }
@@ -45,10 +60,9 @@ function score(c, q) {
45
60
  return s;
46
61
  }
47
62
  function searchCatalog(query, category) {
48
- let pool = catalog;
49
- if (category) {
63
+ let pool = components;
64
+ if (category)
50
65
  pool = pool.filter((c) => c.category.toLowerCase() === category.toLowerCase());
51
- }
52
66
  if (!query)
53
67
  return pool;
54
68
  return pool
@@ -58,94 +72,123 @@ function searchCatalog(query, category) {
58
72
  .map((x) => x.c);
59
73
  }
60
74
  function docsUrl(c) {
61
- return `${DOCS_BASE}/components/${c.slug}`;
75
+ // Convert PascalCase to kebab-case for the docs URL
76
+ const slug = c.name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
77
+ return `${DOCS_BASE}/components/${slug}`;
78
+ }
79
+ function paramRow(p) {
80
+ const def = p.default ?? "—";
81
+ const desc = p.description ?? "";
82
+ const flags = [];
83
+ if (p.isCascading)
84
+ flags.push("cascading");
85
+ if (p.captureUnmatched)
86
+ flags.push("captures unmatched");
87
+ const flagStr = flags.length ? ` _(${flags.join(", ")})_` : "";
88
+ return `| \`${p.name}\` | \`${p.type}\` | \`${def}\` | ${desc}${flagStr} |`;
62
89
  }
63
- function toRichMarkdown(c) {
64
- const paramRows = c.params
65
- .map((p) => `| \`${p.name}\` | \`${p.type}\` | \`${p.default}\` | ${p.description} |`)
90
+ function toComponentMarkdown(c) {
91
+ const paramRows = c.parameters.map(paramRow).join("\n");
92
+ const enumRows = c.enums
93
+ .map((e) => `- **${e.name}**: ${e.values.join(", ")}${e.description ? ` — ${e.description}` : ""}`)
66
94
  .join("\n");
67
- const slotRows = c.slots
68
- .map((s) => `| \`${s.name}\` | ${s.description} |`)
95
+ const eventRows = c.events
96
+ .map((e) => `- **${e.name}** \`${e.type}\`${e.description ? ` — ${e.description}` : ""}`)
97
+ .join("\n");
98
+ const subRows = Object.values(c.subComponents)
99
+ .map((s) => `- **${s.componentName}** (${s.parameters.length} params)`)
69
100
  .join("\n");
70
- const cssVarsBlock = c.cssVars.length
71
- ? `## CSS Variables\n\n${c.cssVars.map((v) => `- \`${v}\``).join("\n")}\n\n`
72
- : "";
73
- return [
74
- `# ${c.name}`,
75
- ``,
76
- `**Category:** ${c.category}`,
77
- ``,
78
- c.description,
79
- ``,
80
- `## Parameters`,
81
- ``,
82
- `| Param | Type | Default | Description |`,
83
- `|---|---|---|---|`,
84
- paramRows || `| _(none)_ | | | |`,
85
- ``,
86
- c.slots.length ? `## Slots\n\n| Slot | Description |\n|---|---|\n${slotRows}\n` : ``,
87
- `## Example`,
88
- ``,
89
- "```razor",
90
- c.example,
91
- "```",
92
- ``,
93
- cssVarsBlock,
94
- `_Docs: ${docsUrl(c)}_`,
95
- ].filter(Boolean).join("\n");
96
- }
97
- function toThinMarkdown(c) {
98
101
  const filesBlock = c.files.length
99
- ? `## Files\n\n${c.files.map((f) => `- \`${f}\``).join("\n")}\n\n`
100
- : "";
101
- const cssVarsBlock = c.cssVars.length
102
- ? `## CSS Variables\n\n${c.cssVars.map((v) => `- \`${v}\``).join("\n")}\n\n`
102
+ ? c.files.map((f) => `- \`${f}\``).join("\n")
103
103
  : "";
104
- const depsBlock = c.dependencies.length
105
- ? `## Dependencies\n\n${c.dependencies.map((d) => `- \`${d}\``).join("\n")}\n\n`
106
- : "";
107
- return [
104
+ const example = curatedExampleByName.get(c.name.toLowerCase());
105
+ const sections = [
108
106
  `# ${c.name}`,
109
- ``,
110
- `**Category:** ${c.category}`,
111
- ``,
107
+ "",
108
+ `**Category:** ${c.category}${c.subcategory ? ` › ${c.subcategory}` : ""}`,
109
+ `**NuGet:** \`${c.nugetPackage}\``,
110
+ `**Namespace:** \`${c.namespace ?? "Lumeo"}\``,
111
+ "",
112
112
  c.description,
113
- ``,
114
- `> Rich schema (parameters, slots, Razor example) is coming soon for this component.`,
115
- `> See the docs site for full usage: [${docsUrl(c)}](${docsUrl(c)})`,
116
- ``,
117
- filesBlock,
118
- depsBlock,
119
- cssVarsBlock,
120
- ].filter(Boolean).join("\n");
121
- }
122
- function toComponentMarkdown(c) {
123
- return c.thin ? toThinMarkdown(c) : toRichMarkdown(c);
113
+ "",
114
+ "## Parameters",
115
+ "",
116
+ "| Name | Type | Default | Description |",
117
+ "|---|---|---|---|",
118
+ paramRows || "| _(none)_ | | | |",
119
+ "",
120
+ ];
121
+ if (c.enums.length)
122
+ sections.push("## Enums", "", enumRows, "");
123
+ if (c.events.length)
124
+ sections.push("## Events", "", eventRows, "");
125
+ if (Object.keys(c.subComponents).length)
126
+ sections.push("## Sub-components", "", subRows, "");
127
+ if (example)
128
+ sections.push("## Example", "", "```razor", example, "```", "");
129
+ if (filesBlock)
130
+ sections.push("## Source files", "", filesBlock, "");
131
+ sections.push(`_Docs: ${docsUrl(c)}_`);
132
+ return sections.join("\n");
124
133
  }
125
134
  function toCategoryMarkdown(category) {
126
- const inCat = catalog.filter((c) => c.category.toLowerCase() === category.toLowerCase());
127
- if (inCat.length === 0) {
128
- return `# ${category}\n\nNo components documented in this category yet.`;
129
- }
135
+ const inCat = components.filter((c) => c.category.toLowerCase() === category.toLowerCase());
136
+ if (inCat.length === 0)
137
+ return `# ${category}\n\nNo components in this category.`;
130
138
  const rows = inCat
131
- .map((c) => `| [\`${c.name}\`](lumeo://component/${c.name}) | ${c.thin ? "" : "*"}${c.description}${c.thin ? "" : "*"} |`)
139
+ .map((c) => `| [\`${c.name}\`](lumeo://component/${c.name}) | ${c.description} |`)
132
140
  .join("\n");
133
141
  return [
134
142
  `# ${category}`,
135
- ``,
143
+ "",
136
144
  `${inCat.length} component${inCat.length === 1 ? "" : "s"}:`,
137
- ``,
145
+ "",
138
146
  `| Component | Description |`,
139
147
  `|---|---|`,
140
148
  rows,
141
- ``,
149
+ "",
142
150
  ].join("\n");
143
151
  }
144
152
  function toListPayload(c) {
145
153
  return {
146
154
  name: c.name,
147
155
  category: c.category,
156
+ subcategory: c.subcategory,
148
157
  description: c.description,
158
+ nugetPackage: c.nugetPackage,
159
+ };
160
+ }
161
+ function toGetPayload(c) {
162
+ // Build a rich JSON payload covering everything Claude Code needs to write
163
+ // correct Razor without consulting external docs.
164
+ const subComponents = Object.values(c.subComponents).map((s) => ({
165
+ name: s.componentName,
166
+ namespace: s.namespace,
167
+ inheritsFrom: s.inheritsFrom,
168
+ implements: s.implements,
169
+ parameters: s.parameters,
170
+ events: s.events,
171
+ enums: s.enums,
172
+ records: s.records,
173
+ }));
174
+ return {
175
+ name: c.name,
176
+ category: c.category,
177
+ subcategory: c.subcategory,
178
+ description: c.description,
179
+ nugetPackage: c.nugetPackage,
180
+ namespace: c.namespace,
181
+ inheritsFrom: c.inheritsFrom,
182
+ implements: c.implements,
183
+ parameters: c.parameters,
184
+ events: c.events,
185
+ enums: c.enums,
186
+ records: c.records,
187
+ cssVars: c.cssVars,
188
+ files: c.files,
189
+ subComponents,
190
+ example: curatedExampleByName.get(c.name.toLowerCase()) ?? null,
191
+ docs: docsUrl(c),
149
192
  };
150
193
  }
151
194
  // ───────────────── Server setup ─────────────────
@@ -163,51 +206,44 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
163
206
  tools: [
164
207
  {
165
208
  name: "lumeo_list_components",
166
- description: `List all ${catalog.length} Lumeo components, optionally filtered by category or a free-text query. ` +
167
- "Returns an array of { name, category, description }. " +
168
- `Known categories: ${CATEGORIES.join(", ")}.`,
209
+ description: `List all ${components.length} Lumeo components, optionally filtered by category or query. ` +
210
+ "Returns { name, category, subcategory, description, nugetPackage } per component. " +
211
+ `Categories: ${CATEGORIES.join(", ")}.`,
169
212
  inputSchema: {
170
213
  type: "object",
171
214
  properties: {
172
- category: {
173
- type: "string",
174
- description: `Filter by category (${CATEGORIES.join(", ")}).`,
175
- },
176
- query: {
177
- type: "string",
178
- description: "Free-text query matched against name, category, and description.",
179
- },
215
+ category: { type: "string", description: `Filter by category (${CATEGORIES.join(", ")}).` },
216
+ query: { type: "string", description: "Free-text query matched against name, category, description." },
180
217
  },
181
218
  },
182
219
  },
183
220
  {
184
221
  name: "lumeo_get_component",
185
- description: "Get the reference for a single Lumeo component. " +
186
- "For the ~35 hand-curated components this returns full schema " +
187
- "(parameters, slots, Razor example, CSS variables). " +
188
- "For the remaining components, returns { name, category, description, files, cssVars, dependencies, note } with a link to the docs site.",
222
+ description: "Get the COMPLETE schema for a Lumeo component: every [Parameter] " +
223
+ "(name, type, default, XML doc summary), nested enums and records, " +
224
+ "EventCallback events, sub-components (e.g. Dialog DialogContent, " +
225
+ "DialogHeader, DialogTrigger, ...), CSS variables, source files, and a " +
226
+ "hand-curated Razor example when available. Sourced from the actual " +
227
+ "Razor source via Roslyn — always in sync with the library.",
189
228
  inputSchema: {
190
229
  type: "object",
191
230
  required: ["name"],
192
231
  properties: {
193
232
  name: {
194
233
  type: "string",
195
- description: "Component name (e.g. \"Button\", \"DataGrid\"). Case-insensitive.",
234
+ description: "Component name (e.g. \"Button\", \"DataGrid\", \"Sheet\"). Case-insensitive.",
196
235
  },
197
236
  },
198
237
  },
199
238
  },
200
239
  {
201
240
  name: "lumeo_search",
202
- description: `Fuzzy search across all ${catalog.length} Lumeo components (names, categories, descriptions). Returns best matches first.`,
241
+ description: `Fuzzy search across all ${components.length} Lumeo components (name, category, description). Best matches first.`,
203
242
  inputSchema: {
204
243
  type: "object",
205
244
  required: ["query"],
206
245
  properties: {
207
- query: {
208
- type: "string",
209
- description: "Search terms (e.g. \"modal\", \"date\", \"chat message\").",
210
- },
246
+ query: { type: "string", description: "Search terms (e.g. \"modal\", \"date\", \"chat message\")." },
211
247
  },
212
248
  },
213
249
  },
@@ -221,9 +257,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
221
257
  const category = typeof a.category === "string" ? a.category : undefined;
222
258
  const query = typeof a.query === "string" ? a.query : "";
223
259
  const results = searchCatalog(query, category).map(toListPayload);
224
- return {
225
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
226
- };
260
+ return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
227
261
  }
228
262
  case "lumeo_get_component": {
229
263
  const wanted = typeof a.name === "string" ? a.name : "";
@@ -237,48 +271,21 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
237
271
  }],
238
272
  };
239
273
  }
240
- const payload = c.thin
241
- ? {
242
- name: c.name,
243
- category: c.category,
244
- description: c.description,
245
- files: c.files,
246
- cssVars: c.cssVars,
247
- dependencies: c.dependencies,
248
- note: `Rich schema coming soon — see ${docsUrl(c)}`,
249
- }
250
- : {
251
- name: c.name,
252
- category: c.category,
253
- description: c.description,
254
- params: c.params,
255
- slots: c.slots,
256
- example: c.example,
257
- cssVars: c.cssVars,
258
- docs: docsUrl(c),
259
- };
260
- return {
261
- content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
262
- };
274
+ return { content: [{ type: "text", text: JSON.stringify(toGetPayload(c), null, 2) }] };
263
275
  }
264
276
  case "lumeo_search": {
265
277
  const query = typeof a.query === "string" ? a.query : "";
266
278
  const results = searchCatalog(query).map(toListPayload);
267
- return {
268
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
269
- };
279
+ return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
270
280
  }
271
281
  default:
272
- return {
273
- isError: true,
274
- content: [{ type: "text", text: `Unknown tool: ${name}` }],
275
- };
282
+ return { isError: true, content: [{ type: "text", text: `Unknown tool: ${name}` }] };
276
283
  }
277
284
  });
278
285
  // ───── Resources ─────
279
286
  server.setRequestHandler(ListResourcesRequestSchema, async () => ({
280
287
  resources: [
281
- ...catalog.map((c) => ({
288
+ ...components.map((c) => ({
282
289
  uri: `lumeo://component/${c.name}`,
283
290
  name: `${c.name} (Lumeo component)`,
284
291
  description: c.description,
@@ -297,7 +304,7 @@ server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({
297
304
  {
298
305
  uriTemplate: "lumeo://component/{name}",
299
306
  name: "Lumeo component reference",
300
- description: "Markdown reference for a single Lumeo component. Rich (params/slots/example) for curated components, thin (files/cssVars + docs link) otherwise.",
307
+ description: "Markdown reference for a single Lumeo component, generated from Razor source.",
301
308
  mimeType: "text/markdown",
302
309
  },
303
310
  {
@@ -312,29 +319,16 @@ server.setRequestHandler(ReadResourceRequestSchema, async (req) => {
312
319
  const uri = req.params.uri;
313
320
  const componentMatch = /^lumeo:\/\/component\/(.+)$/i.exec(uri);
314
321
  if (componentMatch) {
315
- const name = decodeURIComponent(componentMatch[1]);
316
- const c = findComponent(name);
317
- if (!c) {
318
- throw new Error(`Unknown Lumeo component: ${name}`);
319
- }
320
- return {
321
- contents: [{
322
- uri,
323
- mimeType: "text/markdown",
324
- text: toComponentMarkdown(c),
325
- }],
326
- };
322
+ const wanted = decodeURIComponent(componentMatch[1]);
323
+ const c = findComponent(wanted);
324
+ if (!c)
325
+ throw new Error(`Unknown Lumeo component: ${wanted}`);
326
+ return { contents: [{ uri, mimeType: "text/markdown", text: toComponentMarkdown(c) }] };
327
327
  }
328
328
  const categoryMatch = /^lumeo:\/\/category\/(.+)$/i.exec(uri);
329
329
  if (categoryMatch) {
330
330
  const cat = decodeURIComponent(categoryMatch[1]);
331
- return {
332
- contents: [{
333
- uri,
334
- mimeType: "text/markdown",
335
- text: toCategoryMarkdown(cat),
336
- }],
337
- };
331
+ return { contents: [{ uri, mimeType: "text/markdown", text: toCategoryMarkdown(cat) }] };
338
332
  }
339
333
  throw new Error(`Unsupported resource URI: ${uri}`);
340
334
  });
@@ -342,14 +336,9 @@ server.setRequestHandler(ReadResourceRequestSchema, async (req) => {
342
336
  async function main() {
343
337
  const transport = new StdioServerTransport();
344
338
  await server.connect(transport);
345
- // stderr is safe; stdout is the MCP transport
346
- const richCount = catalog.filter((c) => !c.thin).length;
347
- const thinCount = catalog.length - richCount;
348
- const registryNote = registry
349
- ? `, registry v${registry.version}`
350
- : " (no registry — curated-only mode)";
351
- process.stderr.write(`[lumeo-mcp] ready — ${catalog.length} components, ${CATEGORIES.length} categories ` +
352
- `(${richCount} rich, ${thinCount} thin)${registryNote}\n`);
339
+ process.stderr.write(`[lumeo-mcp] ready ${components.length} components, ${CATEGORIES.length} categories, ` +
340
+ `${api.stats.totalParameters} params, ${api.stats.totalEnums} enums, ` +
341
+ `api v${api.version}, generated ${api.generated}\n`);
353
342
  }
354
343
  main().catch((err) => {
355
344
  process.stderr.write(`[lumeo-mcp] fatal: ${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumeo-ui/mcp-server",
3
- "version": "2.0.0-rc.18",
3
+ "version": "2.0.0-rc.19",
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",
@@ -10,6 +10,7 @@
10
10
  "files": [
11
11
  "dist",
12
12
  "src/registry.json",
13
+ "src/components-api.json",
13
14
  "README.md"
14
15
  ],
15
16
  "publishConfig": {