collectui-mcp 1.0.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/README.md +1 -0
- package/bin/server.mjs +3 -0
- package/package.json +28 -0
- package/src/collectui.mjs +222 -0
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# collectui-mcp
|
package/bin/server.mjs
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "collectui-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for browsing Collect UI design inspiration — 14,400+ curated UI designs across 167 categories",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"collectui-mcp": "./bin/server.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"mcp",
|
|
15
|
+
"collectui",
|
|
16
|
+
"design-inspiration",
|
|
17
|
+
"ui-design",
|
|
18
|
+
"dribbble",
|
|
19
|
+
"design-system"
|
|
20
|
+
],
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"author": "jsstech",
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@modelcontextprotocol/sdk": "^1.21.1",
|
|
25
|
+
"cheerio": "^1.0.0",
|
|
26
|
+
"zod": "^3.23.8"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import * as cheerio from 'cheerio';
|
|
5
|
+
|
|
6
|
+
const BASE = 'https://collectui.com';
|
|
7
|
+
const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
|
|
8
|
+
|
|
9
|
+
// Cache categories for 1 hour
|
|
10
|
+
let cachedCategories = null;
|
|
11
|
+
let categoriesCachedAt = 0;
|
|
12
|
+
const CACHE_TTL = 60 * 60 * 1000;
|
|
13
|
+
|
|
14
|
+
async function fetchPage(url) {
|
|
15
|
+
const res = await fetch(url, {
|
|
16
|
+
headers: { 'User-Agent': UA, 'Accept': 'text/html' },
|
|
17
|
+
});
|
|
18
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} fetching ${url}`);
|
|
19
|
+
return res.text();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseCategories(html) {
|
|
23
|
+
const $ = cheerio.load(html);
|
|
24
|
+
const categories = [];
|
|
25
|
+
|
|
26
|
+
$('a[href^="/challenges/"]').each((_, el) => {
|
|
27
|
+
const href = $(el).attr('href');
|
|
28
|
+
const text = $(el).text().trim();
|
|
29
|
+
if (!href || href === '/challenges') return;
|
|
30
|
+
|
|
31
|
+
const slug = href.replace('/challenges/', '').split('?')[0];
|
|
32
|
+
if (!slug || categories.some(c => c.slug === slug)) return;
|
|
33
|
+
|
|
34
|
+
// Try to extract count from text like "Landing Page (1825)"
|
|
35
|
+
const match = text.match(/^(.+?)\s*\((\d+)\)\s*$/);
|
|
36
|
+
const name = match ? match[1].trim() : text;
|
|
37
|
+
const count = match ? parseInt(match[2], 10) : null;
|
|
38
|
+
|
|
39
|
+
categories.push({ name, slug, count, url: `${BASE}/challenges/${slug}` });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return categories;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseDesigns(html, limit) {
|
|
46
|
+
const $ = cheerio.load(html);
|
|
47
|
+
const designs = [];
|
|
48
|
+
|
|
49
|
+
$('img[src*="static.collectui.com/shots"]').each((i, el) => {
|
|
50
|
+
if (designs.length >= limit) return false;
|
|
51
|
+
|
|
52
|
+
const src = $(el).attr('src') || '';
|
|
53
|
+
const alt = $(el).attr('alt') || '';
|
|
54
|
+
const parent = $(el).closest('a');
|
|
55
|
+
const detailHref = parent.attr('href') || '';
|
|
56
|
+
|
|
57
|
+
// Build large image URL from medium
|
|
58
|
+
const largeUrl = src.replace(/-medium(\.\w+)?$/, '-large$1').replace(/-medium$/, '-large');
|
|
59
|
+
|
|
60
|
+
// Find designer nearby
|
|
61
|
+
let designer = '';
|
|
62
|
+
const card = $(el).closest('div, li, article');
|
|
63
|
+
const designerLink = card.find('a[href^="/designers/"]');
|
|
64
|
+
if (designerLink.length) {
|
|
65
|
+
designer = designerLink.text().trim();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Find category nearby
|
|
69
|
+
let category = '';
|
|
70
|
+
const categoryLink = card.find('a[href^="/challenges/"]');
|
|
71
|
+
if (categoryLink.length) {
|
|
72
|
+
category = categoryLink.text().trim();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const detailUrl = detailHref.startsWith('http') ? detailHref
|
|
76
|
+
: detailHref.startsWith('/') ? `${BASE}${detailHref}`
|
|
77
|
+
: '';
|
|
78
|
+
|
|
79
|
+
designs.push({
|
|
80
|
+
imageUrl: src,
|
|
81
|
+
largeImageUrl: largeUrl,
|
|
82
|
+
designer: designer || alt,
|
|
83
|
+
category,
|
|
84
|
+
detailUrl,
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return designs;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function getCategories() {
|
|
92
|
+
const now = Date.now();
|
|
93
|
+
if (cachedCategories && now - categoriesCachedAt < CACHE_TTL) {
|
|
94
|
+
return cachedCategories;
|
|
95
|
+
}
|
|
96
|
+
const html = await fetchPage(BASE);
|
|
97
|
+
cachedCategories = parseCategories(html);
|
|
98
|
+
categoriesCachedAt = now;
|
|
99
|
+
return cachedCategories;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function matchCategories(query, categories) {
|
|
103
|
+
const q = query.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
104
|
+
return categories.filter(c => {
|
|
105
|
+
const name = c.name.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
106
|
+
const slug = c.slug.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
107
|
+
return name.includes(q) || slug.includes(q) || q.includes(name) || q.includes(slug);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function createServer() {
|
|
112
|
+
const server = new McpServer({
|
|
113
|
+
name: 'collectui-mcp',
|
|
114
|
+
version: '1.0.0',
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Tool 1: List categories
|
|
118
|
+
server.tool(
|
|
119
|
+
'collectui_categories',
|
|
120
|
+
'List all Collect UI design challenge categories (167 categories, 14,400+ curated designs)',
|
|
121
|
+
{},
|
|
122
|
+
async () => {
|
|
123
|
+
const categories = await getCategories();
|
|
124
|
+
return {
|
|
125
|
+
content: [{
|
|
126
|
+
type: 'text',
|
|
127
|
+
text: JSON.stringify(categories, null, 2),
|
|
128
|
+
}],
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
// Tool 2: Browse a category
|
|
134
|
+
server.tool(
|
|
135
|
+
'collectui_browse',
|
|
136
|
+
'Browse a Collect UI category and get design inspiration images. Returns image URLs that can be analyzed visually for color palettes, typography, and layout patterns.',
|
|
137
|
+
{
|
|
138
|
+
category: z.string().describe('Category slug (e.g., "landing-page", "monitoring-dashboard", "checkout")'),
|
|
139
|
+
sort: z.enum(['popular', 'newest']).default('popular').describe('Sort order'),
|
|
140
|
+
limit: z.number().min(1).max(50).default(12).describe('Max number of designs to return'),
|
|
141
|
+
},
|
|
142
|
+
async ({ category, sort, limit }) => {
|
|
143
|
+
const url = `${BASE}/challenges/${category}?sortBy=${sort === 'popular' ? 'popularity' : 'newest'}`;
|
|
144
|
+
const html = await fetchPage(url);
|
|
145
|
+
const designs = parseDesigns(html, limit);
|
|
146
|
+
|
|
147
|
+
if (designs.length === 0) {
|
|
148
|
+
return {
|
|
149
|
+
content: [{ type: 'text', text: `No designs found for category "${category}". Use collectui_categories to see available categories.` }],
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
content: [{
|
|
155
|
+
type: 'text',
|
|
156
|
+
text: JSON.stringify({
|
|
157
|
+
category,
|
|
158
|
+
sort,
|
|
159
|
+
count: designs.length,
|
|
160
|
+
designs,
|
|
161
|
+
}, null, 2),
|
|
162
|
+
}],
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
// Tool 3: Search across categories
|
|
168
|
+
server.tool(
|
|
169
|
+
'collectui_search',
|
|
170
|
+
'Search Collect UI for design inspiration by keyword. Finds matching categories and returns top designs from each. Use for domain research (e.g., "dashboard", "e-commerce", "onboarding").',
|
|
171
|
+
{
|
|
172
|
+
query: z.string().describe('Search keyword (e.g., "dashboard", "landing page", "checkout", "music")'),
|
|
173
|
+
limit: z.number().min(1).max(50).default(12).describe('Max total designs to return'),
|
|
174
|
+
},
|
|
175
|
+
async ({ query, limit }) => {
|
|
176
|
+
const categories = await getCategories();
|
|
177
|
+
const matches = matchCategories(query, categories);
|
|
178
|
+
|
|
179
|
+
if (matches.length === 0) {
|
|
180
|
+
return {
|
|
181
|
+
content: [{
|
|
182
|
+
type: 'text',
|
|
183
|
+
text: `No categories match "${query}". Try broader terms. Available: ${categories.slice(0, 20).map(c => c.slug).join(', ')}...`,
|
|
184
|
+
}],
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Distribute limit across matching categories
|
|
189
|
+
const perCategory = Math.max(3, Math.ceil(limit / matches.length));
|
|
190
|
+
const allDesigns = [];
|
|
191
|
+
|
|
192
|
+
for (const cat of matches.slice(0, 5)) {
|
|
193
|
+
if (allDesigns.length >= limit) break;
|
|
194
|
+
try {
|
|
195
|
+
const url = `${BASE}/challenges/${cat.slug}?sortBy=popularity`;
|
|
196
|
+
const html = await fetchPage(url);
|
|
197
|
+
const designs = parseDesigns(html, perCategory);
|
|
198
|
+
allDesigns.push(...designs.map(d => ({ ...d, matchedCategory: cat.name })));
|
|
199
|
+
} catch (e) {
|
|
200
|
+
// Skip failed categories
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
content: [{
|
|
206
|
+
type: 'text',
|
|
207
|
+
text: JSON.stringify({
|
|
208
|
+
query,
|
|
209
|
+
matchedCategories: matches.slice(0, 5).map(c => c.name),
|
|
210
|
+
count: Math.min(allDesigns.length, limit),
|
|
211
|
+
designs: allDesigns.slice(0, limit),
|
|
212
|
+
}, null, 2),
|
|
213
|
+
}],
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
// Start server
|
|
219
|
+
const transport = new StdioServerTransport();
|
|
220
|
+
server.connect(transport);
|
|
221
|
+
console.error('collectui-mcp server running on stdio');
|
|
222
|
+
}
|