untitledui-mcp 0.1.0 → 0.1.3
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/README.md +15 -5
- package/dist/index.js +158 -23
- package/package.json +4 -1
- package/.env.example +0 -1
- package/assets/cover.png +0 -0
- package/src/api/client.test.ts +0 -46
- package/src/api/client.ts +0 -117
- package/src/api/endpoints.ts +0 -12
- package/src/api/types.ts +0 -84
- package/src/cache/memory-cache.test.ts +0 -56
- package/src/cache/memory-cache.ts +0 -64
- package/src/index.ts +0 -96
- package/src/server.ts +0 -374
- package/src/utils/descriptions.ts +0 -65
- package/src/utils/license.test.ts +0 -45
- package/src/utils/license.ts +0 -35
- package/src/utils/search.test.ts +0 -39
- package/src/utils/search.ts +0 -71
- package/tsconfig.json +0 -16
package/README.md
CHANGED
|
@@ -74,7 +74,8 @@ Your AI gets these tools:
|
|
|
74
74
|
| `list_components` | Browse a category |
|
|
75
75
|
| `get_component_with_deps` | Fetch component + all dependencies |
|
|
76
76
|
| `get_component` | Fetch component only |
|
|
77
|
-
| `
|
|
77
|
+
| `list_examples` | Browse available page templates |
|
|
78
|
+
| `get_example` | Fetch a specific page template |
|
|
78
79
|
|
|
79
80
|
### Example: Add a modal
|
|
80
81
|
|
|
@@ -95,13 +96,22 @@ AI calls list_components { type: "application", subfolder: "sidebars" }
|
|
|
95
96
|
→ Returns all sidebar options for you to choose from
|
|
96
97
|
```
|
|
97
98
|
|
|
98
|
-
### Example: Start from template
|
|
99
|
+
### Example: Start from a page template
|
|
99
100
|
|
|
100
101
|
```
|
|
101
|
-
You: "
|
|
102
|
+
You: "Show me available dashboard templates"
|
|
102
103
|
|
|
103
|
-
AI calls
|
|
104
|
-
→ Returns
|
|
104
|
+
AI calls list_examples { path: "" }
|
|
105
|
+
→ Returns: application, marketing
|
|
106
|
+
|
|
107
|
+
AI calls list_examples { path: "application" }
|
|
108
|
+
→ Returns: dashboards-01, dashboards-02, settings-01, ...
|
|
109
|
+
|
|
110
|
+
AI calls list_examples { path: "application/dashboards-01" }
|
|
111
|
+
→ Returns: 01, 02, 03, ... (individual pages)
|
|
112
|
+
|
|
113
|
+
AI calls get_example { path: "application/dashboards-01/01" }
|
|
114
|
+
→ Returns complete page with 27 files, all dependencies
|
|
105
115
|
```
|
|
106
116
|
|
|
107
117
|
## Response Format
|
package/dist/index.js
CHANGED
|
@@ -126,17 +126,60 @@ var UntitledUIClient = class {
|
|
|
126
126
|
}
|
|
127
127
|
return data.components;
|
|
128
128
|
}
|
|
129
|
-
|
|
129
|
+
/**
|
|
130
|
+
* Browse examples hierarchy or fetch example content.
|
|
131
|
+
* - "" → lists types (application, marketing)
|
|
132
|
+
* - "application" → lists examples (dashboards-01, settings-01, etc.)
|
|
133
|
+
* - "application/dashboards-01" → lists pages (01, 02, 03, ...)
|
|
134
|
+
* - "application/dashboards-01/01" → returns actual content
|
|
135
|
+
*/
|
|
136
|
+
async fetchExample(path2) {
|
|
130
137
|
const response = await fetch(ENDPOINTS.fetchExample, {
|
|
131
138
|
method: "POST",
|
|
132
139
|
headers: { "Content-Type": "application/json" },
|
|
133
140
|
body: JSON.stringify({
|
|
134
|
-
example:
|
|
141
|
+
example: path2,
|
|
135
142
|
key: this.licenseKey
|
|
136
143
|
})
|
|
137
144
|
});
|
|
138
145
|
return response.json();
|
|
139
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Fetch all pages in an example and combine them.
|
|
149
|
+
* Path should be "application/dashboards-01" (without page number).
|
|
150
|
+
*/
|
|
151
|
+
async fetchFullExample(examplePath) {
|
|
152
|
+
const listing = await this.fetchExample(examplePath);
|
|
153
|
+
if (listing.type !== "directory" || !listing.results) {
|
|
154
|
+
throw new Error(`Invalid example path: ${examplePath}. Expected a directory with pages.`);
|
|
155
|
+
}
|
|
156
|
+
const pages = listing.results;
|
|
157
|
+
const allDeps = /* @__PURE__ */ new Set();
|
|
158
|
+
const allDevDeps = /* @__PURE__ */ new Set();
|
|
159
|
+
const fetchedPages = [];
|
|
160
|
+
for (const page of pages) {
|
|
161
|
+
const pageData = await this.fetchExample(`${examplePath}/${page}`);
|
|
162
|
+
if (pageData.type === "json-file" && pageData.content) {
|
|
163
|
+
const content = pageData.content;
|
|
164
|
+
fetchedPages.push({
|
|
165
|
+
page,
|
|
166
|
+
files: content.files || [],
|
|
167
|
+
dependencies: content.dependencies || [],
|
|
168
|
+
devDependencies: content.devDependencies || []
|
|
169
|
+
});
|
|
170
|
+
content.dependencies?.forEach((d) => allDeps.add(d));
|
|
171
|
+
content.devDependencies?.forEach((d) => allDevDeps.add(d));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const totalFiles = fetchedPages.reduce((sum, p) => sum + p.files.length, 0);
|
|
175
|
+
return {
|
|
176
|
+
name: examplePath,
|
|
177
|
+
pages: fetchedPages,
|
|
178
|
+
allDependencies: Array.from(allDeps),
|
|
179
|
+
allDevDependencies: Array.from(allDevDeps),
|
|
180
|
+
totalFiles
|
|
181
|
+
};
|
|
182
|
+
}
|
|
140
183
|
};
|
|
141
184
|
|
|
142
185
|
// src/cache/memory-cache.ts
|
|
@@ -289,6 +332,52 @@ function generateDescription(name, type) {
|
|
|
289
332
|
return `UI component: ${name.replace(/-/g, " ")}`;
|
|
290
333
|
}
|
|
291
334
|
|
|
335
|
+
// src/utils/parse-deps.ts
|
|
336
|
+
function parseComponentImports(files) {
|
|
337
|
+
const imports = /* @__PURE__ */ new Set();
|
|
338
|
+
const regex = /@\/components\/(base|application|foundations|shared-assets)\/([^"']+)/g;
|
|
339
|
+
for (const file of files) {
|
|
340
|
+
const code = file.content || file.code || "";
|
|
341
|
+
let match;
|
|
342
|
+
while ((match = regex.exec(code)) !== null) {
|
|
343
|
+
imports.add(match[0]);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const parsed = [];
|
|
347
|
+
for (const imp of imports) {
|
|
348
|
+
const match = imp.match(/@\/components\/(base|application|foundations|shared-assets)\/(.+)/);
|
|
349
|
+
if (match) {
|
|
350
|
+
parsed.push({
|
|
351
|
+
type: match[1],
|
|
352
|
+
path: match[2],
|
|
353
|
+
fullImport: imp
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return parsed;
|
|
358
|
+
}
|
|
359
|
+
function extractBaseComponentPaths(files) {
|
|
360
|
+
const deps = parseComponentImports(files);
|
|
361
|
+
const basePaths = /* @__PURE__ */ new Set();
|
|
362
|
+
for (const dep of deps) {
|
|
363
|
+
if (dep.type === "base") {
|
|
364
|
+
basePaths.add(dep.path);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return Array.from(basePaths);
|
|
368
|
+
}
|
|
369
|
+
function getBaseComponentNames(files) {
|
|
370
|
+
const paths = extractBaseComponentPaths(files);
|
|
371
|
+
const names = /* @__PURE__ */ new Set();
|
|
372
|
+
for (const path2 of paths) {
|
|
373
|
+
const parts = path2.split("/");
|
|
374
|
+
if (parts.length >= 1) {
|
|
375
|
+
names.add(parts[0]);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return Array.from(names);
|
|
379
|
+
}
|
|
380
|
+
|
|
292
381
|
// src/server.ts
|
|
293
382
|
function createServer(licenseKey) {
|
|
294
383
|
const client = new UntitledUIClient(licenseKey);
|
|
@@ -390,18 +479,23 @@ function createServer(licenseKey) {
|
|
|
390
479
|
},
|
|
391
480
|
{
|
|
392
481
|
name: "list_examples",
|
|
393
|
-
description: "
|
|
394
|
-
inputSchema: {
|
|
482
|
+
description: "Browse available page examples. Call without path to see categories, then drill down.",
|
|
483
|
+
inputSchema: {
|
|
484
|
+
type: "object",
|
|
485
|
+
properties: {
|
|
486
|
+
path: { type: "string", description: "Path to browse (e.g., '', 'application', 'application/dashboards-01')" }
|
|
487
|
+
}
|
|
488
|
+
}
|
|
395
489
|
},
|
|
396
490
|
{
|
|
397
491
|
name: "get_example",
|
|
398
|
-
description: "Get a
|
|
492
|
+
description: "Get a single example page with all files. Requires full path including page number.",
|
|
399
493
|
inputSchema: {
|
|
400
494
|
type: "object",
|
|
401
495
|
properties: {
|
|
402
|
-
|
|
496
|
+
path: { type: "string", description: "Full path to example page (e.g., 'application/dashboards-01/01')" }
|
|
403
497
|
},
|
|
404
|
-
required: ["
|
|
498
|
+
required: ["path"]
|
|
405
499
|
}
|
|
406
500
|
},
|
|
407
501
|
{
|
|
@@ -477,7 +571,7 @@ function createServer(licenseKey) {
|
|
|
477
571
|
files: fetched.files,
|
|
478
572
|
dependencies: fetched.dependencies || [],
|
|
479
573
|
devDependencies: fetched.devDependencies || [],
|
|
480
|
-
baseComponents: (fetched.
|
|
574
|
+
baseComponents: getBaseComponentNames(fetched.files)
|
|
481
575
|
};
|
|
482
576
|
cache.set(cacheKey, component, CACHE_TTL.componentCode);
|
|
483
577
|
}
|
|
@@ -500,7 +594,7 @@ function createServer(licenseKey) {
|
|
|
500
594
|
}]
|
|
501
595
|
};
|
|
502
596
|
}
|
|
503
|
-
const baseComponentNames = (primary.
|
|
597
|
+
const baseComponentNames = getBaseComponentNames(primary.files);
|
|
504
598
|
const baseComponents = baseComponentNames.length > 0 ? await client.fetchComponents("base", baseComponentNames) : [];
|
|
505
599
|
const allDeps = /* @__PURE__ */ new Set();
|
|
506
600
|
const allDevDeps = /* @__PURE__ */ new Set();
|
|
@@ -533,28 +627,69 @@ function createServer(licenseKey) {
|
|
|
533
627
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
534
628
|
}
|
|
535
629
|
case "list_examples": {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
630
|
+
const { path: path2 = "" } = args;
|
|
631
|
+
const cacheKey = `examples:list:${path2}`;
|
|
632
|
+
let listing = cache.get(cacheKey);
|
|
633
|
+
if (!listing) {
|
|
634
|
+
listing = await client.fetchExample(path2);
|
|
635
|
+
if (listing) {
|
|
636
|
+
cache.set(cacheKey, listing, CACHE_TTL.componentList);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
if (listing.type === "directory" && listing.results) {
|
|
640
|
+
return {
|
|
641
|
+
content: [{
|
|
642
|
+
type: "text",
|
|
643
|
+
text: JSON.stringify({
|
|
644
|
+
path: path2 || "(root)",
|
|
645
|
+
type: "directory",
|
|
646
|
+
items: listing.results,
|
|
647
|
+
hint: path2 === "" ? "Use list_examples with path='application' or path='marketing' to see available examples" : path2.split("/").length === 1 ? `Use list_examples with path='${path2}/<example>' to see pages` : `Use get_example with path='${path2}/<page>' to fetch a specific page`
|
|
648
|
+
}, null, 2)
|
|
649
|
+
}]
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
return { content: [{ type: "text", text: JSON.stringify(listing, null, 2) }] };
|
|
547
653
|
}
|
|
548
654
|
case "get_example": {
|
|
549
|
-
const {
|
|
550
|
-
const cacheKey = `example:${
|
|
655
|
+
const { path: examplePath } = args;
|
|
656
|
+
const cacheKey = `example:${examplePath}`;
|
|
551
657
|
let example = cache.get(cacheKey);
|
|
552
658
|
if (!example) {
|
|
553
|
-
example = await client.fetchExample(
|
|
659
|
+
example = await client.fetchExample(examplePath);
|
|
554
660
|
if (example) {
|
|
555
661
|
cache.set(cacheKey, example, CACHE_TTL.examples);
|
|
556
662
|
}
|
|
557
663
|
}
|
|
664
|
+
if (example.type === "directory") {
|
|
665
|
+
return {
|
|
666
|
+
content: [{
|
|
667
|
+
type: "text",
|
|
668
|
+
text: JSON.stringify({
|
|
669
|
+
error: "Path is a directory, not a page",
|
|
670
|
+
path: examplePath,
|
|
671
|
+
availableItems: example.results,
|
|
672
|
+
hint: `Use get_example with path='${examplePath}/<item>' to fetch a specific page`
|
|
673
|
+
}, null, 2)
|
|
674
|
+
}]
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
if (example.type === "json-file" && example.content) {
|
|
678
|
+
return {
|
|
679
|
+
content: [{
|
|
680
|
+
type: "text",
|
|
681
|
+
text: JSON.stringify({
|
|
682
|
+
path: examplePath,
|
|
683
|
+
name: example.content.name,
|
|
684
|
+
files: example.content.files,
|
|
685
|
+
dependencies: example.content.dependencies || [],
|
|
686
|
+
devDependencies: example.content.devDependencies || [],
|
|
687
|
+
components: example.content.components || [],
|
|
688
|
+
fileCount: example.content.files?.length || 0
|
|
689
|
+
}, null, 2)
|
|
690
|
+
}]
|
|
691
|
+
};
|
|
692
|
+
}
|
|
558
693
|
return { content: [{ type: "text", text: JSON.stringify(example, null, 2) }] };
|
|
559
694
|
}
|
|
560
695
|
case "validate_license": {
|
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "untitledui-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "MCP server for UntitledUI Pro components - browse, search, and retrieve UI components via Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|
|
8
8
|
"untitledui-mcp": "dist/index.js"
|
|
9
9
|
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
10
13
|
"repository": {
|
|
11
14
|
"type": "git",
|
|
12
15
|
"url": "https://github.com/sbilde/untitledui-mcp"
|
package/.env.example
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
UNTITLEDUI_LICENSE_KEY=your_license_key_here
|
package/assets/cover.png
DELETED
|
Binary file
|
package/src/api/client.test.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
import { UntitledUIClient } from "./client.js";
|
|
3
|
-
|
|
4
|
-
describe("UntitledUIClient", () => {
|
|
5
|
-
describe("validateLicense", () => {
|
|
6
|
-
it("should return true for valid license", async () => {
|
|
7
|
-
const client = new UntitledUIClient("valid-key");
|
|
8
|
-
|
|
9
|
-
global.fetch = vi.fn().mockResolvedValue({
|
|
10
|
-
status: 200,
|
|
11
|
-
ok: true,
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
const result = await client.validateLicense();
|
|
15
|
-
expect(result).toBe(true);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it("should return false for invalid license", async () => {
|
|
19
|
-
const client = new UntitledUIClient("invalid-key");
|
|
20
|
-
|
|
21
|
-
global.fetch = vi.fn().mockResolvedValue({
|
|
22
|
-
status: 401,
|
|
23
|
-
ok: false,
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
const result = await client.validateLicense();
|
|
27
|
-
expect(result).toBe(false);
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
describe("listComponentTypes", () => {
|
|
32
|
-
it("should return array of types", async () => {
|
|
33
|
-
const client = new UntitledUIClient("valid-key");
|
|
34
|
-
|
|
35
|
-
global.fetch = vi.fn().mockResolvedValue({
|
|
36
|
-
ok: true,
|
|
37
|
-
json: () => Promise.resolve({
|
|
38
|
-
types: ["application", "base", "foundations"]
|
|
39
|
-
}),
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
const result = await client.listComponentTypes();
|
|
43
|
-
expect(result).toEqual(["application", "base", "foundations"]);
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
});
|
package/src/api/client.ts
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { ENDPOINTS } from "./endpoints.js";
|
|
2
|
-
import type {
|
|
3
|
-
ComponentListItem,
|
|
4
|
-
ComponentListResponse,
|
|
5
|
-
ComponentsResponse,
|
|
6
|
-
ExampleResponse,
|
|
7
|
-
FetchedComponent,
|
|
8
|
-
} from "./types.js";
|
|
9
|
-
|
|
10
|
-
export class UntitledUIClient {
|
|
11
|
-
constructor(private licenseKey: string) {}
|
|
12
|
-
|
|
13
|
-
async validateLicense(): Promise<boolean> {
|
|
14
|
-
try {
|
|
15
|
-
const response = await fetch(ENDPOINTS.validateKey(this.licenseKey));
|
|
16
|
-
return response.status === 200;
|
|
17
|
-
} catch {
|
|
18
|
-
return false;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async listComponentTypes(): Promise<string[]> {
|
|
23
|
-
const response = await fetch(ENDPOINTS.listTypes(this.licenseKey));
|
|
24
|
-
if (!response.ok) {
|
|
25
|
-
throw new Error(`API error: ${response.status}`);
|
|
26
|
-
}
|
|
27
|
-
const data = await response.json();
|
|
28
|
-
return data.types;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
async listComponents(type: string, subfolder?: string): Promise<ComponentListItem[]> {
|
|
32
|
-
let url: string;
|
|
33
|
-
if (subfolder) {
|
|
34
|
-
url = ENDPOINTS.listSubfolder(this.licenseKey, type, [subfolder]);
|
|
35
|
-
} else {
|
|
36
|
-
url = ENDPOINTS.listComponents(this.licenseKey, type);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const response = await fetch(url);
|
|
40
|
-
if (!response.ok) {
|
|
41
|
-
throw new Error(`API error: ${response.status}`);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const data: ComponentListResponse = await response.json();
|
|
45
|
-
|
|
46
|
-
// Handle subfolder response format
|
|
47
|
-
if (subfolder && Array.isArray(data.components)) {
|
|
48
|
-
const subfolderData = data.components[0];
|
|
49
|
-
if (subfolderData && typeof subfolderData === "object" && subfolder in subfolderData) {
|
|
50
|
-
return (subfolderData as Record<string, ComponentListItem[]>)[subfolder];
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return data.components as ComponentListItem[];
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async fetchComponent(type: string, name: string): Promise<FetchedComponent | null> {
|
|
58
|
-
const response = await fetch(ENDPOINTS.fetchComponents, {
|
|
59
|
-
method: "POST",
|
|
60
|
-
headers: { "Content-Type": "application/json" },
|
|
61
|
-
body: JSON.stringify({
|
|
62
|
-
type,
|
|
63
|
-
components: [name],
|
|
64
|
-
key: this.licenseKey,
|
|
65
|
-
}),
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
if (!response.ok) {
|
|
69
|
-
throw new Error(`API error: ${response.status}`);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const data: ComponentsResponse = await response.json();
|
|
73
|
-
|
|
74
|
-
if (data.pro && data.pro.length > 0) {
|
|
75
|
-
throw new Error(`PRO access required for: ${data.pro.join(", ")}`);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return data.components[0] || null;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
async fetchComponents(type: string, names: string[]): Promise<FetchedComponent[]> {
|
|
82
|
-
const response = await fetch(ENDPOINTS.fetchComponents, {
|
|
83
|
-
method: "POST",
|
|
84
|
-
headers: { "Content-Type": "application/json" },
|
|
85
|
-
body: JSON.stringify({
|
|
86
|
-
type,
|
|
87
|
-
components: names,
|
|
88
|
-
key: this.licenseKey,
|
|
89
|
-
}),
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
if (!response.ok) {
|
|
93
|
-
throw new Error(`API error: ${response.status}`);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const data: ComponentsResponse = await response.json();
|
|
97
|
-
|
|
98
|
-
if (data.pro && data.pro.length > 0) {
|
|
99
|
-
throw new Error(`PRO access required for: ${data.pro.join(", ")}`);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return data.components;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
async fetchExample(name: string): Promise<ExampleResponse> {
|
|
106
|
-
const response = await fetch(ENDPOINTS.fetchExample, {
|
|
107
|
-
method: "POST",
|
|
108
|
-
headers: { "Content-Type": "application/json" },
|
|
109
|
-
body: JSON.stringify({
|
|
110
|
-
example: name,
|
|
111
|
-
key: this.licenseKey,
|
|
112
|
-
}),
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
return response.json();
|
|
116
|
-
}
|
|
117
|
-
}
|
package/src/api/endpoints.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
export const API_BASE = "https://www.untitledui.com/react/api";
|
|
2
|
-
|
|
3
|
-
export const ENDPOINTS = {
|
|
4
|
-
validateKey: (key: string) => `${API_BASE}/validate-key?key=${key}`,
|
|
5
|
-
listTypes: (key: string) => `${API_BASE}/components/list?key=${key}`,
|
|
6
|
-
listComponents: (key: string, type: string) =>
|
|
7
|
-
`${API_BASE}/components/list?key=${key}&type=${type}`,
|
|
8
|
-
listSubfolder: (key: string, type: string, subfolders: string[]) =>
|
|
9
|
-
`${API_BASE}/components/list?key=${key}&type=${type}&subfolders=${subfolders.join(",")}`,
|
|
10
|
-
fetchComponents: `${API_BASE}/components`,
|
|
11
|
-
fetchExample: `${API_BASE}/components/example`,
|
|
12
|
-
} as const;
|
package/src/api/types.ts
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
// UntitledUI API Response Types
|
|
2
|
-
|
|
3
|
-
export interface ComponentType {
|
|
4
|
-
types: string[];
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export interface ComponentListItem {
|
|
8
|
-
name: string;
|
|
9
|
-
type: "file" | "dir";
|
|
10
|
-
count?: number;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface ComponentListResponse {
|
|
14
|
-
components: ComponentListItem[] | ComponentListWithSubfolder[];
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface ComponentListWithSubfolder {
|
|
18
|
-
[subfolder: string]: ComponentListItem[];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface ComponentFile {
|
|
22
|
-
path: string;
|
|
23
|
-
code: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface FetchedComponent {
|
|
27
|
-
name: string;
|
|
28
|
-
files: ComponentFile[];
|
|
29
|
-
dependencies?: string[];
|
|
30
|
-
devDependencies?: string[];
|
|
31
|
-
components?: BaseComponentRef[];
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface BaseComponentRef {
|
|
35
|
-
name: string;
|
|
36
|
-
path: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface ComponentsResponse {
|
|
40
|
-
components: FetchedComponent[];
|
|
41
|
-
pro?: string[];
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface ExampleResponse {
|
|
45
|
-
type: "json-file" | "directory" | "json-files" | "error";
|
|
46
|
-
content?: ExampleContent;
|
|
47
|
-
results?: string[];
|
|
48
|
-
status?: number;
|
|
49
|
-
message?: string;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export interface ExampleContent {
|
|
53
|
-
name: string;
|
|
54
|
-
files: ComponentFile[];
|
|
55
|
-
dependencies?: string[];
|
|
56
|
-
devDependencies?: string[];
|
|
57
|
-
components?: BaseComponentRef[];
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// MCP Tool Response Types
|
|
61
|
-
|
|
62
|
-
export interface MCPComponentResponse {
|
|
63
|
-
name: string;
|
|
64
|
-
type: string;
|
|
65
|
-
description: string;
|
|
66
|
-
files: ComponentFile[];
|
|
67
|
-
dependencies: string[];
|
|
68
|
-
devDependencies: string[];
|
|
69
|
-
baseComponents: string[];
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export interface MCPSearchResult {
|
|
73
|
-
name: string;
|
|
74
|
-
type: string;
|
|
75
|
-
fullPath: string;
|
|
76
|
-
matchType: "exact" | "partial";
|
|
77
|
-
score: number;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export interface MCPErrorResponse {
|
|
81
|
-
error: string;
|
|
82
|
-
code: "INVALID_LICENSE" | "NOT_FOUND" | "API_ERROR" | "NETWORK_ERROR";
|
|
83
|
-
suggestions?: string[];
|
|
84
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { MemoryCache } from "./memory-cache.js";
|
|
3
|
-
|
|
4
|
-
describe("MemoryCache", () => {
|
|
5
|
-
beforeEach(() => {
|
|
6
|
-
vi.useFakeTimers();
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
afterEach(() => {
|
|
10
|
-
vi.useRealTimers();
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it("should store and retrieve values", () => {
|
|
14
|
-
const cache = new MemoryCache();
|
|
15
|
-
cache.set("key1", "value1", 60);
|
|
16
|
-
expect(cache.get("key1")).toBe("value1");
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("should return undefined for missing keys", () => {
|
|
20
|
-
const cache = new MemoryCache();
|
|
21
|
-
expect(cache.get("missing")).toBeUndefined();
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("should expire entries after TTL", () => {
|
|
25
|
-
const cache = new MemoryCache();
|
|
26
|
-
cache.set("key1", "value1", 60);
|
|
27
|
-
|
|
28
|
-
vi.advanceTimersByTime(61 * 1000);
|
|
29
|
-
|
|
30
|
-
expect(cache.get("key1")).toBeUndefined();
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("should clear all entries", () => {
|
|
34
|
-
const cache = new MemoryCache();
|
|
35
|
-
cache.set("key1", "value1", 60);
|
|
36
|
-
cache.set("key2", "value2", 60);
|
|
37
|
-
|
|
38
|
-
cache.clear();
|
|
39
|
-
|
|
40
|
-
expect(cache.get("key1")).toBeUndefined();
|
|
41
|
-
expect(cache.get("key2")).toBeUndefined();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("should clear entries matching pattern", () => {
|
|
45
|
-
const cache = new MemoryCache();
|
|
46
|
-
cache.set("component:button", "data1", 60);
|
|
47
|
-
cache.set("component:input", "data2", 60);
|
|
48
|
-
cache.set("search:button", "data3", 60);
|
|
49
|
-
|
|
50
|
-
const cleared = cache.clearPattern("component:");
|
|
51
|
-
|
|
52
|
-
expect(cleared).toBe(2);
|
|
53
|
-
expect(cache.get("component:button")).toBeUndefined();
|
|
54
|
-
expect(cache.get("search:button")).toBe("data3");
|
|
55
|
-
});
|
|
56
|
-
});
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
interface CacheEntry<T> {
|
|
2
|
-
data: T;
|
|
3
|
-
expiresAt: number;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
export class MemoryCache {
|
|
7
|
-
private cache = new Map<string, CacheEntry<unknown>>();
|
|
8
|
-
|
|
9
|
-
set<T>(key: string, data: T, ttlSeconds: number): void {
|
|
10
|
-
this.cache.set(key, {
|
|
11
|
-
data,
|
|
12
|
-
expiresAt: Date.now() + ttlSeconds * 1000,
|
|
13
|
-
});
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
get<T>(key: string): T | undefined {
|
|
17
|
-
const entry = this.cache.get(key);
|
|
18
|
-
if (!entry) return undefined;
|
|
19
|
-
|
|
20
|
-
if (Date.now() > entry.expiresAt) {
|
|
21
|
-
this.cache.delete(key);
|
|
22
|
-
return undefined;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
return entry.data as T;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
has(key: string): boolean {
|
|
29
|
-
return this.get(key) !== undefined;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
clear(): void {
|
|
33
|
-
this.cache.clear();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
clearPattern(pattern: string): number {
|
|
37
|
-
let cleared = 0;
|
|
38
|
-
for (const key of this.cache.keys()) {
|
|
39
|
-
if (key.startsWith(pattern)) {
|
|
40
|
-
this.cache.delete(key);
|
|
41
|
-
cleared++;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return cleared;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
size(): number {
|
|
48
|
-
for (const [key, entry] of this.cache.entries()) {
|
|
49
|
-
if (Date.now() > entry.expiresAt) {
|
|
50
|
-
this.cache.delete(key);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return this.cache.size;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export const CACHE_TTL = {
|
|
58
|
-
componentTypes: 3600,
|
|
59
|
-
componentList: 3600,
|
|
60
|
-
componentCode: 86400,
|
|
61
|
-
searchResults: 1800,
|
|
62
|
-
examples: 86400,
|
|
63
|
-
licenseValidation: 300,
|
|
64
|
-
} as const;
|