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 ADDED
@@ -0,0 +1 @@
1
+ # collectui-mcp
package/bin/server.mjs ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { createServer } from '../src/collectui.mjs';
3
+ createServer();
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
+ }