untitledui-mcp 0.1.0 → 0.1.1

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/package.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "untitledui-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
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
@@ -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
- }
@@ -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;
package/src/index.ts DELETED
@@ -1,96 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { resolveLicenseKey } from "./utils/license.js";
4
- import { runServer } from "./server.js";
5
- import { UntitledUIClient } from "./api/client.js";
6
-
7
- async function main() {
8
- const args = process.argv.slice(2);
9
-
10
- // Parse CLI arguments
11
- let cliLicenseKey: string | undefined;
12
- let testMode = false;
13
-
14
- for (let i = 0; i < args.length; i++) {
15
- if (args[i] === "--license-key" && args[i + 1]) {
16
- cliLicenseKey = args[i + 1];
17
- i++;
18
- } else if (args[i] === "--test") {
19
- testMode = true;
20
- } else if (args[i] === "--help" || args[i] === "-h") {
21
- console.log(`
22
- mcp-untitledui - MCP server for UntitledUI Pro components
23
-
24
- Usage:
25
- mcp-untitledui [options]
26
-
27
- Options:
28
- --license-key <key> Specify license key (overrides env/config)
29
- --test Test connection and exit
30
- --help, -h Show this help message
31
-
32
- Environment:
33
- UNTITLEDUI_LICENSE_KEY License key (if not using --license-key)
34
-
35
- Config:
36
- ~/.untitledui/config.json Auto-detected from UntitledUI CLI login
37
- `);
38
- process.exit(0);
39
- }
40
- }
41
-
42
- // Resolve license key (optional - some features work without it)
43
- const licenseKey = resolveLicenseKey(cliLicenseKey) || "";
44
-
45
- // Test mode
46
- if (testMode) {
47
- console.log("Testing connection...");
48
-
49
- if (!licenseKey) {
50
- console.error("✗ No license key configured");
51
- console.error("");
52
- console.error("Get your license key:");
53
- console.error(" npx untitledui login");
54
- console.error("");
55
- console.error("Then configure it via:");
56
- console.error(" • Environment: UNTITLEDUI_LICENSE_KEY=<key>");
57
- console.error(" • MCP config env field");
58
- console.error(" • Auto-detected from ~/.untitledui/config.json");
59
- process.exit(1);
60
- }
61
-
62
- const client = new UntitledUIClient(licenseKey);
63
-
64
- const valid = await client.validateLicense();
65
- if (!valid) {
66
- console.error("✗ License key is invalid");
67
- process.exit(1);
68
- }
69
- console.log("✓ License key is valid");
70
-
71
- try {
72
- const types = await client.listComponentTypes();
73
- console.log(`✓ API connection successful`);
74
- console.log(`✓ ${types.length} component types available`);
75
- console.log("✓ Ready to serve");
76
- process.exit(0);
77
- } catch (error) {
78
- console.error("✗ API connection failed:", error);
79
- process.exit(1);
80
- }
81
- }
82
-
83
- // Warn if no license key
84
- if (!licenseKey) {
85
- console.error("Warning: No license key configured. API calls will fail.");
86
- console.error("Run 'npx untitledui login' to authenticate.");
87
- }
88
-
89
- // Run server
90
- await runServer(licenseKey);
91
- }
92
-
93
- main().catch((error) => {
94
- console.error("Fatal error:", error);
95
- process.exit(1);
96
- });
package/src/server.ts DELETED
@@ -1,374 +0,0 @@
1
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
- import {
4
- CallToolRequestSchema,
5
- ListToolsRequestSchema,
6
- } from "@modelcontextprotocol/sdk/types.js";
7
-
8
- import { UntitledUIClient } from "./api/client.js";
9
- import { MemoryCache, CACHE_TTL } from "./cache/memory-cache.js";
10
- import { fuzzySearch, type SearchableItem } from "./utils/search.js";
11
- import { generateDescription } from "./utils/descriptions.js";
12
- import type { ComponentListItem, MCPComponentResponse } from "./api/types.js";
13
-
14
- export function createServer(licenseKey: string) {
15
- const client = new UntitledUIClient(licenseKey);
16
- const cache = new MemoryCache();
17
-
18
- const server = new Server(
19
- {
20
- name: "untitledui-mcp",
21
- version: "0.1.0",
22
- },
23
- {
24
- capabilities: {
25
- tools: {},
26
- },
27
- }
28
- );
29
-
30
- // Helper: Build searchable index
31
- async function buildSearchIndex(): Promise<SearchableItem[]> {
32
- const cacheKey = "search:index";
33
- const cached = cache.get<SearchableItem[]>(cacheKey);
34
- if (cached) return cached;
35
-
36
- const items: SearchableItem[] = [];
37
- const types = await client.listComponentTypes();
38
-
39
- for (const type of types) {
40
- const components = await client.listComponents(type);
41
- for (const comp of components) {
42
- if (comp.type === "dir" && comp.count) {
43
- // Has variants - fetch them
44
- const variants = await client.listComponents(type, comp.name);
45
- for (const variant of variants) {
46
- items.push({
47
- name: variant.name,
48
- type,
49
- fullPath: `${type}/${comp.name}/${variant.name}`,
50
- });
51
- }
52
- } else {
53
- items.push({
54
- name: comp.name,
55
- type,
56
- fullPath: `${type}/${comp.name}`,
57
- });
58
- }
59
- }
60
- }
61
-
62
- cache.set(cacheKey, items, CACHE_TTL.componentList);
63
- return items;
64
- }
65
-
66
- // List available tools
67
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
68
- tools: [
69
- {
70
- name: "list_component_types",
71
- description: "List all available component categories (application, base, marketing, etc.)",
72
- inputSchema: { type: "object", properties: {} },
73
- },
74
- {
75
- name: "list_components",
76
- description: "List components in a category. Use subfolder for variants (e.g., type='application', subfolder='modals')",
77
- inputSchema: {
78
- type: "object",
79
- properties: {
80
- type: { type: "string", description: "Component type (application, base, foundations, marketing, shared-assets)" },
81
- subfolder: { type: "string", description: "Optional subfolder for variants (e.g., 'modals', 'slideout-menus')" },
82
- },
83
- required: ["type"],
84
- },
85
- },
86
- {
87
- name: "search_components",
88
- description: "Search for components by name across all categories",
89
- inputSchema: {
90
- type: "object",
91
- properties: {
92
- query: { type: "string", description: "Search query" },
93
- },
94
- required: ["query"],
95
- },
96
- },
97
- {
98
- name: "get_component",
99
- description: "Get a single component's code. Does NOT include dependencies - use get_component_with_deps for that.",
100
- inputSchema: {
101
- type: "object",
102
- properties: {
103
- type: { type: "string", description: "Component type" },
104
- name: { type: "string", description: "Component name (e.g., 'button' or 'modals/ai-assistant-modal')" },
105
- },
106
- required: ["type", "name"],
107
- },
108
- },
109
- {
110
- name: "get_component_with_deps",
111
- description: "Get a component with all its base component dependencies included",
112
- inputSchema: {
113
- type: "object",
114
- properties: {
115
- type: { type: "string", description: "Component type" },
116
- name: { type: "string", description: "Component name" },
117
- },
118
- required: ["type", "name"],
119
- },
120
- },
121
- {
122
- name: "list_examples",
123
- description: "List available page examples (dashboards, marketing pages, etc.)",
124
- inputSchema: { type: "object", properties: {} },
125
- },
126
- {
127
- name: "get_example",
128
- description: "Get a complete page example with all files",
129
- inputSchema: {
130
- type: "object",
131
- properties: {
132
- name: { type: "string", description: "Example name (e.g., 'application', 'marketing')" },
133
- },
134
- required: ["name"],
135
- },
136
- },
137
- {
138
- name: "validate_license",
139
- description: "Verify that the license key is valid",
140
- inputSchema: { type: "object", properties: {} },
141
- },
142
- {
143
- name: "clear_cache",
144
- description: "Clear cached data. Optionally specify a pattern to clear specific entries.",
145
- inputSchema: {
146
- type: "object",
147
- properties: {
148
- pattern: { type: "string", description: "Optional pattern to match (e.g., 'component:' clears all component cache)" },
149
- },
150
- },
151
- },
152
- ],
153
- }));
154
-
155
- // Handle tool calls
156
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
157
- const { name, arguments: args } = request.params;
158
-
159
- try {
160
- switch (name) {
161
- case "list_component_types": {
162
- const cacheKey = "types";
163
- let types = cache.get<string[]>(cacheKey);
164
- if (!types) {
165
- types = await client.listComponentTypes();
166
- cache.set(cacheKey, types, CACHE_TTL.componentTypes);
167
- }
168
- return { content: [{ type: "text", text: JSON.stringify({ types }, null, 2) }] };
169
- }
170
-
171
- case "list_components": {
172
- const { type, subfolder } = args as { type: string; subfolder?: string };
173
- const cacheKey = subfolder ? `list:${type}:${subfolder}` : `list:${type}`;
174
-
175
- let components = cache.get<ComponentListItem[]>(cacheKey);
176
- if (!components) {
177
- components = await client.listComponents(type, subfolder);
178
- cache.set(cacheKey, components, CACHE_TTL.componentList);
179
- }
180
-
181
- return { content: [{ type: "text", text: JSON.stringify({ type, subfolder, components }, null, 2) }] };
182
- }
183
-
184
- case "search_components": {
185
- const { query } = args as { query: string };
186
- const index = await buildSearchIndex();
187
- const results = fuzzySearch(query, index);
188
- return { content: [{ type: "text", text: JSON.stringify({ query, results }, null, 2) }] };
189
- }
190
-
191
- case "get_component": {
192
- const { type, name: componentName } = args as { type: string; name: string };
193
- const cacheKey = `component:${type}:${componentName}`;
194
-
195
- let component = cache.get<MCPComponentResponse>(cacheKey);
196
- if (!component) {
197
- const fetched = await client.fetchComponent(type, componentName);
198
- if (!fetched) {
199
- // Not found - suggest alternatives
200
- const index = await buildSearchIndex();
201
- const suggestions = fuzzySearch(componentName, index, 5).map(r => r.fullPath);
202
- return {
203
- content: [{
204
- type: "text",
205
- text: JSON.stringify({
206
- error: `Component "${componentName}" not found in ${type}`,
207
- code: "NOT_FOUND",
208
- suggestions,
209
- }, null, 2),
210
- }],
211
- };
212
- }
213
-
214
- component = {
215
- name: fetched.name,
216
- type,
217
- description: generateDescription(fetched.name, type),
218
- files: fetched.files,
219
- dependencies: fetched.dependencies || [],
220
- devDependencies: fetched.devDependencies || [],
221
- baseComponents: (fetched.components || []).map(c => c.name),
222
- };
223
- cache.set(cacheKey, component, CACHE_TTL.componentCode);
224
- }
225
-
226
- return { content: [{ type: "text", text: JSON.stringify(component, null, 2) }] };
227
- }
228
-
229
- case "get_component_with_deps": {
230
- const { type, name: componentName } = args as { type: string; name: string };
231
-
232
- const primary = await client.fetchComponent(type, componentName);
233
- if (!primary) {
234
- const index = await buildSearchIndex();
235
- const suggestions = fuzzySearch(componentName, index, 5).map(r => r.fullPath);
236
- return {
237
- content: [{
238
- type: "text",
239
- text: JSON.stringify({
240
- error: `Component "${componentName}" not found`,
241
- code: "NOT_FOUND",
242
- suggestions,
243
- }, null, 2),
244
- }],
245
- };
246
- }
247
-
248
- // Fetch base components
249
- const baseComponentNames = (primary.components || []).map(c => c.name);
250
- const baseComponents = baseComponentNames.length > 0
251
- ? await client.fetchComponents("base", baseComponentNames)
252
- : [];
253
-
254
- // Deduplicate dependencies
255
- const allDeps = new Set<string>();
256
- const allDevDeps = new Set<string>();
257
-
258
- [primary, ...baseComponents].forEach(c => {
259
- c.dependencies?.forEach(d => allDeps.add(d));
260
- c.devDependencies?.forEach(d => allDevDeps.add(d));
261
- });
262
-
263
- const result = {
264
- primary: {
265
- name: primary.name,
266
- type,
267
- description: generateDescription(primary.name, type),
268
- files: primary.files,
269
- dependencies: primary.dependencies || [],
270
- devDependencies: primary.devDependencies || [],
271
- baseComponents: baseComponentNames,
272
- },
273
- baseComponents: baseComponents.map(c => ({
274
- name: c.name,
275
- type: "base",
276
- description: generateDescription(c.name, "base"),
277
- files: c.files,
278
- dependencies: c.dependencies || [],
279
- devDependencies: c.devDependencies || [],
280
- })),
281
- totalFiles: primary.files.length + baseComponents.reduce((sum, c) => sum + c.files.length, 0),
282
- allDependencies: Array.from(allDeps),
283
- allDevDependencies: Array.from(allDevDeps),
284
- };
285
-
286
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
287
- }
288
-
289
- case "list_examples": {
290
- return {
291
- content: [{
292
- type: "text",
293
- text: JSON.stringify({
294
- examples: [
295
- { name: "application", type: "application", description: "Dashboard application example" },
296
- { name: "marketing", type: "marketing", description: "Marketing landing page example" },
297
- ],
298
- }, null, 2),
299
- }],
300
- };
301
- }
302
-
303
- case "get_example": {
304
- const { name: exampleName } = args as { name: string };
305
- const cacheKey = `example:${exampleName}`;
306
-
307
- let example = cache.get(cacheKey);
308
- if (!example) {
309
- example = await client.fetchExample(exampleName);
310
- if (example) {
311
- cache.set(cacheKey, example, CACHE_TTL.examples);
312
- }
313
- }
314
-
315
- return { content: [{ type: "text", text: JSON.stringify(example, null, 2) }] };
316
- }
317
-
318
- case "validate_license": {
319
- const valid = await client.validateLicense();
320
- return {
321
- content: [{
322
- type: "text",
323
- text: JSON.stringify({
324
- valid,
325
- message: valid ? "License key is valid" : "Invalid or missing license key",
326
- }, null, 2),
327
- }],
328
- };
329
- }
330
-
331
- case "clear_cache": {
332
- const { pattern } = args as { pattern?: string };
333
- let cleared: number;
334
- if (pattern) {
335
- cleared = cache.clearPattern(pattern);
336
- } else {
337
- cleared = cache.size();
338
- cache.clear();
339
- }
340
- return {
341
- content: [{
342
- type: "text",
343
- text: JSON.stringify({ cleared, message: `Cleared ${cleared} cache entries` }, null, 2),
344
- }],
345
- };
346
- }
347
-
348
- default:
349
- return {
350
- content: [{
351
- type: "text",
352
- text: JSON.stringify({ error: `Unknown tool: ${name}`, code: "UNKNOWN_TOOL" }, null, 2),
353
- }],
354
- };
355
- }
356
- } catch (error) {
357
- const message = error instanceof Error ? error.message : String(error);
358
- return {
359
- content: [{
360
- type: "text",
361
- text: JSON.stringify({ error: message, code: "API_ERROR" }, null, 2),
362
- }],
363
- };
364
- }
365
- });
366
-
367
- return server;
368
- }
369
-
370
- export async function runServer(licenseKey: string) {
371
- const server = createServer(licenseKey);
372
- const transport = new StdioServerTransport();
373
- await server.connect(transport);
374
- }
@@ -1,65 +0,0 @@
1
- // Generated descriptions based on component name and type
2
- // Since UntitledUI API doesn't provide descriptions, we generate them
3
-
4
- const TYPE_DESCRIPTIONS: Record<string, string> = {
5
- application: "Application UI component for dashboards and web apps",
6
- base: "Core UI primitive component",
7
- foundations: "Foundational element (icon, logo, visual)",
8
- marketing: "Marketing section component for landing pages",
9
- "shared-assets": "Shared visual asset (illustration, pattern, mockup)",
10
- icons: "Icon component",
11
- };
12
-
13
- const NAME_PATTERNS: [RegExp, string][] = [
14
- [/modal/i, "Modal dialog component"],
15
- [/button/i, "Interactive button component"],
16
- [/input/i, "Form input component"],
17
- [/select/i, "Selection/dropdown component"],
18
- [/table/i, "Data table component"],
19
- [/calendar/i, "Calendar/date component"],
20
- [/date-picker/i, "Date selection component"],
21
- [/sidebar/i, "Sidebar navigation component"],
22
- [/header/i, "Header/navigation component"],
23
- [/footer/i, "Footer section component"],
24
- [/card/i, "Card container component"],
25
- [/avatar/i, "User avatar component"],
26
- [/badge/i, "Badge/label component"],
27
- [/alert/i, "Alert/notification component"],
28
- [/toast/i, "Toast notification component"],
29
- [/dropdown/i, "Dropdown menu component"],
30
- [/tabs/i, "Tabbed interface component"],
31
- [/pagination/i, "Pagination component"],
32
- [/carousel/i, "Carousel/slider component"],
33
- [/chart/i, "Chart/visualization component"],
34
- [/metric/i, "Metrics/statistics component"],
35
- [/form/i, "Form component"],
36
- [/pricing/i, "Pricing section component"],
37
- [/testimonial/i, "Testimonial section component"],
38
- [/feature/i, "Features section component"],
39
- [/cta/i, "Call-to-action section component"],
40
- [/hero/i, "Hero section component"],
41
- [/faq/i, "FAQ section component"],
42
- [/blog/i, "Blog section component"],
43
- [/team/i, "Team section component"],
44
- [/contact/i, "Contact section component"],
45
- [/login/i, "Login/authentication component"],
46
- [/signup/i, "Signup/registration component"],
47
- ];
48
-
49
- export function generateDescription(name: string, type: string): string {
50
- // Check name patterns first
51
- for (const [pattern, description] of NAME_PATTERNS) {
52
- if (pattern.test(name)) {
53
- return description;
54
- }
55
- }
56
-
57
- // Fall back to type description
58
- const typeDesc = TYPE_DESCRIPTIONS[type];
59
- if (typeDesc) {
60
- return `${typeDesc}: ${name.replace(/-/g, " ")}`;
61
- }
62
-
63
- // Generic fallback
64
- return `UI component: ${name.replace(/-/g, " ")}`;
65
- }
@@ -1,45 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
- import { resolveLicenseKey } from "./license.js";
3
- import * as fs from "fs";
4
- import * as os from "os";
5
-
6
- vi.mock("fs");
7
- vi.mock("os");
8
-
9
- describe("resolveLicenseKey", () => {
10
- beforeEach(() => {
11
- vi.resetAllMocks();
12
- delete process.env.UNTITLEDUI_LICENSE_KEY;
13
- });
14
-
15
- it("should prefer CLI argument over env var", () => {
16
- process.env.UNTITLEDUI_LICENSE_KEY = "env-key";
17
- const result = resolveLicenseKey("cli-key");
18
- expect(result).toBe("cli-key");
19
- });
20
-
21
- it("should use env var if no CLI argument", () => {
22
- process.env.UNTITLEDUI_LICENSE_KEY = "env-key";
23
- const result = resolveLicenseKey();
24
- expect(result).toBe("env-key");
25
- });
26
-
27
- it("should read from config file if no env var", () => {
28
- vi.mocked(os.homedir).mockReturnValue("/home/user");
29
- vi.mocked(fs.existsSync).mockReturnValue(true);
30
- vi.mocked(fs.readFileSync).mockReturnValue(
31
- JSON.stringify({ license: "file-key" })
32
- );
33
-
34
- const result = resolveLicenseKey();
35
- expect(result).toBe("file-key");
36
- });
37
-
38
- it("should return undefined if no key found", () => {
39
- vi.mocked(os.homedir).mockReturnValue("/home/user");
40
- vi.mocked(fs.existsSync).mockReturnValue(false);
41
-
42
- const result = resolveLicenseKey();
43
- expect(result).toBeUndefined();
44
- });
45
- });
@@ -1,35 +0,0 @@
1
- import * as fs from "fs";
2
- import * as path from "path";
3
- import * as os from "os";
4
-
5
- function getConfigPath(): string {
6
- return path.join(os.homedir(), ".untitledui", "config.json");
7
- }
8
-
9
- export function resolveLicenseKey(cliArg?: string): string | undefined {
10
- // Priority 1: CLI argument
11
- if (cliArg) {
12
- return cliArg;
13
- }
14
-
15
- // Priority 2: Environment variable
16
- const envKey = process.env.UNTITLEDUI_LICENSE_KEY;
17
- if (envKey) {
18
- return envKey;
19
- }
20
-
21
- // Priority 3: Config file (~/.untitledui/config.json)
22
- try {
23
- const configPath = getConfigPath();
24
- if (fs.existsSync(configPath)) {
25
- const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
26
- if (config.license) {
27
- return config.license;
28
- }
29
- }
30
- } catch {
31
- // Ignore file read errors
32
- }
33
-
34
- return undefined;
35
- }
@@ -1,39 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { fuzzySearch, type SearchableItem } from "./search.js";
3
-
4
- describe("fuzzySearch", () => {
5
- const items: SearchableItem[] = [
6
- { name: "button", type: "base", fullPath: "base/button" },
7
- { name: "date-picker", type: "application", fullPath: "application/date-picker" },
8
- { name: "date-range-picker", type: "application", fullPath: "application/date-range-picker" },
9
- { name: "ai-assistant-modal", type: "application", fullPath: "application/modals/ai-assistant-modal" },
10
- ];
11
-
12
- it("should find exact matches", () => {
13
- const results = fuzzySearch("button", items);
14
- expect(results[0].name).toBe("button");
15
- expect(results[0].matchType).toBe("exact");
16
- });
17
-
18
- it("should find partial matches", () => {
19
- const results = fuzzySearch("date", items);
20
- expect(results.length).toBe(2);
21
- expect(results.every(r => r.name.includes("date"))).toBe(true);
22
- });
23
-
24
- it("should rank exact matches higher", () => {
25
- const results = fuzzySearch("date-picker", items);
26
- expect(results[0].name).toBe("date-picker");
27
- expect(results[0].matchType).toBe("exact");
28
- });
29
-
30
- it("should return empty array for no matches", () => {
31
- const results = fuzzySearch("nonexistent", items);
32
- expect(results).toEqual([]);
33
- });
34
-
35
- it("should limit results", () => {
36
- const results = fuzzySearch("a", items, 2);
37
- expect(results.length).toBeLessThanOrEqual(2);
38
- });
39
- });
@@ -1,71 +0,0 @@
1
- export interface SearchableItem {
2
- name: string;
3
- type: string;
4
- fullPath: string;
5
- }
6
-
7
- export interface SearchResult extends SearchableItem {
8
- matchType: "exact" | "partial";
9
- score: number;
10
- }
11
-
12
- export function fuzzySearch(
13
- query: string,
14
- items: SearchableItem[],
15
- limit = 20
16
- ): SearchResult[] {
17
- const queryLower = query.toLowerCase();
18
-
19
- const results: SearchResult[] = [];
20
-
21
- for (const item of items) {
22
- const nameLower = item.name.toLowerCase();
23
- const fullPathLower = item.fullPath.toLowerCase();
24
-
25
- let score = 0;
26
- let matchType: "exact" | "partial" = "partial";
27
-
28
- // Exact match on name
29
- if (nameLower === queryLower) {
30
- score = 1.0;
31
- matchType = "exact";
32
- }
33
- // Name starts with query
34
- else if (nameLower.startsWith(queryLower)) {
35
- score = 0.9;
36
- }
37
- // Name contains query
38
- else if (nameLower.includes(queryLower)) {
39
- score = 0.7;
40
- }
41
- // Full path contains query
42
- else if (fullPathLower.includes(queryLower)) {
43
- score = 0.5;
44
- }
45
- // Fuzzy: all query chars appear in order
46
- else {
47
- let queryIndex = 0;
48
- for (const char of nameLower) {
49
- if (char === queryLower[queryIndex]) {
50
- queryIndex++;
51
- }
52
- if (queryIndex === queryLower.length) {
53
- score = 0.3;
54
- break;
55
- }
56
- }
57
- }
58
-
59
- if (score > 0) {
60
- results.push({
61
- ...item,
62
- matchType,
63
- score,
64
- });
65
- }
66
- }
67
-
68
- return results
69
- .sort((a, b) => b.score - a.score)
70
- .slice(0, limit);
71
- }
package/tsconfig.json DELETED
@@ -1,16 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "ESNext",
5
- "moduleResolution": "bundler",
6
- "esModuleInterop": true,
7
- "strict": true,
8
- "skipLibCheck": true,
9
- "outDir": "dist",
10
- "rootDir": "src",
11
- "declaration": true,
12
- "resolveJsonModule": true
13
- },
14
- "include": ["src/**/*"],
15
- "exclude": ["node_modules", "dist"]
16
- }