untitledui-mcp 0.1.0
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/.env.example +1 -0
- package/README.md +139 -0
- package/assets/cover.png +0 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +688 -0
- package/package.json +37 -0
- package/src/api/client.test.ts +46 -0
- package/src/api/client.ts +117 -0
- package/src/api/endpoints.ts +12 -0
- package/src/api/types.ts +84 -0
- package/src/cache/memory-cache.test.ts +56 -0
- package/src/cache/memory-cache.ts +64 -0
- package/src/index.ts +96 -0
- package/src/server.ts +374 -0
- package/src/utils/descriptions.ts +65 -0
- package/src/utils/license.test.ts +45 -0
- package/src/utils/license.ts +35 -0
- package/src/utils/search.test.ts +39 -0
- package/src/utils/search.ts +71 -0
- package/tsconfig.json +16 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/utils/license.ts
|
|
4
|
+
import * as fs from "fs";
|
|
5
|
+
import * as path from "path";
|
|
6
|
+
import * as os from "os";
|
|
7
|
+
function getConfigPath() {
|
|
8
|
+
return path.join(os.homedir(), ".untitledui", "config.json");
|
|
9
|
+
}
|
|
10
|
+
function resolveLicenseKey(cliArg) {
|
|
11
|
+
if (cliArg) {
|
|
12
|
+
return cliArg;
|
|
13
|
+
}
|
|
14
|
+
const envKey = process.env.UNTITLEDUI_LICENSE_KEY;
|
|
15
|
+
if (envKey) {
|
|
16
|
+
return envKey;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const configPath = getConfigPath();
|
|
20
|
+
if (fs.existsSync(configPath)) {
|
|
21
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
22
|
+
if (config.license) {
|
|
23
|
+
return config.license;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
} catch {
|
|
27
|
+
}
|
|
28
|
+
return void 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// src/server.ts
|
|
32
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
33
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
34
|
+
import {
|
|
35
|
+
CallToolRequestSchema,
|
|
36
|
+
ListToolsRequestSchema
|
|
37
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
38
|
+
|
|
39
|
+
// src/api/endpoints.ts
|
|
40
|
+
var API_BASE = "https://www.untitledui.com/react/api";
|
|
41
|
+
var ENDPOINTS = {
|
|
42
|
+
validateKey: (key) => `${API_BASE}/validate-key?key=${key}`,
|
|
43
|
+
listTypes: (key) => `${API_BASE}/components/list?key=${key}`,
|
|
44
|
+
listComponents: (key, type) => `${API_BASE}/components/list?key=${key}&type=${type}`,
|
|
45
|
+
listSubfolder: (key, type, subfolders) => `${API_BASE}/components/list?key=${key}&type=${type}&subfolders=${subfolders.join(",")}`,
|
|
46
|
+
fetchComponents: `${API_BASE}/components`,
|
|
47
|
+
fetchExample: `${API_BASE}/components/example`
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// src/api/client.ts
|
|
51
|
+
var UntitledUIClient = class {
|
|
52
|
+
constructor(licenseKey) {
|
|
53
|
+
this.licenseKey = licenseKey;
|
|
54
|
+
}
|
|
55
|
+
async validateLicense() {
|
|
56
|
+
try {
|
|
57
|
+
const response = await fetch(ENDPOINTS.validateKey(this.licenseKey));
|
|
58
|
+
return response.status === 200;
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async listComponentTypes() {
|
|
64
|
+
const response = await fetch(ENDPOINTS.listTypes(this.licenseKey));
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
throw new Error(`API error: ${response.status}`);
|
|
67
|
+
}
|
|
68
|
+
const data = await response.json();
|
|
69
|
+
return data.types;
|
|
70
|
+
}
|
|
71
|
+
async listComponents(type, subfolder) {
|
|
72
|
+
let url;
|
|
73
|
+
if (subfolder) {
|
|
74
|
+
url = ENDPOINTS.listSubfolder(this.licenseKey, type, [subfolder]);
|
|
75
|
+
} else {
|
|
76
|
+
url = ENDPOINTS.listComponents(this.licenseKey, type);
|
|
77
|
+
}
|
|
78
|
+
const response = await fetch(url);
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
throw new Error(`API error: ${response.status}`);
|
|
81
|
+
}
|
|
82
|
+
const data = await response.json();
|
|
83
|
+
if (subfolder && Array.isArray(data.components)) {
|
|
84
|
+
const subfolderData = data.components[0];
|
|
85
|
+
if (subfolderData && typeof subfolderData === "object" && subfolder in subfolderData) {
|
|
86
|
+
return subfolderData[subfolder];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return data.components;
|
|
90
|
+
}
|
|
91
|
+
async fetchComponent(type, name) {
|
|
92
|
+
const response = await fetch(ENDPOINTS.fetchComponents, {
|
|
93
|
+
method: "POST",
|
|
94
|
+
headers: { "Content-Type": "application/json" },
|
|
95
|
+
body: JSON.stringify({
|
|
96
|
+
type,
|
|
97
|
+
components: [name],
|
|
98
|
+
key: this.licenseKey
|
|
99
|
+
})
|
|
100
|
+
});
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
throw new Error(`API error: ${response.status}`);
|
|
103
|
+
}
|
|
104
|
+
const data = await response.json();
|
|
105
|
+
if (data.pro && data.pro.length > 0) {
|
|
106
|
+
throw new Error(`PRO access required for: ${data.pro.join(", ")}`);
|
|
107
|
+
}
|
|
108
|
+
return data.components[0] || null;
|
|
109
|
+
}
|
|
110
|
+
async fetchComponents(type, names) {
|
|
111
|
+
const response = await fetch(ENDPOINTS.fetchComponents, {
|
|
112
|
+
method: "POST",
|
|
113
|
+
headers: { "Content-Type": "application/json" },
|
|
114
|
+
body: JSON.stringify({
|
|
115
|
+
type,
|
|
116
|
+
components: names,
|
|
117
|
+
key: this.licenseKey
|
|
118
|
+
})
|
|
119
|
+
});
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
throw new Error(`API error: ${response.status}`);
|
|
122
|
+
}
|
|
123
|
+
const data = await response.json();
|
|
124
|
+
if (data.pro && data.pro.length > 0) {
|
|
125
|
+
throw new Error(`PRO access required for: ${data.pro.join(", ")}`);
|
|
126
|
+
}
|
|
127
|
+
return data.components;
|
|
128
|
+
}
|
|
129
|
+
async fetchExample(name) {
|
|
130
|
+
const response = await fetch(ENDPOINTS.fetchExample, {
|
|
131
|
+
method: "POST",
|
|
132
|
+
headers: { "Content-Type": "application/json" },
|
|
133
|
+
body: JSON.stringify({
|
|
134
|
+
example: name,
|
|
135
|
+
key: this.licenseKey
|
|
136
|
+
})
|
|
137
|
+
});
|
|
138
|
+
return response.json();
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// src/cache/memory-cache.ts
|
|
143
|
+
var MemoryCache = class {
|
|
144
|
+
cache = /* @__PURE__ */ new Map();
|
|
145
|
+
set(key, data, ttlSeconds) {
|
|
146
|
+
this.cache.set(key, {
|
|
147
|
+
data,
|
|
148
|
+
expiresAt: Date.now() + ttlSeconds * 1e3
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
get(key) {
|
|
152
|
+
const entry = this.cache.get(key);
|
|
153
|
+
if (!entry) return void 0;
|
|
154
|
+
if (Date.now() > entry.expiresAt) {
|
|
155
|
+
this.cache.delete(key);
|
|
156
|
+
return void 0;
|
|
157
|
+
}
|
|
158
|
+
return entry.data;
|
|
159
|
+
}
|
|
160
|
+
has(key) {
|
|
161
|
+
return this.get(key) !== void 0;
|
|
162
|
+
}
|
|
163
|
+
clear() {
|
|
164
|
+
this.cache.clear();
|
|
165
|
+
}
|
|
166
|
+
clearPattern(pattern) {
|
|
167
|
+
let cleared = 0;
|
|
168
|
+
for (const key of this.cache.keys()) {
|
|
169
|
+
if (key.startsWith(pattern)) {
|
|
170
|
+
this.cache.delete(key);
|
|
171
|
+
cleared++;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return cleared;
|
|
175
|
+
}
|
|
176
|
+
size() {
|
|
177
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
178
|
+
if (Date.now() > entry.expiresAt) {
|
|
179
|
+
this.cache.delete(key);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return this.cache.size;
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
var CACHE_TTL = {
|
|
186
|
+
componentTypes: 3600,
|
|
187
|
+
componentList: 3600,
|
|
188
|
+
componentCode: 86400,
|
|
189
|
+
searchResults: 1800,
|
|
190
|
+
examples: 86400,
|
|
191
|
+
licenseValidation: 300
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// src/utils/search.ts
|
|
195
|
+
function fuzzySearch(query, items, limit = 20) {
|
|
196
|
+
const queryLower = query.toLowerCase();
|
|
197
|
+
const results = [];
|
|
198
|
+
for (const item of items) {
|
|
199
|
+
const nameLower = item.name.toLowerCase();
|
|
200
|
+
const fullPathLower = item.fullPath.toLowerCase();
|
|
201
|
+
let score = 0;
|
|
202
|
+
let matchType = "partial";
|
|
203
|
+
if (nameLower === queryLower) {
|
|
204
|
+
score = 1;
|
|
205
|
+
matchType = "exact";
|
|
206
|
+
} else if (nameLower.startsWith(queryLower)) {
|
|
207
|
+
score = 0.9;
|
|
208
|
+
} else if (nameLower.includes(queryLower)) {
|
|
209
|
+
score = 0.7;
|
|
210
|
+
} else if (fullPathLower.includes(queryLower)) {
|
|
211
|
+
score = 0.5;
|
|
212
|
+
} else {
|
|
213
|
+
let queryIndex = 0;
|
|
214
|
+
for (const char of nameLower) {
|
|
215
|
+
if (char === queryLower[queryIndex]) {
|
|
216
|
+
queryIndex++;
|
|
217
|
+
}
|
|
218
|
+
if (queryIndex === queryLower.length) {
|
|
219
|
+
score = 0.3;
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (score > 0) {
|
|
225
|
+
results.push({
|
|
226
|
+
...item,
|
|
227
|
+
matchType,
|
|
228
|
+
score
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return results.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// src/utils/descriptions.ts
|
|
236
|
+
var TYPE_DESCRIPTIONS = {
|
|
237
|
+
application: "Application UI component for dashboards and web apps",
|
|
238
|
+
base: "Core UI primitive component",
|
|
239
|
+
foundations: "Foundational element (icon, logo, visual)",
|
|
240
|
+
marketing: "Marketing section component for landing pages",
|
|
241
|
+
"shared-assets": "Shared visual asset (illustration, pattern, mockup)",
|
|
242
|
+
icons: "Icon component"
|
|
243
|
+
};
|
|
244
|
+
var NAME_PATTERNS = [
|
|
245
|
+
[/modal/i, "Modal dialog component"],
|
|
246
|
+
[/button/i, "Interactive button component"],
|
|
247
|
+
[/input/i, "Form input component"],
|
|
248
|
+
[/select/i, "Selection/dropdown component"],
|
|
249
|
+
[/table/i, "Data table component"],
|
|
250
|
+
[/calendar/i, "Calendar/date component"],
|
|
251
|
+
[/date-picker/i, "Date selection component"],
|
|
252
|
+
[/sidebar/i, "Sidebar navigation component"],
|
|
253
|
+
[/header/i, "Header/navigation component"],
|
|
254
|
+
[/footer/i, "Footer section component"],
|
|
255
|
+
[/card/i, "Card container component"],
|
|
256
|
+
[/avatar/i, "User avatar component"],
|
|
257
|
+
[/badge/i, "Badge/label component"],
|
|
258
|
+
[/alert/i, "Alert/notification component"],
|
|
259
|
+
[/toast/i, "Toast notification component"],
|
|
260
|
+
[/dropdown/i, "Dropdown menu component"],
|
|
261
|
+
[/tabs/i, "Tabbed interface component"],
|
|
262
|
+
[/pagination/i, "Pagination component"],
|
|
263
|
+
[/carousel/i, "Carousel/slider component"],
|
|
264
|
+
[/chart/i, "Chart/visualization component"],
|
|
265
|
+
[/metric/i, "Metrics/statistics component"],
|
|
266
|
+
[/form/i, "Form component"],
|
|
267
|
+
[/pricing/i, "Pricing section component"],
|
|
268
|
+
[/testimonial/i, "Testimonial section component"],
|
|
269
|
+
[/feature/i, "Features section component"],
|
|
270
|
+
[/cta/i, "Call-to-action section component"],
|
|
271
|
+
[/hero/i, "Hero section component"],
|
|
272
|
+
[/faq/i, "FAQ section component"],
|
|
273
|
+
[/blog/i, "Blog section component"],
|
|
274
|
+
[/team/i, "Team section component"],
|
|
275
|
+
[/contact/i, "Contact section component"],
|
|
276
|
+
[/login/i, "Login/authentication component"],
|
|
277
|
+
[/signup/i, "Signup/registration component"]
|
|
278
|
+
];
|
|
279
|
+
function generateDescription(name, type) {
|
|
280
|
+
for (const [pattern, description] of NAME_PATTERNS) {
|
|
281
|
+
if (pattern.test(name)) {
|
|
282
|
+
return description;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const typeDesc = TYPE_DESCRIPTIONS[type];
|
|
286
|
+
if (typeDesc) {
|
|
287
|
+
return `${typeDesc}: ${name.replace(/-/g, " ")}`;
|
|
288
|
+
}
|
|
289
|
+
return `UI component: ${name.replace(/-/g, " ")}`;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/server.ts
|
|
293
|
+
function createServer(licenseKey) {
|
|
294
|
+
const client = new UntitledUIClient(licenseKey);
|
|
295
|
+
const cache = new MemoryCache();
|
|
296
|
+
const server = new Server(
|
|
297
|
+
{
|
|
298
|
+
name: "untitledui-mcp",
|
|
299
|
+
version: "0.1.0"
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
capabilities: {
|
|
303
|
+
tools: {}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
);
|
|
307
|
+
async function buildSearchIndex() {
|
|
308
|
+
const cacheKey = "search:index";
|
|
309
|
+
const cached = cache.get(cacheKey);
|
|
310
|
+
if (cached) return cached;
|
|
311
|
+
const items = [];
|
|
312
|
+
const types = await client.listComponentTypes();
|
|
313
|
+
for (const type of types) {
|
|
314
|
+
const components = await client.listComponents(type);
|
|
315
|
+
for (const comp of components) {
|
|
316
|
+
if (comp.type === "dir" && comp.count) {
|
|
317
|
+
const variants = await client.listComponents(type, comp.name);
|
|
318
|
+
for (const variant of variants) {
|
|
319
|
+
items.push({
|
|
320
|
+
name: variant.name,
|
|
321
|
+
type,
|
|
322
|
+
fullPath: `${type}/${comp.name}/${variant.name}`
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
} else {
|
|
326
|
+
items.push({
|
|
327
|
+
name: comp.name,
|
|
328
|
+
type,
|
|
329
|
+
fullPath: `${type}/${comp.name}`
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
cache.set(cacheKey, items, CACHE_TTL.componentList);
|
|
335
|
+
return items;
|
|
336
|
+
}
|
|
337
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
338
|
+
tools: [
|
|
339
|
+
{
|
|
340
|
+
name: "list_component_types",
|
|
341
|
+
description: "List all available component categories (application, base, marketing, etc.)",
|
|
342
|
+
inputSchema: { type: "object", properties: {} }
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
name: "list_components",
|
|
346
|
+
description: "List components in a category. Use subfolder for variants (e.g., type='application', subfolder='modals')",
|
|
347
|
+
inputSchema: {
|
|
348
|
+
type: "object",
|
|
349
|
+
properties: {
|
|
350
|
+
type: { type: "string", description: "Component type (application, base, foundations, marketing, shared-assets)" },
|
|
351
|
+
subfolder: { type: "string", description: "Optional subfolder for variants (e.g., 'modals', 'slideout-menus')" }
|
|
352
|
+
},
|
|
353
|
+
required: ["type"]
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
name: "search_components",
|
|
358
|
+
description: "Search for components by name across all categories",
|
|
359
|
+
inputSchema: {
|
|
360
|
+
type: "object",
|
|
361
|
+
properties: {
|
|
362
|
+
query: { type: "string", description: "Search query" }
|
|
363
|
+
},
|
|
364
|
+
required: ["query"]
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
name: "get_component",
|
|
369
|
+
description: "Get a single component's code. Does NOT include dependencies - use get_component_with_deps for that.",
|
|
370
|
+
inputSchema: {
|
|
371
|
+
type: "object",
|
|
372
|
+
properties: {
|
|
373
|
+
type: { type: "string", description: "Component type" },
|
|
374
|
+
name: { type: "string", description: "Component name (e.g., 'button' or 'modals/ai-assistant-modal')" }
|
|
375
|
+
},
|
|
376
|
+
required: ["type", "name"]
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
name: "get_component_with_deps",
|
|
381
|
+
description: "Get a component with all its base component dependencies included",
|
|
382
|
+
inputSchema: {
|
|
383
|
+
type: "object",
|
|
384
|
+
properties: {
|
|
385
|
+
type: { type: "string", description: "Component type" },
|
|
386
|
+
name: { type: "string", description: "Component name" }
|
|
387
|
+
},
|
|
388
|
+
required: ["type", "name"]
|
|
389
|
+
}
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
name: "list_examples",
|
|
393
|
+
description: "List available page examples (dashboards, marketing pages, etc.)",
|
|
394
|
+
inputSchema: { type: "object", properties: {} }
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
name: "get_example",
|
|
398
|
+
description: "Get a complete page example with all files",
|
|
399
|
+
inputSchema: {
|
|
400
|
+
type: "object",
|
|
401
|
+
properties: {
|
|
402
|
+
name: { type: "string", description: "Example name (e.g., 'application', 'marketing')" }
|
|
403
|
+
},
|
|
404
|
+
required: ["name"]
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
name: "validate_license",
|
|
409
|
+
description: "Verify that the license key is valid",
|
|
410
|
+
inputSchema: { type: "object", properties: {} }
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
name: "clear_cache",
|
|
414
|
+
description: "Clear cached data. Optionally specify a pattern to clear specific entries.",
|
|
415
|
+
inputSchema: {
|
|
416
|
+
type: "object",
|
|
417
|
+
properties: {
|
|
418
|
+
pattern: { type: "string", description: "Optional pattern to match (e.g., 'component:' clears all component cache)" }
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
]
|
|
423
|
+
}));
|
|
424
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
425
|
+
const { name, arguments: args } = request.params;
|
|
426
|
+
try {
|
|
427
|
+
switch (name) {
|
|
428
|
+
case "list_component_types": {
|
|
429
|
+
const cacheKey = "types";
|
|
430
|
+
let types = cache.get(cacheKey);
|
|
431
|
+
if (!types) {
|
|
432
|
+
types = await client.listComponentTypes();
|
|
433
|
+
cache.set(cacheKey, types, CACHE_TTL.componentTypes);
|
|
434
|
+
}
|
|
435
|
+
return { content: [{ type: "text", text: JSON.stringify({ types }, null, 2) }] };
|
|
436
|
+
}
|
|
437
|
+
case "list_components": {
|
|
438
|
+
const { type, subfolder } = args;
|
|
439
|
+
const cacheKey = subfolder ? `list:${type}:${subfolder}` : `list:${type}`;
|
|
440
|
+
let components = cache.get(cacheKey);
|
|
441
|
+
if (!components) {
|
|
442
|
+
components = await client.listComponents(type, subfolder);
|
|
443
|
+
cache.set(cacheKey, components, CACHE_TTL.componentList);
|
|
444
|
+
}
|
|
445
|
+
return { content: [{ type: "text", text: JSON.stringify({ type, subfolder, components }, null, 2) }] };
|
|
446
|
+
}
|
|
447
|
+
case "search_components": {
|
|
448
|
+
const { query } = args;
|
|
449
|
+
const index = await buildSearchIndex();
|
|
450
|
+
const results = fuzzySearch(query, index);
|
|
451
|
+
return { content: [{ type: "text", text: JSON.stringify({ query, results }, null, 2) }] };
|
|
452
|
+
}
|
|
453
|
+
case "get_component": {
|
|
454
|
+
const { type, name: componentName } = args;
|
|
455
|
+
const cacheKey = `component:${type}:${componentName}`;
|
|
456
|
+
let component = cache.get(cacheKey);
|
|
457
|
+
if (!component) {
|
|
458
|
+
const fetched = await client.fetchComponent(type, componentName);
|
|
459
|
+
if (!fetched) {
|
|
460
|
+
const index = await buildSearchIndex();
|
|
461
|
+
const suggestions = fuzzySearch(componentName, index, 5).map((r) => r.fullPath);
|
|
462
|
+
return {
|
|
463
|
+
content: [{
|
|
464
|
+
type: "text",
|
|
465
|
+
text: JSON.stringify({
|
|
466
|
+
error: `Component "${componentName}" not found in ${type}`,
|
|
467
|
+
code: "NOT_FOUND",
|
|
468
|
+
suggestions
|
|
469
|
+
}, null, 2)
|
|
470
|
+
}]
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
component = {
|
|
474
|
+
name: fetched.name,
|
|
475
|
+
type,
|
|
476
|
+
description: generateDescription(fetched.name, type),
|
|
477
|
+
files: fetched.files,
|
|
478
|
+
dependencies: fetched.dependencies || [],
|
|
479
|
+
devDependencies: fetched.devDependencies || [],
|
|
480
|
+
baseComponents: (fetched.components || []).map((c) => c.name)
|
|
481
|
+
};
|
|
482
|
+
cache.set(cacheKey, component, CACHE_TTL.componentCode);
|
|
483
|
+
}
|
|
484
|
+
return { content: [{ type: "text", text: JSON.stringify(component, null, 2) }] };
|
|
485
|
+
}
|
|
486
|
+
case "get_component_with_deps": {
|
|
487
|
+
const { type, name: componentName } = args;
|
|
488
|
+
const primary = await client.fetchComponent(type, componentName);
|
|
489
|
+
if (!primary) {
|
|
490
|
+
const index = await buildSearchIndex();
|
|
491
|
+
const suggestions = fuzzySearch(componentName, index, 5).map((r) => r.fullPath);
|
|
492
|
+
return {
|
|
493
|
+
content: [{
|
|
494
|
+
type: "text",
|
|
495
|
+
text: JSON.stringify({
|
|
496
|
+
error: `Component "${componentName}" not found`,
|
|
497
|
+
code: "NOT_FOUND",
|
|
498
|
+
suggestions
|
|
499
|
+
}, null, 2)
|
|
500
|
+
}]
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
const baseComponentNames = (primary.components || []).map((c) => c.name);
|
|
504
|
+
const baseComponents = baseComponentNames.length > 0 ? await client.fetchComponents("base", baseComponentNames) : [];
|
|
505
|
+
const allDeps = /* @__PURE__ */ new Set();
|
|
506
|
+
const allDevDeps = /* @__PURE__ */ new Set();
|
|
507
|
+
[primary, ...baseComponents].forEach((c) => {
|
|
508
|
+
c.dependencies?.forEach((d) => allDeps.add(d));
|
|
509
|
+
c.devDependencies?.forEach((d) => allDevDeps.add(d));
|
|
510
|
+
});
|
|
511
|
+
const result = {
|
|
512
|
+
primary: {
|
|
513
|
+
name: primary.name,
|
|
514
|
+
type,
|
|
515
|
+
description: generateDescription(primary.name, type),
|
|
516
|
+
files: primary.files,
|
|
517
|
+
dependencies: primary.dependencies || [],
|
|
518
|
+
devDependencies: primary.devDependencies || [],
|
|
519
|
+
baseComponents: baseComponentNames
|
|
520
|
+
},
|
|
521
|
+
baseComponents: baseComponents.map((c) => ({
|
|
522
|
+
name: c.name,
|
|
523
|
+
type: "base",
|
|
524
|
+
description: generateDescription(c.name, "base"),
|
|
525
|
+
files: c.files,
|
|
526
|
+
dependencies: c.dependencies || [],
|
|
527
|
+
devDependencies: c.devDependencies || []
|
|
528
|
+
})),
|
|
529
|
+
totalFiles: primary.files.length + baseComponents.reduce((sum, c) => sum + c.files.length, 0),
|
|
530
|
+
allDependencies: Array.from(allDeps),
|
|
531
|
+
allDevDependencies: Array.from(allDevDeps)
|
|
532
|
+
};
|
|
533
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
534
|
+
}
|
|
535
|
+
case "list_examples": {
|
|
536
|
+
return {
|
|
537
|
+
content: [{
|
|
538
|
+
type: "text",
|
|
539
|
+
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
|
+
]
|
|
544
|
+
}, null, 2)
|
|
545
|
+
}]
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
case "get_example": {
|
|
549
|
+
const { name: exampleName } = args;
|
|
550
|
+
const cacheKey = `example:${exampleName}`;
|
|
551
|
+
let example = cache.get(cacheKey);
|
|
552
|
+
if (!example) {
|
|
553
|
+
example = await client.fetchExample(exampleName);
|
|
554
|
+
if (example) {
|
|
555
|
+
cache.set(cacheKey, example, CACHE_TTL.examples);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return { content: [{ type: "text", text: JSON.stringify(example, null, 2) }] };
|
|
559
|
+
}
|
|
560
|
+
case "validate_license": {
|
|
561
|
+
const valid = await client.validateLicense();
|
|
562
|
+
return {
|
|
563
|
+
content: [{
|
|
564
|
+
type: "text",
|
|
565
|
+
text: JSON.stringify({
|
|
566
|
+
valid,
|
|
567
|
+
message: valid ? "License key is valid" : "Invalid or missing license key"
|
|
568
|
+
}, null, 2)
|
|
569
|
+
}]
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
case "clear_cache": {
|
|
573
|
+
const { pattern } = args;
|
|
574
|
+
let cleared;
|
|
575
|
+
if (pattern) {
|
|
576
|
+
cleared = cache.clearPattern(pattern);
|
|
577
|
+
} else {
|
|
578
|
+
cleared = cache.size();
|
|
579
|
+
cache.clear();
|
|
580
|
+
}
|
|
581
|
+
return {
|
|
582
|
+
content: [{
|
|
583
|
+
type: "text",
|
|
584
|
+
text: JSON.stringify({ cleared, message: `Cleared ${cleared} cache entries` }, null, 2)
|
|
585
|
+
}]
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
default:
|
|
589
|
+
return {
|
|
590
|
+
content: [{
|
|
591
|
+
type: "text",
|
|
592
|
+
text: JSON.stringify({ error: `Unknown tool: ${name}`, code: "UNKNOWN_TOOL" }, null, 2)
|
|
593
|
+
}]
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
} catch (error) {
|
|
597
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
598
|
+
return {
|
|
599
|
+
content: [{
|
|
600
|
+
type: "text",
|
|
601
|
+
text: JSON.stringify({ error: message, code: "API_ERROR" }, null, 2)
|
|
602
|
+
}]
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
return server;
|
|
607
|
+
}
|
|
608
|
+
async function runServer(licenseKey) {
|
|
609
|
+
const server = createServer(licenseKey);
|
|
610
|
+
const transport = new StdioServerTransport();
|
|
611
|
+
await server.connect(transport);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// src/index.ts
|
|
615
|
+
async function main() {
|
|
616
|
+
const args = process.argv.slice(2);
|
|
617
|
+
let cliLicenseKey;
|
|
618
|
+
let testMode = false;
|
|
619
|
+
for (let i = 0; i < args.length; i++) {
|
|
620
|
+
if (args[i] === "--license-key" && args[i + 1]) {
|
|
621
|
+
cliLicenseKey = args[i + 1];
|
|
622
|
+
i++;
|
|
623
|
+
} else if (args[i] === "--test") {
|
|
624
|
+
testMode = true;
|
|
625
|
+
} else if (args[i] === "--help" || args[i] === "-h") {
|
|
626
|
+
console.log(`
|
|
627
|
+
mcp-untitledui - MCP server for UntitledUI Pro components
|
|
628
|
+
|
|
629
|
+
Usage:
|
|
630
|
+
mcp-untitledui [options]
|
|
631
|
+
|
|
632
|
+
Options:
|
|
633
|
+
--license-key <key> Specify license key (overrides env/config)
|
|
634
|
+
--test Test connection and exit
|
|
635
|
+
--help, -h Show this help message
|
|
636
|
+
|
|
637
|
+
Environment:
|
|
638
|
+
UNTITLEDUI_LICENSE_KEY License key (if not using --license-key)
|
|
639
|
+
|
|
640
|
+
Config:
|
|
641
|
+
~/.untitledui/config.json Auto-detected from UntitledUI CLI login
|
|
642
|
+
`);
|
|
643
|
+
process.exit(0);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
const licenseKey = resolveLicenseKey(cliLicenseKey) || "";
|
|
647
|
+
if (testMode) {
|
|
648
|
+
console.log("Testing connection...");
|
|
649
|
+
if (!licenseKey) {
|
|
650
|
+
console.error("\u2717 No license key configured");
|
|
651
|
+
console.error("");
|
|
652
|
+
console.error("Get your license key:");
|
|
653
|
+
console.error(" npx untitledui login");
|
|
654
|
+
console.error("");
|
|
655
|
+
console.error("Then configure it via:");
|
|
656
|
+
console.error(" \u2022 Environment: UNTITLEDUI_LICENSE_KEY=<key>");
|
|
657
|
+
console.error(" \u2022 MCP config env field");
|
|
658
|
+
console.error(" \u2022 Auto-detected from ~/.untitledui/config.json");
|
|
659
|
+
process.exit(1);
|
|
660
|
+
}
|
|
661
|
+
const client = new UntitledUIClient(licenseKey);
|
|
662
|
+
const valid = await client.validateLicense();
|
|
663
|
+
if (!valid) {
|
|
664
|
+
console.error("\u2717 License key is invalid");
|
|
665
|
+
process.exit(1);
|
|
666
|
+
}
|
|
667
|
+
console.log("\u2713 License key is valid");
|
|
668
|
+
try {
|
|
669
|
+
const types = await client.listComponentTypes();
|
|
670
|
+
console.log(`\u2713 API connection successful`);
|
|
671
|
+
console.log(`\u2713 ${types.length} component types available`);
|
|
672
|
+
console.log("\u2713 Ready to serve");
|
|
673
|
+
process.exit(0);
|
|
674
|
+
} catch (error) {
|
|
675
|
+
console.error("\u2717 API connection failed:", error);
|
|
676
|
+
process.exit(1);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
if (!licenseKey) {
|
|
680
|
+
console.error("Warning: No license key configured. API calls will fail.");
|
|
681
|
+
console.error("Run 'npx untitledui login' to authenticate.");
|
|
682
|
+
}
|
|
683
|
+
await runServer(licenseKey);
|
|
684
|
+
}
|
|
685
|
+
main().catch((error) => {
|
|
686
|
+
console.error("Fatal error:", error);
|
|
687
|
+
process.exit(1);
|
|
688
|
+
});
|