ply-ui-mcp 0.1.7

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,1165 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/index.ts
27
+ var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
28
+
29
+ // src/catalog.ts
30
+ var import_mcp2 = require("@modelcontextprotocol/sdk/server/mcp.js");
31
+
32
+ // src/resources.ts
33
+ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
34
+
35
+ // src/registry.ts
36
+ var import_zod = require("zod");
37
+ var SITE_URL = "https://ply-ui.com";
38
+ var env = typeof process !== "undefined" && process.env ? process.env : {};
39
+ function firstEnv(...names) {
40
+ for (const name of names) {
41
+ const v = env[name]?.trim();
42
+ if (v) return v;
43
+ }
44
+ }
45
+ var FREE_REGISTRY_URL = firstEnv("PLY_REGISTRY_URL", "BASE_UI_REGISTRY_URL") || `${SITE_URL}/registry`;
46
+ var PRO_REGISTRY_URL = firstEnv("PLY_PRO_REGISTRY_URL", "BASE_UI_PRO_REGISTRY_URL") || "https://pro.ply-ui.com/registry";
47
+ var registryFileSchema = import_zod.z.object({
48
+ name: import_zod.z.string(),
49
+ content: import_zod.z.string()
50
+ });
51
+ var registryItemSchema = import_zod.z.object({
52
+ name: import_zod.z.string(),
53
+ tier: import_zod.z.enum(["free", "pro"]),
54
+ category: import_zod.z.enum(["application", "page-layout", "block", "widget", "shell", "component"]).optional().default("component"),
55
+ dependencies: import_zod.z.array(import_zod.z.string()).default([]),
56
+ registryDependencies: import_zod.z.array(import_zod.z.string()).default([]),
57
+ description: import_zod.z.string().optional().default(""),
58
+ usage: import_zod.z.string().optional().default(""),
59
+ keywords: import_zod.z.array(import_zod.z.string()).optional().default([]),
60
+ files: import_zod.z.array(registryFileSchema),
61
+ type: import_zod.z.string().optional()
62
+ });
63
+ var registryIndexSchema = import_zod.z.array(registryItemSchema.omit({ files: true }));
64
+ function itemHaystack(item) {
65
+ return [item.name, item.description, item.usage, ...item.keywords ?? []].filter(Boolean).join(" ").toLowerCase();
66
+ }
67
+ function isSafeItemName(name) {
68
+ return /^[a-z0-9][a-z0-9-.]*$/i.test(name) && !name.includes("..");
69
+ }
70
+ async function fetchJson(url, headers = {}) {
71
+ let res;
72
+ try {
73
+ res = await fetch(url, { headers });
74
+ } catch (err) {
75
+ throw new Error(
76
+ `Network error while contacting the registry (${err.message}). Check your connection and try again.`
77
+ );
78
+ }
79
+ if (res.status === 401 || res.status === 403) {
80
+ throw new Error(
81
+ `License rejected (HTTP ${res.status}). Check PLY_LICENSE_KEY, or see ${SITE_URL}/pricing.`
82
+ );
83
+ }
84
+ if (res.status === 404) {
85
+ const e = new Error("Not found");
86
+ e.code = "NOT_FOUND";
87
+ throw e;
88
+ }
89
+ if (!res.ok) {
90
+ throw new Error(`Registry request failed (HTTP ${res.status}) for ${url}.`);
91
+ }
92
+ return res.json();
93
+ }
94
+ async function fetchIndex() {
95
+ const data = await fetchJson(`${FREE_REGISTRY_URL}/index.json`);
96
+ return registryIndexSchema.parse(data);
97
+ }
98
+ async function fetchFreeItem(name) {
99
+ if (!isSafeItemName(name)) {
100
+ throw new Error(`Invalid component name: '${name}'.`);
101
+ }
102
+ const data = await fetchJson(`${FREE_REGISTRY_URL}/styles/default/${name}.json`);
103
+ return registryItemSchema.parse(data);
104
+ }
105
+ async function fetchText(url) {
106
+ let res;
107
+ try {
108
+ res = await fetch(url);
109
+ } catch (err) {
110
+ throw new Error(`Network error fetching ${url}: ${err.message}`);
111
+ }
112
+ if (!res.ok) {
113
+ throw new Error(`Failed to fetch ${url} (HTTP ${res.status}).`);
114
+ }
115
+ return res.text();
116
+ }
117
+ function suggestNames(query, names, limit = 8) {
118
+ const q = query.toLowerCase().trim();
119
+ if (!q) return [];
120
+ const scored = names.map((name) => {
121
+ const n = name.toLowerCase();
122
+ let score;
123
+ if (n === q) score = 0;
124
+ else if (n.startsWith(q) || q.startsWith(n)) score = 1;
125
+ else if (n.includes(q) || q.includes(n)) score = 2;
126
+ else score = 3 + levenshtein(q, n);
127
+ return { name, score };
128
+ }).filter((s) => s.score <= 6).sort((a, b) => a.score - b.score || a.name.localeCompare(b.name));
129
+ return scored.slice(0, limit).map((s) => s.name);
130
+ }
131
+ function levenshtein(a, b) {
132
+ const m = a.length;
133
+ const n = b.length;
134
+ if (!m) return n;
135
+ if (!n) return m;
136
+ let prev = Array.from({ length: n + 1 }, (_, i) => i);
137
+ for (let i = 1; i <= m; i++) {
138
+ const curr = [i];
139
+ for (let j = 1; j <= n; j++) {
140
+ curr[j] = Math.min(
141
+ prev[j] + 1,
142
+ curr[j - 1] + 1,
143
+ prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)
144
+ );
145
+ }
146
+ prev = curr;
147
+ }
148
+ return prev[n];
149
+ }
150
+
151
+ // src/utf8.ts
152
+ function utf8ByteLength(text) {
153
+ return new TextEncoder().encode(text).byteLength;
154
+ }
155
+
156
+ // src/resources.ts
157
+ function registerResources(server) {
158
+ server.registerResource(
159
+ "registry-index",
160
+ "ply-ui://registry/index",
161
+ {
162
+ title: "Registry index",
163
+ description: "Public Ply registry index (names, tiers, categories, description, usage, keywords, deps)",
164
+ mimeType: "application/json"
165
+ },
166
+ async (uri) => {
167
+ const index = await fetchIndex();
168
+ return {
169
+ contents: [
170
+ {
171
+ uri: uri.href,
172
+ mimeType: "application/json",
173
+ text: JSON.stringify(index, null, 2)
174
+ }
175
+ ]
176
+ };
177
+ }
178
+ );
179
+ server.registerResource(
180
+ "docs-llms",
181
+ "ply-ui://docs/llms",
182
+ {
183
+ title: "llms.txt",
184
+ description: "Agent-oriented product overview (keep fetching this URL even when MCP is available)",
185
+ mimeType: "text/plain"
186
+ },
187
+ async (uri) => {
188
+ const text = await fetchText(`${SITE_URL}/llms.txt`);
189
+ return {
190
+ contents: [{ uri: uri.href, mimeType: "text/plain", text }]
191
+ };
192
+ }
193
+ );
194
+ server.registerResource(
195
+ "docs-catalog",
196
+ "ply-ui://docs/catalog",
197
+ {
198
+ title: "AI component catalog",
199
+ description: "docs/ai/components.md \u2014 detailed component reference for agents",
200
+ mimeType: "text/markdown"
201
+ },
202
+ async (uri) => {
203
+ const text = await fetchText(`${SITE_URL}/docs/ai/components.md`);
204
+ return {
205
+ contents: [{ uri: uri.href, mimeType: "text/markdown", text }]
206
+ };
207
+ }
208
+ );
209
+ server.registerResource(
210
+ "docs-cursor-rules",
211
+ "ply-ui://docs/cursor-rules",
212
+ {
213
+ title: "Cursor / Windsurf project rules",
214
+ description: "Save as .cursor/rules/base-ui.mdc (or .windsurf/rules/base-ui.md). Teaches assistants to write standalone zoneless Angular 22 with Ply without running MCP.",
215
+ mimeType: "text/markdown"
216
+ },
217
+ async (uri) => {
218
+ const text = await fetchText(`${SITE_URL}/docs/ai/base-ui.mdc`);
219
+ return {
220
+ contents: [{ uri: uri.href, mimeType: "text/markdown", text }]
221
+ };
222
+ }
223
+ );
224
+ server.registerResource(
225
+ "docs-snippets",
226
+ "ply-ui://docs/snippets",
227
+ {
228
+ title: "VS Code / Cursor snippets",
229
+ description: "Save as .vscode/base-ui.code-snippets. Prefixes like ply-card expand usage markup after CLI install.",
230
+ mimeType: "application/json"
231
+ },
232
+ async (uri) => {
233
+ const text = await fetchText(`${SITE_URL}/docs/ai/base-ui.code-snippets`);
234
+ return {
235
+ contents: [{ uri: uri.href, mimeType: "application/json", text }]
236
+ };
237
+ }
238
+ );
239
+ server.registerResource(
240
+ "component",
241
+ new import_mcp.ResourceTemplate("ply-ui://component/{name}", {
242
+ list: void 0
243
+ }),
244
+ {
245
+ title: "Component metadata",
246
+ description: "Registry metadata for one component. Free source files are listed without content; Pro never streams source via MCP in V1.",
247
+ mimeType: "application/json"
248
+ },
249
+ async (uri, vars) => {
250
+ const name = String(vars.name || "");
251
+ if (!isSafeItemName(name)) {
252
+ return {
253
+ contents: [
254
+ {
255
+ uri: uri.href,
256
+ mimeType: "application/json",
257
+ text: JSON.stringify({ error: `Invalid name: ${name}` })
258
+ }
259
+ ]
260
+ };
261
+ }
262
+ const index = await fetchIndex();
263
+ const entry = index.find((i) => i.name === name);
264
+ if (!entry) {
265
+ return {
266
+ contents: [
267
+ {
268
+ uri: uri.href,
269
+ mimeType: "application/json",
270
+ text: JSON.stringify({ error: `Unknown component: ${name}` })
271
+ }
272
+ ]
273
+ };
274
+ }
275
+ if (entry.tier === "pro") {
276
+ return {
277
+ contents: [
278
+ {
279
+ uri: uri.href,
280
+ mimeType: "application/json",
281
+ text: JSON.stringify(
282
+ {
283
+ ...entry,
284
+ files: null,
285
+ note: `Pro \u2014 metadata only. ${SITE_URL}/pricing`
286
+ },
287
+ null,
288
+ 2
289
+ )
290
+ }
291
+ ]
292
+ };
293
+ }
294
+ const item = await fetchFreeItem(name);
295
+ return {
296
+ contents: [
297
+ {
298
+ uri: uri.href,
299
+ mimeType: "application/json",
300
+ text: JSON.stringify(
301
+ {
302
+ name: item.name,
303
+ tier: item.tier,
304
+ category: item.category,
305
+ description: item.description,
306
+ usage: item.usage,
307
+ keywords: item.keywords,
308
+ dependencies: item.dependencies,
309
+ registryDependencies: item.registryDependencies,
310
+ files: item.files.map((f) => ({
311
+ path: f.name,
312
+ bytes: utf8ByteLength(f.content)
313
+ }))
314
+ },
315
+ null,
316
+ 2
317
+ )
318
+ }
319
+ ]
320
+ };
321
+ }
322
+ );
323
+ }
324
+
325
+ // src/tools/get-component.ts
326
+ var import_zod2 = require("zod");
327
+
328
+ // src/brand.ts
329
+ var CLI_PACKAGE = "ply-ui-cli";
330
+ var CLI_BIN = "ply-ui-cli";
331
+ var CLI_NPX = `npx ${CLI_BIN}`;
332
+ var MCP_PACKAGE = "ply-ui-mcp";
333
+ var MCP_NPX = `npx -y ${MCP_PACKAGE}`;
334
+ var MCP_SERVER_NAME = "ply";
335
+
336
+ // src/install-hint.ts
337
+ function cliAddCommand(name) {
338
+ return name?.trim() ? `${CLI_NPX} add ${name.trim()}` : `${CLI_NPX} add <name>`;
339
+ }
340
+ function installHint(name) {
341
+ return `This catalog does not write files. Install in the project with \`${cliAddCommand(name)}\`. Local stdio MCP (Cursor, Claude Code) can call add_components instead.`;
342
+ }
343
+
344
+ // src/result.ts
345
+ function okJson(data) {
346
+ return {
347
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
348
+ };
349
+ }
350
+ function failJson(message, extra = {}) {
351
+ return {
352
+ isError: true,
353
+ content: [
354
+ {
355
+ type: "text",
356
+ text: JSON.stringify({ ok: false, error: message, ...extra }, null, 2)
357
+ }
358
+ ]
359
+ };
360
+ }
361
+
362
+ // src/tools/get-component.ts
363
+ var getComponentInput = {
364
+ name: import_zod2.z.string().describe("Registry component name (e.g. button, card)"),
365
+ includeSource: import_zod2.z.boolean().optional().default(false).describe(
366
+ "If true, include free-tier source file contents. Pro source is never returned via MCP in V1."
367
+ )
368
+ };
369
+ async function getComponent(args) {
370
+ try {
371
+ const name = args.name.trim();
372
+ if (!isSafeItemName(name)) {
373
+ return failJson(`Invalid component name: '${name}'.`);
374
+ }
375
+ const index = await fetchIndex();
376
+ const entry = index.find((i) => i.name === name);
377
+ if (!entry) {
378
+ const suggestions = suggestNames(
379
+ name,
380
+ index.map((i) => i.name)
381
+ );
382
+ return failJson(`Unknown component: '${name}'.`, {
383
+ suggestions,
384
+ listHint: "Call list_components or search_components."
385
+ });
386
+ }
387
+ const base = {
388
+ ok: true,
389
+ name: entry.name,
390
+ tier: entry.tier,
391
+ category: entry.category || "component",
392
+ description: entry.description || "",
393
+ usage: entry.usage || "",
394
+ keywords: entry.keywords || [],
395
+ dependencies: entry.dependencies,
396
+ registryDependencies: entry.registryDependencies,
397
+ docsUrl: `${SITE_URL}`,
398
+ pricingUrl: `${SITE_URL}/pricing`
399
+ };
400
+ if (entry.tier === "pro") {
401
+ return okJson({
402
+ ...base,
403
+ source: null,
404
+ installCommand: cliAddCommand(name),
405
+ note: `Pro component \u2014 metadata only. ${installHint(name)} Pro install also needs PLY_LICENSE_KEY. Buy at ${SITE_URL}/pricing.`
406
+ });
407
+ }
408
+ const item = await fetchFreeItem(name);
409
+ const files = item.files.map((f) => ({
410
+ path: f.name,
411
+ bytes: utf8ByteLength(f.content),
412
+ ...args.includeSource ? { content: f.content } : {}
413
+ }));
414
+ return okJson({
415
+ ...base,
416
+ files,
417
+ includeSource: Boolean(args.includeSource),
418
+ installCommand: cliAddCommand(name),
419
+ note: args.includeSource ? `Free source included for reading. ${installHint(name)}` : `Source omitted by default. Pass includeSource=true to read free files here. ${installHint(name)}`
420
+ });
421
+ } catch (err) {
422
+ return failJson(err.message || String(err));
423
+ }
424
+ }
425
+
426
+ // src/tools/list-components.ts
427
+ var import_zod3 = require("zod");
428
+ var CATEGORIES = [
429
+ "application",
430
+ "page-layout",
431
+ "block",
432
+ "widget",
433
+ "shell",
434
+ "component"
435
+ ];
436
+ var listComponentsInput = {
437
+ tier: import_zod3.z.enum(["free", "pro", "all"]).optional().default("all").describe("Filter by license tier"),
438
+ category: import_zod3.z.enum(CATEGORIES).optional().describe("Filter by registry category"),
439
+ q: import_zod3.z.string().optional().describe("Substring filter on name, description, usage, or keywords (case-insensitive)")
440
+ };
441
+ async function listComponents(args) {
442
+ try {
443
+ const index = await fetchIndex();
444
+ const tier = args.tier ?? "all";
445
+ const q = args.q?.trim().toLowerCase();
446
+ let items = index;
447
+ if (tier !== "all") {
448
+ items = items.filter((i) => i.tier === tier);
449
+ }
450
+ if (args.category) {
451
+ items = items.filter((i) => (i.category || "component") === args.category);
452
+ }
453
+ if (q) {
454
+ items = items.filter((i) => itemHaystack(i).includes(q));
455
+ }
456
+ items = [...items].sort((a, b) => a.name.localeCompare(b.name));
457
+ return okJson({
458
+ ok: true,
459
+ count: items.length,
460
+ items: items.map((i) => ({
461
+ name: i.name,
462
+ tier: i.tier,
463
+ category: i.category || "component",
464
+ description: i.description || "",
465
+ usage: i.usage || "",
466
+ keywords: i.keywords || [],
467
+ dependencies: i.dependencies,
468
+ registryDependencies: i.registryDependencies
469
+ })),
470
+ hint: `Use get_component for details. ${installHint()}`
471
+ });
472
+ } catch (err) {
473
+ return failJson(err.message || String(err));
474
+ }
475
+ }
476
+
477
+ // src/tools/search-components.ts
478
+ var import_zod4 = require("zod");
479
+
480
+ // src/aliases.ts
481
+ var COMPONENT_ALIASES = {
482
+ sheet: "drawer",
483
+ "side-sheet": "drawer",
484
+ carousel: "slider",
485
+ gallery: "slider",
486
+ "image-slider": "slider",
487
+ range: "range-slider",
488
+ "slider-input": "range-slider",
489
+ "input-slider": "range-slider"
490
+ };
491
+ function canonicalComponentName(name) {
492
+ const key = name.trim().toLowerCase();
493
+ return COMPONENT_ALIASES[key] ?? name.trim();
494
+ }
495
+
496
+ // src/tools/search-components.ts
497
+ var CATEGORIES2 = [
498
+ "application",
499
+ "page-layout",
500
+ "block",
501
+ "widget",
502
+ "shell",
503
+ "component"
504
+ ];
505
+ var searchComponentsInput = {
506
+ query: import_zod4.z.string().min(1).describe('Natural-language or substring query (e.g. "dialog modal", "button"). Matches name, description, usage, and keywords.'),
507
+ tier: import_zod4.z.enum(["free", "pro", "all"]).optional().default("all").describe("Filter by license tier"),
508
+ category: import_zod4.z.enum(CATEGORIES2).optional().describe("Filter by registry category"),
509
+ limit: import_zod4.z.number().int().min(1).max(40).optional().default(12)
510
+ };
511
+ function tokenize(query) {
512
+ return query.toLowerCase().split(/[^a-z0-9]+/i).map((t) => t.trim()).filter((t) => t.length >= 2);
513
+ }
514
+ async function searchComponents(args) {
515
+ try {
516
+ const index = await fetchIndex();
517
+ const tier = args.tier ?? "all";
518
+ const limit = args.limit ?? 12;
519
+ const rawQuery = args.query.trim();
520
+ const canonical = canonicalComponentName(rawQuery);
521
+ const variants = [...new Set([rawQuery, canonical].filter(Boolean))];
522
+ let pool = index;
523
+ if (tier !== "all") {
524
+ pool = pool.filter((i) => i.tier === tier);
525
+ }
526
+ if (args.category) {
527
+ pool = pool.filter((i) => (i.category || "component") === args.category);
528
+ }
529
+ const names = pool.map((i) => i.name);
530
+ const byFuzzy = new Set(variants.flatMap((q) => suggestNames(q, names, limit)));
531
+ const byTokens = pool.filter((i) => {
532
+ const hay = itemHaystack(i);
533
+ return variants.some((q) => {
534
+ const tokens = tokenize(q);
535
+ return tokens.length > 0 && tokens.every((t) => hay.includes(t));
536
+ });
537
+ });
538
+ const ranked = [
539
+ ...byTokens.sort((a, b) => a.name.localeCompare(b.name)),
540
+ ...pool.filter((i) => byFuzzy.has(i.name) && !byTokens.some((t) => t.name === i.name)).sort(
541
+ (a, b) => [...byFuzzy].indexOf(a.name) - [...byFuzzy].indexOf(b.name)
542
+ )
543
+ ].slice(0, limit);
544
+ return okJson({
545
+ ok: true,
546
+ query: args.query,
547
+ count: ranked.length,
548
+ matches: ranked.map((i) => ({
549
+ name: i.name,
550
+ tier: i.tier,
551
+ category: i.category || "component",
552
+ description: i.description || "",
553
+ usage: i.usage || "",
554
+ keywords: i.keywords || [],
555
+ registryDependencies: i.registryDependencies
556
+ })),
557
+ hint: `Next: get_component for details. ${installHint()}`
558
+ });
559
+ } catch (err) {
560
+ return failJson(err.message || String(err));
561
+ }
562
+ }
563
+
564
+ // src/version.ts
565
+ var PACKAGE_VERSION = "0.1.0";
566
+
567
+ // src/catalog.ts
568
+ var catalogAnnotations = {
569
+ readOnlyHint: true,
570
+ destructiveHint: false,
571
+ idempotentHint: true,
572
+ openWorldHint: true
573
+ };
574
+ function createCatalogMcpServer() {
575
+ const server = new import_mcp2.McpServer({
576
+ name: MCP_SERVER_NAME,
577
+ version: PACKAGE_VERSION
578
+ });
579
+ server.registerTool(
580
+ "list_components",
581
+ {
582
+ title: "List Ply components",
583
+ description: `List components from the Ply registry. Filter by tier (free|pro|all), category, or name substring. Does not install anything. To copy a component into a project, tell the user to run ${CLI_NPX} add <name>.`,
584
+ inputSchema: listComponentsInput,
585
+ annotations: catalogAnnotations
586
+ },
587
+ async (args) => listComponents(args)
588
+ );
589
+ server.registerTool(
590
+ "get_component",
591
+ {
592
+ title: "Get component details",
593
+ description: `Get registry metadata for one component (deps, category, file list). Free source only when includeSource=true. Pro source is never streamed via MCP. Does not install; returns installCommand (${CLI_NPX} add <name>).`,
594
+ inputSchema: getComponentInput,
595
+ annotations: catalogAnnotations
596
+ },
597
+ async (args) => getComponent(args)
598
+ );
599
+ server.registerTool(
600
+ "search_components",
601
+ {
602
+ title: "Search Ply components",
603
+ description: `Fuzzy / token search over registry component names (e.g. "dialog", "sheet", "carousel"). Does not install anything. Prefer this before guessing names, then give the user ${CLI_NPX} add <name> (or local stdio add_components).`,
604
+ inputSchema: searchComponentsInput,
605
+ annotations: catalogAnnotations
606
+ },
607
+ async (args) => searchComponents(args)
608
+ );
609
+ registerResources(server);
610
+ return server;
611
+ }
612
+
613
+ // src/tools/add-components.ts
614
+ var import_zod5 = require("zod");
615
+
616
+ // src/cli.ts
617
+ var import_fs = __toESM(require("fs"));
618
+ var import_path = __toESM(require("path"));
619
+ var import_child_process = require("child_process");
620
+ function resolveCliEntrypoint() {
621
+ const envPath = process.env.PLY_CLI_PATH?.trim() || process.env.BASE_UI_CLI_PATH?.trim();
622
+ if (envPath && import_fs.default.existsSync(envPath)) {
623
+ return { command: process.execPath, argsPrefix: [envPath] };
624
+ }
625
+ const sibling = import_path.default.resolve(__dirname, "../../cli/dist/index.js");
626
+ if (import_fs.default.existsSync(sibling)) {
627
+ return { command: process.execPath, argsPrefix: [sibling] };
628
+ }
629
+ for (const pkg of [CLI_PACKAGE, "base-ui-cli"]) {
630
+ try {
631
+ const pkgJson = require.resolve(`${pkg}/package.json`);
632
+ const bin = import_path.default.join(import_path.default.dirname(pkgJson), "dist", "index.js");
633
+ if (import_fs.default.existsSync(bin)) {
634
+ return { command: process.execPath, argsPrefix: [bin] };
635
+ }
636
+ } catch {
637
+ }
638
+ }
639
+ return { command: process.platform === "win32" ? "npx.cmd" : "npx", argsPrefix: ["-y", CLI_PACKAGE] };
640
+ }
641
+ function runCli(cliArgs, options = { cwd: process.cwd() }) {
642
+ const { command, argsPrefix } = resolveCliEntrypoint();
643
+ const args = [...argsPrefix, ...cliArgs];
644
+ const timeoutMs = options.timeoutMs ?? 12e4;
645
+ return new Promise((resolve) => {
646
+ const child = (0, import_child_process.spawn)(command, args, {
647
+ cwd: options.cwd,
648
+ env: { ...process.env, ...options.env, FORCE_COLOR: "0" }
649
+ });
650
+ let stdout = "";
651
+ let stderr = "";
652
+ let settled = false;
653
+ const timer = setTimeout(() => {
654
+ if (settled) return;
655
+ settled = true;
656
+ child.kill("SIGTERM");
657
+ resolve({
658
+ ok: false,
659
+ code: null,
660
+ stdout,
661
+ stderr: stderr + `
662
+ Timed out after ${timeoutMs}ms`
663
+ });
664
+ }, timeoutMs);
665
+ child.stdout.on("data", (chunk) => {
666
+ stdout += chunk.toString("utf8");
667
+ });
668
+ child.stderr.on("data", (chunk) => {
669
+ stderr += chunk.toString("utf8");
670
+ });
671
+ child.on("error", (err) => {
672
+ if (settled) return;
673
+ settled = true;
674
+ clearTimeout(timer);
675
+ resolve({ ok: false, code: null, stdout, stderr: err.message });
676
+ });
677
+ child.on("close", (code) => {
678
+ if (settled) return;
679
+ settled = true;
680
+ clearTimeout(timer);
681
+ resolve({ ok: code === 0, code, stdout, stderr });
682
+ });
683
+ });
684
+ }
685
+ function resolveProjectCwd(cwd) {
686
+ const root = cwd?.trim() || process.env.PLY_CWD?.trim() || process.env.BASE_UI_CWD?.trim() || process.cwd();
687
+ return import_path.default.resolve(root);
688
+ }
689
+
690
+ // src/tools/add-components.ts
691
+ var addComponentsInput = {
692
+ names: import_zod5.z.array(import_zod5.z.string().min(1)).min(1).describe("One or more registry component names to install (aliases like sheet \u2192 drawer are resolved)"),
693
+ cwd: import_zod5.z.string().optional().describe(
694
+ "Project root containing base-ui.json (defaults to MCP process cwd / PLY_CWD / BASE_UI_CWD)"
695
+ ),
696
+ overwrite: import_zod5.z.boolean().optional().default(false).describe("Overwrite existing component folders (maps to CLI --overwrite)"),
697
+ licenseKey: import_zod5.z.string().optional().describe(
698
+ "Ply Pro license. Prefer PLY_LICENSE_KEY (or BASE_UI_LICENSE_KEY) in the MCP server env instead of putting a key in chat."
699
+ )
700
+ };
701
+ async function addComponents(args) {
702
+ try {
703
+ const cwd = resolveProjectCwd(args.cwd);
704
+ const names = [
705
+ ...new Set(
706
+ args.names.map((n) => canonicalComponentName(n.trim())).filter(Boolean)
707
+ )
708
+ ];
709
+ if (!names.length) {
710
+ return failJson("Provide at least one component name.");
711
+ }
712
+ for (const name of names) {
713
+ if (!isSafeItemName(name)) {
714
+ return failJson(`Invalid component name: '${name}'.`);
715
+ }
716
+ }
717
+ const index = await fetchIndex();
718
+ const byName = new Map(index.map((i) => [i.name, i]));
719
+ const unknown = [];
720
+ const pro = [];
721
+ const free = [];
722
+ for (const name of names) {
723
+ const entry = byName.get(name);
724
+ if (!entry) {
725
+ unknown.push(name);
726
+ continue;
727
+ }
728
+ if (entry.tier === "pro") {
729
+ pro.push(name);
730
+ } else {
731
+ free.push(name);
732
+ }
733
+ }
734
+ if (unknown.length) {
735
+ return failJson("Unknown component name(s).", {
736
+ unknown,
737
+ suggestions: unknown.flatMap(
738
+ (n) => suggestNames(
739
+ n,
740
+ index.map((i) => i.name),
741
+ 3
742
+ )
743
+ )
744
+ });
745
+ }
746
+ const licenseKey = args.licenseKey?.trim() || process.env.PLY_LICENSE_KEY?.trim() || process.env.BASE_UI_LICENSE_KEY?.trim() || "";
747
+ if (pro.length && !licenseKey) {
748
+ return failJson(
749
+ `Pro components need PLY_LICENSE_KEY (or licenseKey): ${pro.join(", ")}.`,
750
+ {
751
+ pro,
752
+ pricingUrl: `${SITE_URL}/pricing`,
753
+ note: "Set PLY_LICENSE_KEY on the MCP server process (BASE_UI_LICENSE_KEY still works), then retry. Free components can still be added with this tool.",
754
+ freeCandidates: free
755
+ }
756
+ );
757
+ }
758
+ const queue = [...free];
759
+ const seen = /* @__PURE__ */ new Set();
760
+ const proDeps = [];
761
+ while (queue.length) {
762
+ const name = queue.shift();
763
+ if (seen.has(name)) continue;
764
+ seen.add(name);
765
+ const entry = byName.get(name);
766
+ if (!entry) continue;
767
+ if (entry.tier === "pro") {
768
+ proDeps.push(name);
769
+ continue;
770
+ }
771
+ queue.push(...entry.registryDependencies);
772
+ }
773
+ if (proDeps.length && !licenseKey) {
774
+ return failJson(
775
+ `Install blocked: free selection depends on Pro components: ${proDeps.join(", ")}.`,
776
+ {
777
+ proDependencies: proDeps,
778
+ pricingUrl: `${SITE_URL}/pricing`
779
+ }
780
+ );
781
+ }
782
+ const toInstall = [...free, ...pro];
783
+ const cliArgs = ["add", ...toInstall, "--yes"];
784
+ if (args.overwrite) cliArgs.push("--overwrite");
785
+ const result = await runCli(cliArgs, {
786
+ cwd,
787
+ env: licenseKey ? { PLY_LICENSE_KEY: licenseKey, BASE_UI_LICENSE_KEY: licenseKey } : void 0
788
+ });
789
+ if (!result.ok) {
790
+ return failJson(`${CLI_BIN} add failed.`, {
791
+ cwd,
792
+ names: toInstall,
793
+ exitCode: result.code,
794
+ stdout: result.stdout.trim(),
795
+ stderr: result.stderr.trim(),
796
+ hint: `Ensure the project was initialized (${CLI_NPX} init --yes). CLI remains the canonical install path.`
797
+ });
798
+ }
799
+ return okJson({
800
+ ok: true,
801
+ cwd,
802
+ installed: toInstall,
803
+ cli: `${CLI_BIN} add --yes`,
804
+ stdout: result.stdout.trim(),
805
+ stderr: result.stderr.trim() || void 0,
806
+ note: `Files were written by ${CLI_BIN} (canonical installer). MCP does not replace the CLI.`
807
+ });
808
+ } catch (err) {
809
+ return failJson(err.message || String(err));
810
+ }
811
+ }
812
+
813
+ // src/tools/diff-components.ts
814
+ var import_zod6 = require("zod");
815
+ var diffComponentsInput = {
816
+ names: import_zod6.z.array(import_zod6.z.string().min(1)).optional().describe(
817
+ "Tracked component names to diff. Omit or pass [] to check every component in base-ui-lock.json."
818
+ ),
819
+ cwd: import_zod6.z.string().optional().describe(
820
+ "Project root containing base-ui.json (defaults to MCP process cwd / PLY_CWD)"
821
+ )
822
+ };
823
+ async function diffComponents(args = {}) {
824
+ try {
825
+ const cwd = resolveProjectCwd(args.cwd);
826
+ const names = (args.names ?? []).map((n) => n.trim()).filter(Boolean);
827
+ for (const name of names) {
828
+ if (!isSafeItemName(name)) {
829
+ return failJson(`Invalid component name: '${name}'.`);
830
+ }
831
+ }
832
+ const cliArgs = names.length ? ["diff", ...names] : ["diff"];
833
+ const result = await runCli(cliArgs, { cwd });
834
+ if (!result.ok) {
835
+ return failJson(`${CLI_BIN} diff failed.`, {
836
+ cwd,
837
+ names,
838
+ exitCode: result.code,
839
+ stdout: result.stdout.trim(),
840
+ stderr: result.stderr.trim(),
841
+ hint: "Project must be initialized and components tracked in base-ui-lock.json."
842
+ });
843
+ }
844
+ return okJson({
845
+ ok: true,
846
+ cwd,
847
+ names: names.length ? names : "all-tracked",
848
+ cli: names.length ? `${CLI_BIN} diff ${names.join(" ")}` : `${CLI_BIN} diff`,
849
+ stdout: result.stdout.trim(),
850
+ stderr: result.stderr.trim() || void 0,
851
+ note: "Read-only. Use update_components to apply upstream changes."
852
+ });
853
+ } catch (err) {
854
+ return failJson(err.message || String(err));
855
+ }
856
+ }
857
+
858
+ // src/tools/doctor.ts
859
+ var import_zod7 = require("zod");
860
+ var doctorInput = {
861
+ cwd: import_zod7.z.string().optional().describe(
862
+ "Project root containing base-ui.json (defaults to MCP process cwd / PLY_CWD). Read-only."
863
+ )
864
+ };
865
+ async function doctor(args = {}) {
866
+ try {
867
+ const cwd = resolveProjectCwd(args.cwd);
868
+ const result = await runCli(["doctor"], { cwd });
869
+ const body = {
870
+ cwd,
871
+ cli: `${CLI_BIN} doctor`,
872
+ exitCode: result.code,
873
+ stdout: result.stdout.trim(),
874
+ stderr: result.stderr.trim() || void 0
875
+ };
876
+ if (!result.ok) {
877
+ return failJson(`${CLI_BIN} doctor reported problems.`, body);
878
+ }
879
+ return okJson({ ok: true, ...body });
880
+ } catch (err) {
881
+ return failJson(err.message || String(err));
882
+ }
883
+ }
884
+
885
+ // src/tools/init-project.ts
886
+ var import_zod8 = require("zod");
887
+ var initProjectInput = {
888
+ cwd: import_zod8.z.string().optional().describe(
889
+ "Project root to initialize (defaults to MCP process cwd / PLY_CWD). Always non-interactive (`init --yes`)."
890
+ )
891
+ };
892
+ async function initProject(args = {}) {
893
+ try {
894
+ const cwd = resolveProjectCwd(args.cwd);
895
+ const result = await runCli(["init", "--yes"], { cwd });
896
+ if (!result.ok) {
897
+ return failJson(`${CLI_BIN} init failed.`, {
898
+ cwd,
899
+ exitCode: result.code,
900
+ stdout: result.stdout.trim(),
901
+ stderr: result.stderr.trim(),
902
+ hint: `CLI remains the canonical installer: ${CLI_NPX} init --yes`
903
+ });
904
+ }
905
+ return okJson({
906
+ ok: true,
907
+ cwd,
908
+ cli: `${CLI_BIN} init --yes`,
909
+ stdout: result.stdout.trim(),
910
+ stderr: result.stderr.trim() || void 0
911
+ });
912
+ } catch (err) {
913
+ return failJson(err.message || String(err));
914
+ }
915
+ }
916
+
917
+ // src/tools/update-components.ts
918
+ var import_zod9 = require("zod");
919
+ var updateComponentsInput = {
920
+ names: import_zod9.z.array(import_zod9.z.string().min(1)).optional().describe(
921
+ "Tracked component names to update. Omit or pass [] to update every component in base-ui-lock.json."
922
+ ),
923
+ cwd: import_zod9.z.string().optional().describe(
924
+ "Project root containing base-ui.json (defaults to MCP process cwd / PLY_CWD)"
925
+ ),
926
+ force: import_zod9.z.boolean().optional().default(false).describe(
927
+ "On conflict (both local and upstream changed), take the upstream version. Does not revert local-only edits."
928
+ ),
929
+ overwriteLocal: import_zod9.z.boolean().optional().default(false).describe(
930
+ "Also revert files you edited that upstream did not change (`--overwrite-local`). Default skips those files."
931
+ ),
932
+ merge: import_zod9.z.boolean().optional().default(false).describe(
933
+ "On conflict, apply a 3-way merge using the installed snapshot (`--merge`). force still wins."
934
+ )
935
+ };
936
+ async function updateComponents(args = {}) {
937
+ try {
938
+ const cwd = resolveProjectCwd(args.cwd);
939
+ const names = (args.names ?? []).map((n) => n.trim()).filter(Boolean);
940
+ for (const name of names) {
941
+ if (!isSafeItemName(name)) {
942
+ return failJson(`Invalid component name: '${name}'.`);
943
+ }
944
+ }
945
+ const cliArgs = ["update", ...names, "--yes"];
946
+ if (args.force) cliArgs.push("--force");
947
+ if (args.overwriteLocal) cliArgs.push("--overwrite-local");
948
+ if (args.merge) cliArgs.push("--merge");
949
+ const result = await runCli(cliArgs, { cwd });
950
+ if (!result.ok) {
951
+ return failJson(`${CLI_BIN} update failed.`, {
952
+ cwd,
953
+ names,
954
+ exitCode: result.code,
955
+ stdout: result.stdout.trim(),
956
+ stderr: result.stderr.trim(),
957
+ hint: "Run diff_components first. Conflicts need --force; local-only edits need overwriteLocal."
958
+ });
959
+ }
960
+ return okJson({
961
+ ok: true,
962
+ cwd,
963
+ names: names.length ? names : "all-tracked",
964
+ force: Boolean(args.force),
965
+ overwriteLocal: Boolean(args.overwriteLocal),
966
+ merge: Boolean(args.merge),
967
+ cli: [CLI_BIN, ...cliArgs].join(" "),
968
+ stdout: result.stdout.trim(),
969
+ stderr: result.stderr.trim() || void 0,
970
+ note: `Files were written by ${CLI_BIN} (canonical updater). MCP does not replace the CLI.`
971
+ });
972
+ } catch (err) {
973
+ return failJson(err.message || String(err));
974
+ }
975
+ }
976
+
977
+ // src/server.ts
978
+ function createMcpServer(options = {}) {
979
+ const mutating = options.mutating !== false;
980
+ const server = createCatalogMcpServer();
981
+ if (mutating) {
982
+ server.registerTool(
983
+ "add_components",
984
+ {
985
+ title: "Add Ply components",
986
+ description: `Install registry components by shelling to \`${CLI_BIN} add --yes\`. Pro names require PLY_LICENSE_KEY (or licenseKey). Project must already have base-ui.json.`,
987
+ inputSchema: addComponentsInput
988
+ },
989
+ async (args) => addComponents(args)
990
+ );
991
+ server.registerTool(
992
+ "init_project",
993
+ {
994
+ title: "Initialize Ply in a project",
995
+ description: `Run \`${CLI_BIN} init --yes\` in the target directory (writes base-ui.json, styles, icons, editor rules). Always non-interactive.`,
996
+ inputSchema: initProjectInput
997
+ },
998
+ async (args) => initProject(args)
999
+ );
1000
+ server.registerTool(
1001
+ "doctor",
1002
+ {
1003
+ title: "Verify Ply project setup",
1004
+ description: `Run \`${CLI_BIN} doctor\` (read-only). Returns isError when the CLI exits non-zero.`,
1005
+ inputSchema: doctorInput
1006
+ },
1007
+ async (args) => doctor(args)
1008
+ );
1009
+ server.registerTool(
1010
+ "diff_components",
1011
+ {
1012
+ title: "Diff installed Ply components",
1013
+ description: `Run \`${CLI_BIN} diff\` against tracked components. Read-only. Omit names to check everything in base-ui-lock.json.`,
1014
+ inputSchema: diffComponentsInput
1015
+ },
1016
+ async (args) => diffComponents(args)
1017
+ );
1018
+ server.registerTool(
1019
+ "update_components",
1020
+ {
1021
+ title: "Update installed Ply components",
1022
+ description: `Run \`${CLI_BIN} update --yes\`. Untouched files take upstream. Conflicts skip unless force or merge. Local-only edits skip unless overwriteLocal.`,
1023
+ inputSchema: updateComponentsInput
1024
+ },
1025
+ async (args) => updateComponents(args)
1026
+ );
1027
+ }
1028
+ return server;
1029
+ }
1030
+
1031
+ // src/http.ts
1032
+ var import_node_http = require("http");
1033
+ var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
1034
+ function setCors(res) {
1035
+ res.setHeader("Access-Control-Allow-Origin", "*");
1036
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
1037
+ res.setHeader(
1038
+ "Access-Control-Allow-Headers",
1039
+ "Content-Type, Authorization, Mcp-Session-Id, MCP-Protocol-Version"
1040
+ );
1041
+ }
1042
+ function unauthorized(res) {
1043
+ setCors(res);
1044
+ res.writeHead(401, { "Content-Type": "application/json" });
1045
+ res.end(JSON.stringify({ error: "Unauthorized" }));
1046
+ }
1047
+ function checkAuth(req, token) {
1048
+ if (!token) return true;
1049
+ const header = req.headers.authorization;
1050
+ return header === `Bearer ${token}`;
1051
+ }
1052
+ async function readBody(req) {
1053
+ const chunks = [];
1054
+ for await (const chunk of req) {
1055
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
1056
+ }
1057
+ const raw = Buffer.concat(chunks).toString("utf8").trim();
1058
+ if (!raw) return void 0;
1059
+ return JSON.parse(raw);
1060
+ }
1061
+ function startHttpServer(options) {
1062
+ const server = (0, import_node_http.createServer)(async (req, res) => {
1063
+ try {
1064
+ const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
1065
+ if (req.method === "OPTIONS") {
1066
+ setCors(res);
1067
+ res.writeHead(204);
1068
+ res.end();
1069
+ return;
1070
+ }
1071
+ if (url.pathname === "/" && req.method === "GET") {
1072
+ setCors(res);
1073
+ res.writeHead(200, { "Content-Type": "application/json" });
1074
+ res.end(
1075
+ JSON.stringify({
1076
+ ok: true,
1077
+ name: MCP_PACKAGE,
1078
+ version: PACKAGE_VERSION,
1079
+ transport: "streamable-http",
1080
+ mcp: "/mcp",
1081
+ mutating: !options.readOnly
1082
+ })
1083
+ );
1084
+ return;
1085
+ }
1086
+ if (url.pathname !== "/mcp") {
1087
+ setCors(res);
1088
+ res.writeHead(404, { "Content-Type": "application/json" });
1089
+ res.end(JSON.stringify({ error: "Not found. MCP is POST/GET /mcp." }));
1090
+ return;
1091
+ }
1092
+ if (!checkAuth(req, options.authToken)) {
1093
+ unauthorized(res);
1094
+ return;
1095
+ }
1096
+ setCors(res);
1097
+ const mcp = createMcpServer({ mutating: !options.readOnly });
1098
+ const transport = new import_streamableHttp.StreamableHTTPServerTransport({
1099
+ sessionIdGenerator: void 0
1100
+ });
1101
+ await mcp.connect(transport);
1102
+ const parsedBody = req.method === "POST" ? await readBody(req) : void 0;
1103
+ await transport.handleRequest(req, res, parsedBody);
1104
+ } catch (err) {
1105
+ console.error(`${MCP_PACKAGE} HTTP error:`, err);
1106
+ if (!res.headersSent) {
1107
+ res.writeHead(500, { "Content-Type": "application/json" });
1108
+ }
1109
+ if (!res.writableEnded) {
1110
+ res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
1111
+ }
1112
+ }
1113
+ });
1114
+ server.listen(options.port, options.host, () => {
1115
+ const addr = server.address();
1116
+ const port = typeof addr === "object" && addr ? addr.port : options.port;
1117
+ console.error(
1118
+ `${MCP_PACKAGE} v${PACKAGE_VERSION} Streamable HTTP on http://${options.host}:${port}/mcp` + (options.readOnly ? " (read-only catalog)" : "")
1119
+ );
1120
+ });
1121
+ return server;
1122
+ }
1123
+
1124
+ // src/index.ts
1125
+ function parseArgs(argv) {
1126
+ let http = argv.includes("--http") || process.env.PLY_MCP_HTTP === "1" || process.env.BASE_UI_MCP_HTTP === "1";
1127
+ let readOnly = argv.includes("--read-only") || process.env.PLY_MCP_READ_ONLY === "1" || process.env.BASE_UI_MCP_READ_ONLY === "1";
1128
+ let port = Number(process.env.PORT || process.env.PLY_MCP_PORT || process.env.BASE_UI_MCP_PORT || 3334);
1129
+ let host = process.env.PLY_MCP_HOST || process.env.BASE_UI_MCP_HOST || (http ? "127.0.0.1" : "127.0.0.1");
1130
+ for (let i = 0; i < argv.length; i++) {
1131
+ const arg = argv[i];
1132
+ if (arg === "--port" && argv[i + 1]) {
1133
+ port = Number(argv[++i]);
1134
+ } else if (arg?.startsWith("--port=")) {
1135
+ port = Number(arg.slice("--port=".length));
1136
+ } else if (arg === "--host" && argv[i + 1]) {
1137
+ host = argv[++i];
1138
+ } else if (arg?.startsWith("--host=")) {
1139
+ host = arg.slice("--host=".length);
1140
+ }
1141
+ }
1142
+ if (!Number.isFinite(port) || port <= 0) port = 3334;
1143
+ return { http, readOnly, port, host };
1144
+ }
1145
+ async function main() {
1146
+ const opts = parseArgs(process.argv.slice(2));
1147
+ if (opts.http) {
1148
+ startHttpServer({
1149
+ host: opts.host,
1150
+ port: opts.port,
1151
+ readOnly: opts.readOnly,
1152
+ authToken: process.env.PLY_MCP_AUTH_TOKEN?.trim() || process.env.BASE_UI_MCP_AUTH_TOKEN?.trim() || void 0
1153
+ });
1154
+ return;
1155
+ }
1156
+ const server = createMcpServer({ mutating: true });
1157
+ const transport = new import_stdio.StdioServerTransport();
1158
+ await server.connect(transport);
1159
+ console.error(`${MCP_PACKAGE} v${PACKAGE_VERSION} ready on stdio`);
1160
+ }
1161
+ main().catch((err) => {
1162
+ console.error(`${MCP_PACKAGE} failed to start:`, err);
1163
+ process.exit(1);
1164
+ });
1165
+ //# sourceMappingURL=index.js.map