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.
Files changed (3) hide show
  1. package/README.md +15 -5
  2. package/dist/index.js +281 -21
  3. 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
- | `get_example` | Get complete page template |
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: "Set up a dashboard layout"
102
+ You: "Show me available dashboard templates"
102
103
 
103
- AI calls get_example { name: "application" }
104
- → Returns complete dashboard with sidebar, header, and sample pages
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
- async fetchExample(name) {
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: name,
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. Does NOT include dependencies - use get_component_with_deps for that.",
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 its base component dependencies included",
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: "List available page examples (dashboards, marketing pages, etc.)",
394
- inputSchema: { type: "object", properties: {} }
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 complete page example with all files",
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
- name: { type: "string", description: "Example name (e.g., 'application', 'marketing')" }
531
+ path: { type: "string", description: "Full path to example page (e.g., 'application/dashboards-01/01')" }
403
532
  },
404
- required: ["name"]
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.components || []).map((c) => c.name)
609
+ baseComponents: getBaseComponentNames(fetched.files)
481
610
  };
482
611
  cache.set(cacheKey, component, CACHE_TTL.componentCode);
483
612
  }
484
- return { content: [{ type: "text", text: JSON.stringify(component, null, 2) }] };
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.components || []).map((c) => c.name);
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 "list_examples": {
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
- examples: [
541
- { name: "application", type: "application", description: "Dashboard application example" },
542
- { name: "marketing", type: "marketing", description: "Marketing landing page example" }
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 { name: exampleName } = args;
550
- const cacheKey = `example:${exampleName}`;
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(exampleName);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "untitledui-mcp",
3
- "version": "0.1.1",
3
+ "version": "0.1.4",
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",