untitledui-mcp 0.1.1 → 0.1.4
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 +281 -21
- package/package.json +1 -1
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,74 @@ 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
|
+
|
|
381
|
+
// src/utils/tokens.ts
|
|
382
|
+
function estimateTokens(content) {
|
|
383
|
+
return Math.ceil(content.length / 4);
|
|
384
|
+
}
|
|
385
|
+
function estimateFileTokens(file) {
|
|
386
|
+
const content = file.code || file.content || "";
|
|
387
|
+
return estimateTokens(content);
|
|
388
|
+
}
|
|
389
|
+
function estimateComponentTokens(files) {
|
|
390
|
+
return files.reduce((sum, file) => sum + estimateFileTokens(file), 0);
|
|
391
|
+
}
|
|
392
|
+
function getFileTokenList(files) {
|
|
393
|
+
return files.map((file) => ({
|
|
394
|
+
path: file.path,
|
|
395
|
+
tokens: estimateFileTokens(file)
|
|
396
|
+
}));
|
|
397
|
+
}
|
|
398
|
+
var CLAUDE_READ_TOKEN_LIMIT = 25e3;
|
|
399
|
+
function isLikelyTooLarge(estimatedTokens) {
|
|
400
|
+
return estimatedTokens > CLAUDE_READ_TOKEN_LIMIT;
|
|
401
|
+
}
|
|
402
|
+
|
|
292
403
|
// src/server.ts
|
|
293
404
|
function createServer(licenseKey) {
|
|
294
405
|
const client = new UntitledUIClient(licenseKey);
|
|
@@ -366,7 +477,7 @@ function createServer(licenseKey) {
|
|
|
366
477
|
},
|
|
367
478
|
{
|
|
368
479
|
name: "get_component",
|
|
369
|
-
description: "Get a single component's code.
|
|
480
|
+
description: "Get a single component's code with token estimates. Returns estimatedTokens and file list. If estimatedTokens > 25000, consider using get_component_file for specific files instead.",
|
|
370
481
|
inputSchema: {
|
|
371
482
|
type: "object",
|
|
372
483
|
properties: {
|
|
@@ -378,7 +489,7 @@ function createServer(licenseKey) {
|
|
|
378
489
|
},
|
|
379
490
|
{
|
|
380
491
|
name: "get_component_with_deps",
|
|
381
|
-
description: "Get a component with all
|
|
492
|
+
description: "Get a component with all base dependencies. Returns estimatedTokens. If response is too large (>25000 tokens), use get_component_file to fetch specific files individually.",
|
|
382
493
|
inputSchema: {
|
|
383
494
|
type: "object",
|
|
384
495
|
properties: {
|
|
@@ -388,20 +499,38 @@ function createServer(licenseKey) {
|
|
|
388
499
|
required: ["type", "name"]
|
|
389
500
|
}
|
|
390
501
|
},
|
|
502
|
+
{
|
|
503
|
+
name: "get_component_file",
|
|
504
|
+
description: "Get a single file from a component. Use this when get_component or get_component_with_deps returns a large response (>25000 tokens). First call get_component to see the file list, then fetch specific files as needed.",
|
|
505
|
+
inputSchema: {
|
|
506
|
+
type: "object",
|
|
507
|
+
properties: {
|
|
508
|
+
type: { type: "string", description: "Component type" },
|
|
509
|
+
name: { type: "string", description: "Component name" },
|
|
510
|
+
file: { type: "string", description: "File path within the component (e.g., 'Button.tsx' or 'variants/InputPhone.tsx')" }
|
|
511
|
+
},
|
|
512
|
+
required: ["type", "name", "file"]
|
|
513
|
+
}
|
|
514
|
+
},
|
|
391
515
|
{
|
|
392
516
|
name: "list_examples",
|
|
393
|
-
description: "
|
|
394
|
-
inputSchema: {
|
|
517
|
+
description: "Browse available page examples. Call without path to see categories, then drill down.",
|
|
518
|
+
inputSchema: {
|
|
519
|
+
type: "object",
|
|
520
|
+
properties: {
|
|
521
|
+
path: { type: "string", description: "Path to browse (e.g., '', 'application', 'application/dashboards-01')" }
|
|
522
|
+
}
|
|
523
|
+
}
|
|
395
524
|
},
|
|
396
525
|
{
|
|
397
526
|
name: "get_example",
|
|
398
|
-
description: "Get a
|
|
527
|
+
description: "Get a single example page with all files. Requires full path including page number.",
|
|
399
528
|
inputSchema: {
|
|
400
529
|
type: "object",
|
|
401
530
|
properties: {
|
|
402
|
-
|
|
531
|
+
path: { type: "string", description: "Full path to example page (e.g., 'application/dashboards-01/01')" }
|
|
403
532
|
},
|
|
404
|
-
required: ["
|
|
533
|
+
required: ["path"]
|
|
405
534
|
}
|
|
406
535
|
},
|
|
407
536
|
{
|
|
@@ -477,11 +606,24 @@ function createServer(licenseKey) {
|
|
|
477
606
|
files: fetched.files,
|
|
478
607
|
dependencies: fetched.dependencies || [],
|
|
479
608
|
devDependencies: fetched.devDependencies || [],
|
|
480
|
-
baseComponents: (fetched.
|
|
609
|
+
baseComponents: getBaseComponentNames(fetched.files)
|
|
481
610
|
};
|
|
482
611
|
cache.set(cacheKey, component, CACHE_TTL.componentCode);
|
|
483
612
|
}
|
|
484
|
-
|
|
613
|
+
const estimatedTokens = estimateComponentTokens(component.files);
|
|
614
|
+
const fileTokens = getFileTokenList(component.files);
|
|
615
|
+
const tooLarge = isLikelyTooLarge(estimatedTokens);
|
|
616
|
+
const result = {
|
|
617
|
+
...component,
|
|
618
|
+
estimatedTokens,
|
|
619
|
+
fileCount: component.files.length,
|
|
620
|
+
fileList: fileTokens,
|
|
621
|
+
...tooLarge && {
|
|
622
|
+
warning: `Response is large (${estimatedTokens} tokens). Consider using get_component_file for specific files.`,
|
|
623
|
+
hint: "Use get_component_file with type, name, and file path to fetch individual files."
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
485
627
|
}
|
|
486
628
|
case "get_component_with_deps": {
|
|
487
629
|
const { type, name: componentName } = args;
|
|
@@ -500,7 +642,7 @@ function createServer(licenseKey) {
|
|
|
500
642
|
}]
|
|
501
643
|
};
|
|
502
644
|
}
|
|
503
|
-
const baseComponentNames = (primary.
|
|
645
|
+
const baseComponentNames = getBaseComponentNames(primary.files);
|
|
504
646
|
const baseComponents = baseComponentNames.length > 0 ? await client.fetchComponents("base", baseComponentNames) : [];
|
|
505
647
|
const allDeps = /* @__PURE__ */ new Set();
|
|
506
648
|
const allDevDeps = /* @__PURE__ */ new Set();
|
|
@@ -508,12 +650,19 @@ function createServer(licenseKey) {
|
|
|
508
650
|
c.dependencies?.forEach((d) => allDeps.add(d));
|
|
509
651
|
c.devDependencies?.forEach((d) => allDevDeps.add(d));
|
|
510
652
|
});
|
|
653
|
+
const allFiles = [
|
|
654
|
+
...primary.files,
|
|
655
|
+
...baseComponents.flatMap((c) => c.files)
|
|
656
|
+
];
|
|
657
|
+
const estimatedTokens = estimateComponentTokens(allFiles);
|
|
658
|
+
const tooLarge = isLikelyTooLarge(estimatedTokens);
|
|
511
659
|
const result = {
|
|
512
660
|
primary: {
|
|
513
661
|
name: primary.name,
|
|
514
662
|
type,
|
|
515
663
|
description: generateDescription(primary.name, type),
|
|
516
664
|
files: primary.files,
|
|
665
|
+
fileList: getFileTokenList(primary.files),
|
|
517
666
|
dependencies: primary.dependencies || [],
|
|
518
667
|
devDependencies: primary.devDependencies || [],
|
|
519
668
|
baseComponents: baseComponentNames
|
|
@@ -523,38 +672,149 @@ function createServer(licenseKey) {
|
|
|
523
672
|
type: "base",
|
|
524
673
|
description: generateDescription(c.name, "base"),
|
|
525
674
|
files: c.files,
|
|
675
|
+
fileList: getFileTokenList(c.files),
|
|
526
676
|
dependencies: c.dependencies || [],
|
|
527
677
|
devDependencies: c.devDependencies || []
|
|
528
678
|
})),
|
|
529
679
|
totalFiles: primary.files.length + baseComponents.reduce((sum, c) => sum + c.files.length, 0),
|
|
680
|
+
estimatedTokens,
|
|
530
681
|
allDependencies: Array.from(allDeps),
|
|
531
|
-
allDevDependencies: Array.from(allDevDeps)
|
|
682
|
+
allDevDependencies: Array.from(allDevDeps),
|
|
683
|
+
...tooLarge && {
|
|
684
|
+
warning: `Response is large (${estimatedTokens} tokens, limit is ${CLAUDE_READ_TOKEN_LIMIT}). Consider using get_component_file for specific files.`,
|
|
685
|
+
hint: "To fetch individual files, use get_component_file with the component type, name, and file path from fileList."
|
|
686
|
+
}
|
|
532
687
|
};
|
|
533
688
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
534
689
|
}
|
|
535
|
-
case "
|
|
690
|
+
case "get_component_file": {
|
|
691
|
+
const { type, name: componentName, file: filePath } = args;
|
|
692
|
+
const cacheKey = `component:${type}:${componentName}`;
|
|
693
|
+
let files;
|
|
694
|
+
const cached = cache.get(cacheKey);
|
|
695
|
+
if (cached) {
|
|
696
|
+
files = cached.files;
|
|
697
|
+
} else {
|
|
698
|
+
const fetched = await client.fetchComponent(type, componentName);
|
|
699
|
+
if (!fetched) {
|
|
700
|
+
const index = await buildSearchIndex();
|
|
701
|
+
const suggestions = fuzzySearch(componentName, index, 5).map((r) => r.fullPath);
|
|
702
|
+
return {
|
|
703
|
+
content: [{
|
|
704
|
+
type: "text",
|
|
705
|
+
text: JSON.stringify({
|
|
706
|
+
error: `Component "${componentName}" not found`,
|
|
707
|
+
code: "NOT_FOUND",
|
|
708
|
+
suggestions
|
|
709
|
+
}, null, 2)
|
|
710
|
+
}]
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
files = fetched.files;
|
|
714
|
+
}
|
|
715
|
+
const file = files.find(
|
|
716
|
+
(f) => f.path === filePath || f.path.endsWith(`/${filePath}`) || f.path.endsWith(filePath)
|
|
717
|
+
);
|
|
718
|
+
if (!file) {
|
|
719
|
+
const availableFiles = files.map((f) => f.path);
|
|
720
|
+
return {
|
|
721
|
+
content: [{
|
|
722
|
+
type: "text",
|
|
723
|
+
text: JSON.stringify({
|
|
724
|
+
error: `File "${filePath}" not found in ${type}/${componentName}`,
|
|
725
|
+
code: "FILE_NOT_FOUND",
|
|
726
|
+
availableFiles,
|
|
727
|
+
hint: "Use one of the file paths from availableFiles"
|
|
728
|
+
}, null, 2)
|
|
729
|
+
}]
|
|
730
|
+
};
|
|
731
|
+
}
|
|
536
732
|
return {
|
|
537
733
|
content: [{
|
|
538
734
|
type: "text",
|
|
539
735
|
text: JSON.stringify({
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
736
|
+
component: `${type}/${componentName}`,
|
|
737
|
+
file: {
|
|
738
|
+
path: file.path,
|
|
739
|
+
code: file.code
|
|
740
|
+
},
|
|
741
|
+
estimatedTokens: estimateComponentTokens([file])
|
|
544
742
|
}, null, 2)
|
|
545
743
|
}]
|
|
546
744
|
};
|
|
547
745
|
}
|
|
746
|
+
case "list_examples": {
|
|
747
|
+
const { path: path2 = "" } = args;
|
|
748
|
+
const cacheKey = `examples:list:${path2}`;
|
|
749
|
+
let listing = cache.get(cacheKey);
|
|
750
|
+
if (!listing) {
|
|
751
|
+
listing = await client.fetchExample(path2);
|
|
752
|
+
if (listing) {
|
|
753
|
+
cache.set(cacheKey, listing, CACHE_TTL.componentList);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
if (listing.type === "directory" && listing.results) {
|
|
757
|
+
return {
|
|
758
|
+
content: [{
|
|
759
|
+
type: "text",
|
|
760
|
+
text: JSON.stringify({
|
|
761
|
+
path: path2 || "(root)",
|
|
762
|
+
type: "directory",
|
|
763
|
+
items: listing.results,
|
|
764
|
+
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`
|
|
765
|
+
}, null, 2)
|
|
766
|
+
}]
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
return { content: [{ type: "text", text: JSON.stringify(listing, null, 2) }] };
|
|
770
|
+
}
|
|
548
771
|
case "get_example": {
|
|
549
|
-
const {
|
|
550
|
-
const cacheKey = `example:${
|
|
772
|
+
const { path: examplePath } = args;
|
|
773
|
+
const cacheKey = `example:${examplePath}`;
|
|
551
774
|
let example = cache.get(cacheKey);
|
|
552
775
|
if (!example) {
|
|
553
|
-
example = await client.fetchExample(
|
|
776
|
+
example = await client.fetchExample(examplePath);
|
|
554
777
|
if (example) {
|
|
555
778
|
cache.set(cacheKey, example, CACHE_TTL.examples);
|
|
556
779
|
}
|
|
557
780
|
}
|
|
781
|
+
if (example.type === "directory") {
|
|
782
|
+
return {
|
|
783
|
+
content: [{
|
|
784
|
+
type: "text",
|
|
785
|
+
text: JSON.stringify({
|
|
786
|
+
error: "Path is a directory, not a page",
|
|
787
|
+
path: examplePath,
|
|
788
|
+
availableItems: example.results,
|
|
789
|
+
hint: `Use get_example with path='${examplePath}/<item>' to fetch a specific page`
|
|
790
|
+
}, null, 2)
|
|
791
|
+
}]
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
if (example.type === "json-file" && example.content) {
|
|
795
|
+
const files = example.content.files || [];
|
|
796
|
+
const estimatedTokens = estimateComponentTokens(files);
|
|
797
|
+
const tooLarge = isLikelyTooLarge(estimatedTokens);
|
|
798
|
+
return {
|
|
799
|
+
content: [{
|
|
800
|
+
type: "text",
|
|
801
|
+
text: JSON.stringify({
|
|
802
|
+
path: examplePath,
|
|
803
|
+
name: example.content.name,
|
|
804
|
+
files,
|
|
805
|
+
fileList: getFileTokenList(files),
|
|
806
|
+
dependencies: example.content.dependencies || [],
|
|
807
|
+
devDependencies: example.content.devDependencies || [],
|
|
808
|
+
components: example.content.components || [],
|
|
809
|
+
fileCount: files.length,
|
|
810
|
+
estimatedTokens,
|
|
811
|
+
...tooLarge && {
|
|
812
|
+
warning: `Response is large (${estimatedTokens} tokens). Consider fetching specific component files instead.`
|
|
813
|
+
}
|
|
814
|
+
}, null, 2)
|
|
815
|
+
}]
|
|
816
|
+
};
|
|
817
|
+
}
|
|
558
818
|
return { content: [{ type: "text", text: JSON.stringify(example, null, 2) }] };
|
|
559
819
|
}
|
|
560
820
|
case "validate_license": {
|
package/package.json
CHANGED