@lumeo-ui/mcp-server 2.0.0-rc.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 ADDED
@@ -0,0 +1,357 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Lumeo MCP Server
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.):
8
+ *
9
+ * 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
13
+ *
14
+ * Resources (URI template):
15
+ * - lumeo://component/{name} — markdown reference per component
16
+ * - lumeo://category/{name} — all components in a category
17
+ *
18
+ * Transport: stdio (the standard for spawned MCP servers).
19
+ */
20
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
21
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
22
+ import { CallToolRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
23
+ import { catalog, CATEGORIES, registry, } from "./components.js";
24
+ const DOCS_BASE = "https://lumeo.nativ.sh";
25
+ // ───────────────── Helpers ─────────────────
26
+ const byName = new Map(catalog.map((c) => [c.name.toLowerCase(), c]));
27
+ function findComponent(name) {
28
+ return byName.get(name.toLowerCase());
29
+ }
30
+ function score(c, q) {
31
+ const needle = q.toLowerCase();
32
+ if (!needle)
33
+ return 0;
34
+ let s = 0;
35
+ if (c.name.toLowerCase() === needle)
36
+ s += 100;
37
+ if (c.name.toLowerCase().startsWith(needle))
38
+ s += 50;
39
+ if (c.name.toLowerCase().includes(needle))
40
+ s += 25;
41
+ if (c.category.toLowerCase().includes(needle))
42
+ s += 10;
43
+ if (c.description.toLowerCase().includes(needle))
44
+ s += 5;
45
+ return s;
46
+ }
47
+ function searchCatalog(query, category) {
48
+ let pool = catalog;
49
+ if (category) {
50
+ pool = pool.filter((c) => c.category.toLowerCase() === category.toLowerCase());
51
+ }
52
+ if (!query)
53
+ return pool;
54
+ return pool
55
+ .map((c) => ({ c, s: score(c, query) }))
56
+ .filter((x) => x.s > 0)
57
+ .sort((a, b) => b.s - a.s)
58
+ .map((x) => x.c);
59
+ }
60
+ function docsUrl(c) {
61
+ return `${DOCS_BASE}/components/${c.slug}`;
62
+ }
63
+ function toRichMarkdown(c) {
64
+ const paramRows = c.params
65
+ .map((p) => `| \`${p.name}\` | \`${p.type}\` | \`${p.default}\` | ${p.description} |`)
66
+ .join("\n");
67
+ const slotRows = c.slots
68
+ .map((s) => `| \`${s.name}\` | ${s.description} |`)
69
+ .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
+ 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`
103
+ : "";
104
+ const depsBlock = c.dependencies.length
105
+ ? `## Dependencies\n\n${c.dependencies.map((d) => `- \`${d}\``).join("\n")}\n\n`
106
+ : "";
107
+ return [
108
+ `# ${c.name}`,
109
+ ``,
110
+ `**Category:** ${c.category}`,
111
+ ``,
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);
124
+ }
125
+ 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
+ }
130
+ const rows = inCat
131
+ .map((c) => `| [\`${c.name}\`](lumeo://component/${c.name}) | ${c.thin ? "" : "*"}${c.description}${c.thin ? "" : "*"} |`)
132
+ .join("\n");
133
+ return [
134
+ `# ${category}`,
135
+ ``,
136
+ `${inCat.length} component${inCat.length === 1 ? "" : "s"}:`,
137
+ ``,
138
+ `| Component | Description |`,
139
+ `|---|---|`,
140
+ rows,
141
+ ``,
142
+ ].join("\n");
143
+ }
144
+ function toListPayload(c) {
145
+ return {
146
+ name: c.name,
147
+ category: c.category,
148
+ description: c.description,
149
+ };
150
+ }
151
+ // ───────────────── Server setup ─────────────────
152
+ const server = new Server({
153
+ name: "lumeo-mcp",
154
+ version: "2.0.0",
155
+ }, {
156
+ capabilities: {
157
+ tools: {},
158
+ resources: {},
159
+ },
160
+ });
161
+ // ───── Tools ─────
162
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
163
+ tools: [
164
+ {
165
+ 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(", ")}.`,
169
+ inputSchema: {
170
+ type: "object",
171
+ 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
+ },
180
+ },
181
+ },
182
+ },
183
+ {
184
+ 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.",
189
+ inputSchema: {
190
+ type: "object",
191
+ required: ["name"],
192
+ properties: {
193
+ name: {
194
+ type: "string",
195
+ description: "Component name (e.g. \"Button\", \"DataGrid\"). Case-insensitive.",
196
+ },
197
+ },
198
+ },
199
+ },
200
+ {
201
+ name: "lumeo_search",
202
+ description: `Fuzzy search across all ${catalog.length} Lumeo components (names, categories, descriptions). Returns best matches first.`,
203
+ inputSchema: {
204
+ type: "object",
205
+ required: ["query"],
206
+ properties: {
207
+ query: {
208
+ type: "string",
209
+ description: "Search terms (e.g. \"modal\", \"date\", \"chat message\").",
210
+ },
211
+ },
212
+ },
213
+ },
214
+ ],
215
+ }));
216
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
217
+ const { name, arguments: args } = req.params;
218
+ const a = (args ?? {});
219
+ switch (name) {
220
+ case "lumeo_list_components": {
221
+ const category = typeof a.category === "string" ? a.category : undefined;
222
+ const query = typeof a.query === "string" ? a.query : "";
223
+ const results = searchCatalog(query, category).map(toListPayload);
224
+ return {
225
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
226
+ };
227
+ }
228
+ case "lumeo_get_component": {
229
+ const wanted = typeof a.name === "string" ? a.name : "";
230
+ const c = findComponent(wanted);
231
+ if (!c) {
232
+ return {
233
+ isError: true,
234
+ content: [{
235
+ type: "text",
236
+ text: `Component "${wanted}" not found. Use lumeo_list_components or lumeo_search to discover available components.`,
237
+ }],
238
+ };
239
+ }
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
+ };
263
+ }
264
+ case "lumeo_search": {
265
+ const query = typeof a.query === "string" ? a.query : "";
266
+ const results = searchCatalog(query).map(toListPayload);
267
+ return {
268
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
269
+ };
270
+ }
271
+ default:
272
+ return {
273
+ isError: true,
274
+ content: [{ type: "text", text: `Unknown tool: ${name}` }],
275
+ };
276
+ }
277
+ });
278
+ // ───── Resources ─────
279
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
280
+ resources: [
281
+ ...catalog.map((c) => ({
282
+ uri: `lumeo://component/${c.name}`,
283
+ name: `${c.name} (Lumeo component)`,
284
+ description: c.description,
285
+ mimeType: "text/markdown",
286
+ })),
287
+ ...CATEGORIES.map((cat) => ({
288
+ uri: `lumeo://category/${cat}`,
289
+ name: `Lumeo ${cat} components`,
290
+ description: `Overview of all Lumeo components in the ${cat} category.`,
291
+ mimeType: "text/markdown",
292
+ })),
293
+ ],
294
+ }));
295
+ server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({
296
+ resourceTemplates: [
297
+ {
298
+ uriTemplate: "lumeo://component/{name}",
299
+ 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.",
301
+ mimeType: "text/markdown",
302
+ },
303
+ {
304
+ uriTemplate: "lumeo://category/{name}",
305
+ name: "Lumeo category overview",
306
+ description: "Markdown overview of all components in a Lumeo category.",
307
+ mimeType: "text/markdown",
308
+ },
309
+ ],
310
+ }));
311
+ server.setRequestHandler(ReadResourceRequestSchema, async (req) => {
312
+ const uri = req.params.uri;
313
+ const componentMatch = /^lumeo:\/\/component\/(.+)$/i.exec(uri);
314
+ 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
+ };
327
+ }
328
+ const categoryMatch = /^lumeo:\/\/category\/(.+)$/i.exec(uri);
329
+ if (categoryMatch) {
330
+ const cat = decodeURIComponent(categoryMatch[1]);
331
+ return {
332
+ contents: [{
333
+ uri,
334
+ mimeType: "text/markdown",
335
+ text: toCategoryMarkdown(cat),
336
+ }],
337
+ };
338
+ }
339
+ throw new Error(`Unsupported resource URI: ${uri}`);
340
+ });
341
+ // ───── Start ─────
342
+ async function main() {
343
+ const transport = new StdioServerTransport();
344
+ 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`);
353
+ }
354
+ main().catch((err) => {
355
+ process.stderr.write(`[lumeo-mcp] fatal: ${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
356
+ process.exit(1);
357
+ });
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Loads Lumeo's generated registry.json (synced into src/registry.json at
3
+ * prebuild time — see `scripts/sync-registry.mjs`). All 125 components are
4
+ * surfaced to the MCP server through this file so `lumeo_list_components`,
5
+ * `lumeo_get_component`, and `lumeo_search` can cover the full catalog.
6
+ *
7
+ * Shape of the file:
8
+ * {
9
+ * "$schema": "...",
10
+ * "version": "...",
11
+ * "generated": "...",
12
+ * "components": {
13
+ * "<slug>": {
14
+ * "name": "ComponentName",
15
+ * "category": "Forms",
16
+ * "description": "...",
17
+ * "files": [...],
18
+ * "dependencies": [...],
19
+ * "cssVars": [...],
20
+ * "registryUrl": "https://lumeo.nativ.sh/registry/<slug>.json"
21
+ * }
22
+ * }
23
+ * }
24
+ *
25
+ * Failures are swallowed — the MCP server stays functional (just with the
26
+ * hand-curated catalog only) when the sync step didn't run.
27
+ */
28
+ import { readFileSync, existsSync } from "node:fs";
29
+ import { fileURLToPath } from "node:url";
30
+ import { dirname, resolve } from "node:path";
31
+ function findRegistryPath() {
32
+ try {
33
+ const here = dirname(fileURLToPath(import.meta.url));
34
+ // dist/ → tools/lumeo-mcp; src/ → tools/lumeo-mcp
35
+ const candidates = [
36
+ resolve(here, "../src/registry.json"),
37
+ resolve(here, "./registry.json"),
38
+ // Fall back to the monorepo source if the sync step never ran.
39
+ resolve(here, "../../..", "src/Lumeo/registry/registry.json"),
40
+ resolve(here, "../..", "src/Lumeo/registry/registry.json"),
41
+ ];
42
+ for (const c of candidates) {
43
+ if (existsSync(c))
44
+ return c;
45
+ }
46
+ }
47
+ catch {
48
+ // ignore
49
+ }
50
+ return null;
51
+ }
52
+ export function loadRegistry() {
53
+ const path = findRegistryPath();
54
+ if (!path)
55
+ return null;
56
+ try {
57
+ const raw = readFileSync(path, "utf8");
58
+ const parsed = JSON.parse(raw);
59
+ if (!parsed || typeof parsed !== "object" || !parsed.components)
60
+ return null;
61
+ const components = [];
62
+ for (const [slug, entry] of Object.entries(parsed.components)) {
63
+ if (!entry || !entry.name)
64
+ continue;
65
+ components.push({
66
+ slug,
67
+ name: entry.name,
68
+ category: entry.category ?? "Unknown",
69
+ description: entry.description ?? "",
70
+ files: entry.files ?? [],
71
+ dependencies: entry.dependencies ?? [],
72
+ cssVars: entry.cssVars ?? [],
73
+ registryUrl: entry.registryUrl,
74
+ });
75
+ }
76
+ return {
77
+ version: parsed.version ?? "unknown",
78
+ generated: parsed.generated ?? "",
79
+ components,
80
+ };
81
+ }
82
+ catch {
83
+ // swallow — fall back to the hand-curated catalog only
84
+ }
85
+ return null;
86
+ }
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@lumeo-ui/mcp-server",
3
+ "version": "2.0.0-rc.1",
4
+ "description": "Model Context Protocol server for the Lumeo Blazor component library. Lets LLMs (Claude, Copilot, Cursor) author correct Lumeo markup.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "lumeo-mcp": "./dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "src/registry.json",
13
+ "README.md"
14
+ ],
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "scripts": {
19
+ "prebuild": "node scripts/sync-registry.mjs",
20
+ "build": "tsc",
21
+ "start": "node dist/index.js",
22
+ "dev": "tsc --watch"
23
+ },
24
+ "keywords": [
25
+ "mcp",
26
+ "model-context-protocol",
27
+ "lumeo",
28
+ "blazor",
29
+ "llm"
30
+ ],
31
+ "license": "MIT",
32
+ "dependencies": {
33
+ "@modelcontextprotocol/sdk": "^1.0.0"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^22.0.0",
37
+ "typescript": "^5.6.0"
38
+ }
39
+ }