nyxui-mcp 1.0.0

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.
Files changed (3) hide show
  1. package/mcp-server.mjs +686 -0
  2. package/package.json +40 -0
  3. package/readme.md +64 -0
package/mcp-server.mjs ADDED
@@ -0,0 +1,686 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { fileURLToPath, pathToFileURL } from "node:url";
4
+ import {
5
+ McpServer,
6
+ ResourceTemplate,
7
+ } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
+ import { z } from "zod";
10
+
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const siteUrl = "https://nyxui.com";
13
+
14
+ const docSections = ["components", "templates", "blocks", "root"];
15
+ const registryTypes = [
16
+ "registry:style",
17
+ "registry:ui",
18
+ "registry:example",
19
+ "registry:lib",
20
+ ];
21
+ const registryKinds = ["component", "example", "style", "utility"];
22
+
23
+ let docsCache;
24
+ let registryCache;
25
+ const publicRegistryCache = new Map();
26
+
27
+ // ── fetchers ───────────────────────────────────────────────────
28
+
29
+ async function getRegistry() {
30
+ if (registryCache) return registryCache;
31
+ const res = await fetch(`${siteUrl}/registry.json`);
32
+ if (!res.ok) throw new Error(`Failed to fetch registry: ${res.status}`);
33
+ registryCache = await res.json();
34
+ return registryCache;
35
+ }
36
+
37
+ async function getPublicRegistryItem(name) {
38
+ if (publicRegistryCache.has(name)) return publicRegistryCache.get(name);
39
+ try {
40
+ const res = await fetch(`${siteUrl}/r/${name}.json`);
41
+ if (!res.ok) {
42
+ publicRegistryCache.set(name, null);
43
+ return null;
44
+ }
45
+ const parsed = await res.json();
46
+ publicRegistryCache.set(name, parsed);
47
+ return parsed;
48
+ } catch {
49
+ publicRegistryCache.set(name, null);
50
+ return null;
51
+ }
52
+ }
53
+
54
+ async function getDocs() {
55
+ if (docsCache) return docsCache;
56
+ const res = await fetch(`${siteUrl}/api/docs`);
57
+ if (!res.ok) throw new Error(`Failed to fetch docs: ${res.status}`);
58
+ const raw = await res.json();
59
+
60
+ // parse frontmatter from each doc
61
+ docsCache = raw.map(({ slug, content }) => {
62
+ const parsed = parseFrontmatter(content);
63
+ const section = getDocSection(slug);
64
+ return {
65
+ slug,
66
+ section,
67
+ title: typeof parsed.data.title === "string" ? parsed.data.title : slug,
68
+ description:
69
+ typeof parsed.data.description === "string"
70
+ ? parsed.data.description
71
+ : "",
72
+ tags: Array.isArray(parsed.data.tags) ? parsed.data.tags : [],
73
+ published: parsed.data.published !== false,
74
+ frontmatter: parsed.data,
75
+ body: parsed.body,
76
+ content,
77
+ };
78
+ });
79
+
80
+ docsCache.sort((a, b) => a.slug.localeCompare(b.slug));
81
+ return docsCache;
82
+ }
83
+
84
+ // ── frontmatter parser ─────────────────────────────────────────
85
+
86
+ function cleanValue(value) {
87
+ const trimmed = value.trim();
88
+ if (
89
+ (trimmed.startsWith('"') && trimmed.endsWith('"')) ||
90
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))
91
+ ) {
92
+ return trimmed.slice(1, -1);
93
+ }
94
+ if (trimmed === "true") return true;
95
+ if (trimmed === "false") return false;
96
+ if (/^\d+(\.\d+)?$/.test(trimmed)) return Number(trimmed);
97
+ return trimmed;
98
+ }
99
+
100
+ function parseArray(value) {
101
+ const inner = value.trim().replace(/^\[/, "").replace(/\]$/, "");
102
+ if (!inner) return [];
103
+ return inner
104
+ .split(",")
105
+ .map((item) => cleanValue(item))
106
+ .filter(Boolean);
107
+ }
108
+
109
+ function parseFrontmatter(raw) {
110
+ const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
111
+ if (!match) return { data: {}, body: raw };
112
+
113
+ const block = match[1];
114
+ const data = {};
115
+
116
+ for (const line of block.split(/\r?\n/)) {
117
+ const keyValue = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
118
+ if (!keyValue) continue;
119
+ const [, key, value] = keyValue;
120
+ if (value.trim().startsWith("[") && value.trim().endsWith("]")) {
121
+ data[key] = parseArray(value);
122
+ } else {
123
+ data[key] = cleanValue(value);
124
+ }
125
+ }
126
+
127
+ return { data, body: raw.slice(match[0].length) };
128
+ }
129
+
130
+ // ── helpers ────────────────────────────────────────────────────
131
+
132
+ function normalizeText(value) {
133
+ return value
134
+ .toLowerCase()
135
+ .replace(/[^a-z0-9]+/g, " ")
136
+ .trim();
137
+ }
138
+
139
+ function tokenize(value) {
140
+ return new Set(
141
+ normalizeText(value)
142
+ .split(/\s+/)
143
+ .filter((t) => t.length > 2),
144
+ );
145
+ }
146
+
147
+ function escapeRegExp(value) {
148
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
149
+ }
150
+
151
+ function getDocSection(slug) {
152
+ const section = slug.split("/")[0];
153
+ return docSections.includes(section) ? section : "root";
154
+ }
155
+
156
+ function getDocUrl(doc) {
157
+ if (doc.section === "components")
158
+ return `${siteUrl}/components/${doc.slug.replace(/^components\//, "")}`;
159
+ if (doc.section === "templates")
160
+ return `${siteUrl}/templates/${doc.slug.replace(/^templates\//, "")}`;
161
+ if (doc.section === "blocks")
162
+ return `${siteUrl}/blocks/${doc.slug.replace(/^blocks\//, "")}`;
163
+ if (doc.slug === "index") return `${siteUrl}/docs`;
164
+ return `${siteUrl}/docs/${doc.slug}`;
165
+ }
166
+
167
+ function buildSnippet(body, query) {
168
+ const normalizedBody = body.replace(/\s+/g, " ").trim();
169
+ const normalizedQuery = query.toLowerCase();
170
+ const index = normalizedBody.toLowerCase().indexOf(normalizedQuery);
171
+ if (index === -1) return normalizedBody.slice(0, 220);
172
+ const start = Math.max(0, index - 80);
173
+ const end = Math.min(normalizedBody.length, index + query.length + 140);
174
+ return normalizedBody.slice(start, end);
175
+ }
176
+
177
+ function scoreDoc(doc, query) {
178
+ const haystack =
179
+ `${doc.title}\n${doc.description}\n${doc.body}`.toLowerCase();
180
+ const normalizedQuery = query.toLowerCase();
181
+ if (!haystack.includes(normalizedQuery)) return 0;
182
+ let score = 1;
183
+ if (doc.title.toLowerCase().includes(normalizedQuery)) score += 5;
184
+ if (doc.description.toLowerCase().includes(normalizedQuery)) score += 3;
185
+ const matches = haystack.match(
186
+ new RegExp(escapeRegExp(normalizedQuery), "g"),
187
+ );
188
+ score += Math.min(matches?.length ?? 0, 10);
189
+ return score;
190
+ }
191
+
192
+ function getRegistryKind(item) {
193
+ switch (item.type) {
194
+ case "registry:ui":
195
+ return "component";
196
+ case "registry:example":
197
+ return "example";
198
+ case "registry:style":
199
+ return "style";
200
+ case "registry:lib":
201
+ return "utility";
202
+ default:
203
+ return item.type;
204
+ }
205
+ }
206
+
207
+ function matchesRegistryFilter(item, filter) {
208
+ if (!filter) return true;
209
+ if (registryTypes.includes(filter)) return item.type === filter;
210
+ switch (filter) {
211
+ case "component":
212
+ return item.type === "registry:ui";
213
+ case "example":
214
+ return item.type === "registry:example";
215
+ case "style":
216
+ return item.type === "registry:style";
217
+ case "utility":
218
+ return item.type === "registry:lib";
219
+ default:
220
+ return false;
221
+ }
222
+ }
223
+
224
+ function scoreRegistryItem(item, query) {
225
+ const kind = getRegistryKind(item);
226
+ const haystack = [
227
+ item.name,
228
+ item.title ?? "",
229
+ item.description ?? "",
230
+ item.type,
231
+ kind,
232
+ ]
233
+ .join("\n")
234
+ .toLowerCase();
235
+ const normalizedQuery = query.toLowerCase();
236
+ if (!haystack.includes(normalizedQuery)) return 0;
237
+ let score = 1;
238
+ if (item.name.toLowerCase() === normalizedQuery) score += 10;
239
+ if (item.name.toLowerCase().includes(normalizedQuery)) score += 6;
240
+ if ((item.title ?? "").toLowerCase().includes(normalizedQuery)) score += 5;
241
+ if ((item.description ?? "").toLowerCase().includes(normalizedQuery))
242
+ score += 3;
243
+ if (kind.includes(normalizedQuery)) score += 2;
244
+ const matches = haystack.match(
245
+ new RegExp(escapeRegExp(normalizedQuery), "g"),
246
+ );
247
+ score += Math.min(matches?.length ?? 0, 10);
248
+ return score;
249
+ }
250
+
251
+ function normalizeRegistryDependency(value) {
252
+ if (typeof value !== "string") return "";
253
+ const normalized = value.includes("/r/") ? value.split("/r/").pop() : value;
254
+ return normalized.replace(/\.json$/, "");
255
+ }
256
+
257
+ function getInstallUrl(name) {
258
+ return `${siteUrl}/r/${name}.json`;
259
+ }
260
+ function getInstallCommand(name) {
261
+ return `npx shadcn@latest add "${getInstallUrl(name)}"`;
262
+ }
263
+
264
+ function findDocForName(docs, name) {
265
+ const exactCandidates = [
266
+ `components/${name}`,
267
+ `templates/${name}`,
268
+ `blocks/${name}`,
269
+ name,
270
+ ];
271
+ return (
272
+ docs.find((doc) => exactCandidates.includes(doc.slug)) ??
273
+ docs.find((doc) => doc.slug.endsWith(`/${name}`)) ??
274
+ null
275
+ );
276
+ }
277
+
278
+ function getExamplesForItem(registry, name) {
279
+ return registry.items.filter(
280
+ (item) =>
281
+ item.type === "registry:example" &&
282
+ (item.registryDependencies ?? []).some(
283
+ (dep) => normalizeRegistryDependency(dep) === name,
284
+ ),
285
+ );
286
+ }
287
+
288
+ function buildRegistryPreview(item, docs, registry) {
289
+ const doc = findDocForName(docs, item.name);
290
+ const examples = getExamplesForItem(registry, item.name);
291
+ return {
292
+ name: item.name,
293
+ kind: getRegistryKind(item),
294
+ type: item.type,
295
+ title: item.title ?? item.name,
296
+ description: item.description ?? null,
297
+ installUrl: getInstallUrl(item.name),
298
+ installCommand: getInstallCommand(item.name),
299
+ docsUrl: doc ? getDocUrl(doc) : null,
300
+ exampleNames: examples.slice(0, 5).map((e) => e.name),
301
+ };
302
+ }
303
+
304
+ function getRelatedItems(baseItem, docs, registry, limit = 5) {
305
+ const baseDoc = findDocForName(docs, baseItem.name);
306
+ const baseTags = new Set(
307
+ (baseDoc?.tags ?? []).map((tag) => normalizeText(tag)),
308
+ );
309
+ const baseTokens = tokenize(
310
+ `${baseItem.name} ${baseItem.title ?? ""} ${baseItem.description ?? ""}`,
311
+ );
312
+
313
+ return registry.items
314
+ .filter((c) => c.name !== baseItem.name && c.type === baseItem.type)
315
+ .map((candidate) => {
316
+ const candidateDoc = findDocForName(docs, candidate.name);
317
+ const candidateTokens = tokenize(
318
+ `${candidate.name} ${candidate.title ?? ""} ${candidate.description ?? ""}`,
319
+ );
320
+ let score = 0;
321
+ if (baseDoc && candidateDoc && candidateDoc.section === baseDoc.section)
322
+ score += 2;
323
+ if (baseDoc && candidateDoc) {
324
+ const sharedTags = (candidateDoc.tags ?? []).filter((tag) =>
325
+ baseTags.has(normalizeText(tag)),
326
+ ).length;
327
+ score += sharedTags * 3;
328
+ }
329
+ for (const token of candidateTokens) {
330
+ if (baseTokens.has(token)) score += 1;
331
+ }
332
+ return { candidate, score };
333
+ })
334
+ .filter((e) => e.score > 0)
335
+ .sort(
336
+ (a, b) =>
337
+ b.score - a.score || a.candidate.name.localeCompare(b.candidate.name),
338
+ )
339
+ .slice(0, limit)
340
+ .map(({ candidate }) => buildRegistryPreview(candidate, docs, registry));
341
+ }
342
+
343
+ async function buildRegistryItemPayload(
344
+ item,
345
+ { includeSource = false, includeExamples = true, includeRelated = true } = {},
346
+ ) {
347
+ const docs = await getDocs();
348
+ const registry = await getRegistry();
349
+ const doc = findDocForName(docs, item.name);
350
+ const publicItem = await getPublicRegistryItem(item.name);
351
+ const mergedItem = publicItem ? { ...item, ...publicItem } : item;
352
+
353
+ const files = includeSource
354
+ ? (mergedItem.files ?? [])
355
+ : (mergedItem.files ?? []).map(({ content, ...file }) => file);
356
+
357
+ return {
358
+ name: mergedItem.name,
359
+ kind: getRegistryKind(mergedItem),
360
+ type: mergedItem.type,
361
+ title: mergedItem.title ?? mergedItem.name,
362
+ description: mergedItem.description ?? null,
363
+ install: {
364
+ url: getInstallUrl(mergedItem.name),
365
+ command: getInstallCommand(mergedItem.name),
366
+ },
367
+ docs: doc
368
+ ? {
369
+ slug: doc.slug,
370
+ section: doc.section,
371
+ title: doc.title,
372
+ description: doc.description,
373
+ tags: doc.tags,
374
+ url: getDocUrl(doc),
375
+ }
376
+ : null,
377
+ dependencies: mergedItem.dependencies ?? [],
378
+ devDependencies: mergedItem.devDependencies ?? [],
379
+ registryDependencies: mergedItem.registryDependencies ?? [],
380
+ files,
381
+ cssVars: mergedItem.cssVars ?? null,
382
+ css: mergedItem.css ?? null,
383
+ examples: includeExamples
384
+ ? getExamplesForItem(registry, mergedItem.name).map((e) =>
385
+ buildRegistryPreview(e, docs, registry),
386
+ )
387
+ : [],
388
+ relatedItems: includeRelated
389
+ ? getRelatedItems(mergedItem, docs, registry)
390
+ : [],
391
+ };
392
+ }
393
+
394
+ async function listRegistryItemsResult({ filter, query, limit, offset }) {
395
+ const docs = await getDocs();
396
+ const registry = await getRegistry();
397
+
398
+ const filteredItems = registry.items
399
+ .filter((item) => matchesRegistryFilter(item, filter))
400
+ .map((item) => ({
401
+ item,
402
+ score: query ? scoreRegistryItem(item, query) : 1,
403
+ }))
404
+ .filter((e) => e.score > 0)
405
+ .sort((a, b) =>
406
+ query
407
+ ? b.score - a.score || a.item.name.localeCompare(b.item.name)
408
+ : a.item.name.localeCompare(b.item.name),
409
+ );
410
+
411
+ const total = filteredItems.length;
412
+ const items = filteredItems
413
+ .slice(offset, offset + limit)
414
+ .map(({ item }) => buildRegistryPreview(item, docs, registry));
415
+
416
+ return { total, limit, offset, items };
417
+ }
418
+
419
+ // ── result helpers ─────────────────────────────────────────────
420
+
421
+ function textResult(value) {
422
+ const text =
423
+ typeof value === "string" ? value : JSON.stringify(value, null, 2);
424
+ return { content: [{ type: "text", text }] };
425
+ }
426
+
427
+ function errorResult(message) {
428
+ return { isError: true, content: [{ type: "text", text: message }] };
429
+ }
430
+
431
+ function validateRegistryFilter(filter) {
432
+ if (!filter) return null;
433
+ if (registryKinds.includes(filter) || registryTypes.includes(filter))
434
+ return null;
435
+ return `Invalid registry filter "${filter}". Use one of ${[...registryKinds, ...registryTypes].join(", ")}.`;
436
+ }
437
+
438
+ // ── server ─────────────────────────────────────────────────────
439
+
440
+ export function createServer() {
441
+ const server = new McpServer({ name: "nyxui-mcp", version: "1.0.0" });
442
+
443
+ server.tool(
444
+ "listRegistryItems",
445
+ {
446
+ kind: z.string().optional(),
447
+ query: z.string().min(1).optional(),
448
+ limit: z.number().int().min(1).max(50).default(20),
449
+ offset: z.number().int().min(0).default(0),
450
+ },
451
+ async ({ kind, query, limit, offset }) => {
452
+ const validationError = validateRegistryFilter(kind);
453
+ if (validationError) return errorResult(validationError);
454
+ return textResult(
455
+ await listRegistryItemsResult({ filter: kind, query, limit, offset }),
456
+ );
457
+ },
458
+ );
459
+
460
+ server.tool(
461
+ "searchRegistryItems",
462
+ {
463
+ query: z.string().min(1),
464
+ kind: z.string().optional(),
465
+ limit: z.number().int().min(1).max(50).default(10),
466
+ offset: z.number().int().min(0).default(0),
467
+ },
468
+ async ({ query, kind, limit, offset }) => {
469
+ const validationError = validateRegistryFilter(kind);
470
+ if (validationError) return errorResult(validationError);
471
+ return textResult(
472
+ await listRegistryItemsResult({ filter: kind, query, limit, offset }),
473
+ );
474
+ },
475
+ );
476
+
477
+ server.tool(
478
+ "getRegistryItem",
479
+ {
480
+ name: z.string().min(1),
481
+ includeSource: z.boolean().default(false),
482
+ includeExamples: z.boolean().default(true),
483
+ includeRelated: z.boolean().default(true),
484
+ },
485
+ async ({ name, includeSource, includeExamples, includeRelated }) => {
486
+ const registry = await getRegistry();
487
+ const item = registry.items.find((e) => e.name === name);
488
+ if (!item) return errorResult(`Registry item "${name}" not found.`);
489
+ return textResult(
490
+ await buildRegistryItemPayload(item, {
491
+ includeSource,
492
+ includeExamples,
493
+ includeRelated,
494
+ }),
495
+ );
496
+ },
497
+ );
498
+
499
+ server.tool("project_overview", {}, async () => {
500
+ const docs = await getDocs();
501
+ const registry = await getRegistry();
502
+
503
+ const docsBySection = docs.reduce((acc, doc) => {
504
+ acc[doc.section] = (acc[doc.section] ?? 0) + 1;
505
+ return acc;
506
+ }, {});
507
+
508
+ const registryByKind = registry.items.reduce((acc, item) => {
509
+ const kind = getRegistryKind(item);
510
+ acc[kind] = (acc[kind] ?? 0) + 1;
511
+ return acc;
512
+ }, {});
513
+
514
+ return textResult({
515
+ name: "Nyx UI",
516
+ siteUrl,
517
+ docsCount: docs.length,
518
+ registryItemCount: registry.items.length,
519
+ docsBySection,
520
+ registryByKind,
521
+ primaryTools: [
522
+ "listRegistryItems",
523
+ "searchRegistryItems",
524
+ "getRegistryItem",
525
+ ],
526
+ });
527
+ });
528
+
529
+ server.tool(
530
+ "list_docs",
531
+ {
532
+ section: z.enum(["components", "templates", "blocks", "root"]).optional(),
533
+ includeUnpublished: z.boolean().default(false),
534
+ },
535
+ async ({ section, includeUnpublished }) => {
536
+ const docs = await getDocs();
537
+ const filteredDocs = docs
538
+ .filter((doc) => !section || doc.section === section)
539
+ .filter((doc) => includeUnpublished || doc.published);
540
+ return textResult(
541
+ filteredDocs.map((doc) => ({
542
+ slug: doc.slug,
543
+ section: doc.section,
544
+ title: doc.title,
545
+ description: doc.description,
546
+ tags: doc.tags,
547
+ published: doc.published,
548
+ url: getDocUrl(doc),
549
+ })),
550
+ );
551
+ },
552
+ );
553
+
554
+ server.tool("get_doc", { slug: z.string().min(1) }, async ({ slug }) => {
555
+ const docs = await getDocs();
556
+ const doc = docs.find((e) => e.slug === slug.replace(/\.mdx$/, ""));
557
+ if (!doc) return errorResult(`Document not found for slug "${slug}".`);
558
+ return textResult({
559
+ slug: doc.slug,
560
+ section: doc.section,
561
+ title: doc.title,
562
+ description: doc.description,
563
+ frontmatter: doc.frontmatter,
564
+ url: getDocUrl(doc),
565
+ body: doc.body,
566
+ });
567
+ });
568
+
569
+ server.tool(
570
+ "search_docs",
571
+ {
572
+ query: z.string().min(1),
573
+ section: z.enum(["components", "templates", "blocks", "root"]).optional(),
574
+ limit: z.number().int().min(1).max(20).default(5),
575
+ includeUnpublished: z.boolean().default(false),
576
+ },
577
+ async ({ query, section, limit, includeUnpublished }) => {
578
+ const docs = await getDocs();
579
+ const matches = docs
580
+ .filter((doc) => !section || doc.section === section)
581
+ .filter((doc) => includeUnpublished || doc.published)
582
+ .map((doc) => ({ doc, score: scoreDoc(doc, query) }))
583
+ .filter((r) => r.score > 0)
584
+ .sort((a, b) => b.score - a.score)
585
+ .slice(0, limit)
586
+ .map(({ doc, score }) => ({
587
+ slug: doc.slug,
588
+ section: doc.section,
589
+ title: doc.title,
590
+ description: doc.description,
591
+ score,
592
+ url: getDocUrl(doc),
593
+ snippet: buildSnippet(doc.body, query),
594
+ }));
595
+ return textResult(matches);
596
+ },
597
+ );
598
+
599
+ server.resource("catalog", "nyxui://catalog", async (uri) => {
600
+ const docs = await getDocs();
601
+ const registry = await getRegistry();
602
+ return {
603
+ contents: [
604
+ {
605
+ uri: uri.href,
606
+ mimeType: "application/json",
607
+ text: JSON.stringify(
608
+ {
609
+ name: "Nyx UI MCP Catalog",
610
+ primaryTools: [
611
+ "listRegistryItems",
612
+ "searchRegistryItems",
613
+ "getRegistryItem",
614
+ ],
615
+ docs: docs.map((doc) => ({
616
+ slug: doc.slug,
617
+ section: doc.section,
618
+ title: doc.title,
619
+ url: getDocUrl(doc),
620
+ })),
621
+ registryItems: registry.items.map((item) =>
622
+ buildRegistryPreview(item, docs, registry),
623
+ ),
624
+ },
625
+ null,
626
+ 2,
627
+ ),
628
+ },
629
+ ],
630
+ };
631
+ });
632
+
633
+ server.resource(
634
+ "registry-item",
635
+ new ResourceTemplate("nyxui://registry/{name}", { list: undefined }),
636
+ async (uri, { name }) => {
637
+ const registry = await getRegistry();
638
+ const item = registry.items.find((e) => e.name === name);
639
+ if (!item) {
640
+ return {
641
+ contents: [
642
+ {
643
+ uri: uri.href,
644
+ mimeType: "application/json",
645
+ text: JSON.stringify(
646
+ { error: `Registry item "${name}" not found.` },
647
+ null,
648
+ 2,
649
+ ),
650
+ },
651
+ ],
652
+ };
653
+ }
654
+ const publicItem = await getPublicRegistryItem(name);
655
+ return {
656
+ contents: [
657
+ {
658
+ uri: uri.href,
659
+ mimeType: "application/json",
660
+ text: JSON.stringify(publicItem ?? item, null, 2),
661
+ },
662
+ ],
663
+ };
664
+ },
665
+ );
666
+
667
+ return server;
668
+ }
669
+
670
+ export async function startServer() {
671
+ const server = createServer();
672
+ const transport = new StdioServerTransport();
673
+ await server.connect(transport);
674
+ return server;
675
+ }
676
+
677
+ const isEntrypoint =
678
+ process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url;
679
+
680
+ if (isEntrypoint) {
681
+ startServer().catch((error) => {
682
+ console.error("[nyxui-mcp] Failed to start server");
683
+ console.error(error);
684
+ process.exit(1);
685
+ });
686
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "nyxui-mcp",
3
+ "version": "1.0.0",
4
+ "description": "Official MCP server for Nyx UI - Modern React components built with Tailwind CSS and Framer Motion",
5
+ "type": "module",
6
+ "bin": {
7
+ "nyxui-mcp": "./mcp-server.mjs"
8
+ },
9
+ "scripts": {
10
+ "start": "node mcp-server.mjs"
11
+ },
12
+ "dependencies": {
13
+ "@modelcontextprotocol/sdk": "latest",
14
+ "zod": "latest"
15
+ },
16
+ "keywords": [
17
+ "mcp",
18
+ "nyxui",
19
+ "nyx-ui",
20
+ "ui-components",
21
+ "model-context-protocol",
22
+ "react",
23
+ "tailwindcss",
24
+ "framer-motion",
25
+ "nextjs"
26
+ ],
27
+ "author": "Mihir Jaiswal",
28
+ "license": "MIT",
29
+ "homepage": "https://nyxui.com",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/MihirJaiswal/nyxui-mcp"
33
+ },
34
+ "bugs": {
35
+ "url": "https://github.com/MihirJaiswal/nyxui-mcp/issues"
36
+ },
37
+ "engines": {
38
+ "node": ">=18.0.0"
39
+ }
40
+ }
package/readme.md ADDED
@@ -0,0 +1,64 @@
1
+ # nyxui-mcp
2
+
3
+ Official [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) server for [Nyx UI](https://nyxui.com/).
4
+
5
+ ## Installation
6
+
7
+ Add to your MCP client config:
8
+
9
+ ```json
10
+ {
11
+ "mcpServers": {
12
+ "nyxui": {
13
+ "command": "npx",
14
+ "args": ["-y", "nyxui-mcp@latest"]
15
+ }
16
+ }
17
+ }
18
+ ```
19
+
20
+ ### Supported Clients
21
+
22
+ - Claude Desktop
23
+ - Cursor
24
+ - Windsurf
25
+ - Cline
26
+
27
+ ## Example Usage
28
+
29
+ Once configured, you can ask things like:
30
+
31
+ > "Install the animated code block from Nyx UI"
32
+
33
+ > "Show me all Nyx UI components"
34
+
35
+ > "How do I use the Apple Glass Effect component?"
36
+
37
+ > "Find me a card component from Nyx UI"
38
+
39
+ ## Available Tools
40
+
41
+ | Tool | Description |
42
+ | --------------------- | ---------------------------------------------------------------- |
43
+ | `listRegistryItems` | Browse all Nyx UI components with optional filters |
44
+ | `searchRegistryItems` | Search components by keyword |
45
+ | `getRegistryItem` | Get details, install command and source for a specific component |
46
+ | `search_docs` | Search Nyx UI documentation |
47
+ | `get_doc` | Get a specific documentation page |
48
+ | `list_docs` | List all documentation pages |
49
+ | `project_overview` | Get a high level overview of Nyx UI |
50
+
51
+ ## Links
52
+
53
+ - [Nyx UI Website](https://nyxui.com)
54
+ - [Components](https://nyxui.com/components)
55
+ - [Documentation](https://nyxui.com/docs)
56
+ - [GitHub](https://github.com/MihirJaiswal/nyxui)
57
+
58
+ ## Author
59
+
60
+ Made by [Mihir Jaiswal](https://x.com/mihir_jaiswal_)
61
+
62
+ ## License
63
+
64
+ MIT