awess 0.2.0 → 0.3.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.ts +260 -203
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "awess",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "MCP server for searching curated awesome lists and their contents",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
package/src/index.ts CHANGED
@@ -3,7 +3,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import MiniSearch from "minisearch";
5
5
  import { z } from "zod";
6
- import type { ItemsIndex } from "./types";
6
+ import type { ItemsIndex, Item } from "./types";
7
7
 
8
8
  interface AwesomeList {
9
9
  repo: string;
@@ -14,6 +14,11 @@ interface AwesomeList {
14
14
  source: string;
15
15
  }
16
16
 
17
+ interface IndexedItem extends Item {
18
+ id: number;
19
+ listRepo: string;
20
+ }
21
+
17
22
  // CDN URLs - jsDelivr primary (faster, global CDN), GitHub raw fallback
18
23
  const JSDELIVR_URL = "https://cdn.jsdelivr.net/gh/arimxyer/ass@main/data";
19
24
  const GITHUB_RAW_URL = "https://raw.githubusercontent.com/arimxyer/ass/main/data";
@@ -81,8 +86,8 @@ async function loadGzippedData<T>(filename: string): Promise<T> {
81
86
  // Load curated data
82
87
  const lists: AwesomeList[] = await loadData("lists.json");
83
88
 
84
- // Initialize search index
85
- const search = new MiniSearch<AwesomeList>({
89
+ // Initialize list search index
90
+ const listSearch = new MiniSearch<AwesomeList>({
86
91
  fields: ["name", "repo", "description"],
87
92
  storeFields: ["repo", "name", "stars", "description", "pushed_at", "source"],
88
93
  searchOptions: {
@@ -92,235 +97,216 @@ const search = new MiniSearch<AwesomeList>({
92
97
  },
93
98
  });
94
99
 
95
- // Index all lists with repo as id
96
- search.addAll(lists.map((list, i) => ({ id: i, ...list })));
100
+ // Index all lists
101
+ listSearch.addAll(lists.map((list, i) => ({ id: i, ...list })));
97
102
 
98
- // Create MCP server
99
- const server = new McpServer({
100
- name: "ass",
101
- version: "0.1.0",
102
- });
103
+ // Load items index
104
+ let itemsIndex: ItemsIndex | null = null;
105
+ let allItems: IndexedItem[] = [];
106
+ let itemSearch: MiniSearch<IndexedItem> | null = null;
103
107
 
104
- // Tool: Search awesome lists
105
- server.tool(
106
- "search",
107
- "Search curated awesome lists by keyword. Returns matching lists sorted by relevance and stars.",
108
- {
109
- query: z.string().describe("Search query (e.g., 'rust', 'machine learning', 'react')"),
110
- limit: z.number().optional().describe("Maximum results to return (default: 10)"),
111
- minStars: z.number().optional().describe("Minimum star count filter (default: 0)"),
112
- },
113
- async ({ query, limit = 10, minStars = 0 }) => {
114
- const results = search
115
- .search(query)
116
- .filter((r) => r.stars >= minStars)
117
- .slice(0, limit)
118
- .map((r) => ({
119
- repo: r.repo,
120
- name: r.name,
121
- stars: r.stars,
122
- description: r.description,
123
- lastUpdated: r.pushed_at,
124
- score: r.score,
125
- }));
108
+ try {
109
+ itemsIndex = await loadGzippedData<ItemsIndex>("items.json.gz");
126
110
 
127
- return {
128
- content: [
129
- {
130
- type: "text",
131
- text: JSON.stringify(results, null, 2),
132
- },
133
- ],
134
- };
111
+ // Build flat list of all items for global search
112
+ let itemId = 0;
113
+ for (const [listRepo, entry] of Object.entries(itemsIndex.lists)) {
114
+ for (const item of entry.items) {
115
+ allItems.push({ ...item, id: itemId++, listRepo });
116
+ }
135
117
  }
136
- );
137
118
 
138
- // Tool: Get list details
119
+ // Initialize item search index
120
+ itemSearch = new MiniSearch<IndexedItem>({
121
+ fields: ["name", "description", "category"],
122
+ storeFields: ["name", "url", "description", "category", "subcategory", "github", "listRepo"],
123
+ searchOptions: {
124
+ boost: { name: 2, category: 1.5, description: 1 },
125
+ fuzzy: 0.2,
126
+ prefix: true,
127
+ },
128
+ });
129
+
130
+ itemSearch.addAll(allItems);
131
+
132
+ console.error(`Loaded ${itemsIndex.itemCount} items from ${itemsIndex.listCount} lists`);
133
+ } catch {
134
+ console.error("No items.json.gz found - item search will be unavailable");
135
+ }
136
+
137
+ // Create MCP server
138
+ const server = new McpServer({
139
+ name: "awess",
140
+ version: "0.3.0",
141
+ });
142
+
143
+ // Tool: Search awesome lists (unified search, top_lists, get_list)
139
144
  server.tool(
140
- "get_list",
141
- "Get details for a specific awesome list by repository name",
145
+ "search_lists",
146
+ "Search curated awesome lists. Returns lists sorted by relevance (if query provided) or stars.",
142
147
  {
143
- repo: z.string().describe("Repository name (e.g., 'sindresorhus/awesome')"),
148
+ query: z.string().optional().describe("Search query (e.g., 'rust', 'machine learning')"),
149
+ repo: z.string().optional().describe("Exact repo lookup (e.g., 'sindresorhus/awesome')"),
150
+ sortBy: z.enum(["relevance", "stars", "updated"]).optional().describe("Sort order (default: 'stars', or 'relevance' if query provided)"),
151
+ minStars: z.number().optional().describe("Minimum star count filter"),
152
+ limit: z.number().optional().describe("Maximum results (default: 20)"),
144
153
  },
145
- async ({ repo }) => {
146
- const list = lists.find(
147
- (l) => l.repo.toLowerCase() === repo.toLowerCase()
148
- );
149
-
150
- if (!list) {
154
+ async ({ query, repo, sortBy, minStars = 0, limit = 20 }) => {
155
+ // Exact repo lookup
156
+ if (repo) {
157
+ const list = lists.find(l => l.repo.toLowerCase() === repo.toLowerCase());
158
+ if (!list) {
159
+ return {
160
+ content: [{ type: "text", text: `List not found: ${repo}` }],
161
+ };
162
+ }
151
163
  return {
152
- content: [
153
- {
154
- type: "text",
155
- text: `List not found: ${repo}`,
156
- },
157
- ],
164
+ content: [{
165
+ type: "text",
166
+ text: JSON.stringify({
167
+ repo: list.repo,
168
+ name: list.name,
169
+ stars: list.stars,
170
+ description: list.description,
171
+ lastUpdated: list.pushed_at,
172
+ source: list.source,
173
+ githubUrl: `https://github.com/${list.repo}`,
174
+ }, null, 2),
175
+ }],
158
176
  };
159
177
  }
160
178
 
161
- return {
162
- content: [
163
- {
164
- type: "text",
165
- text: JSON.stringify(
166
- {
167
- repo: list.repo,
168
- name: list.name,
169
- stars: list.stars,
170
- description: list.description,
171
- lastUpdated: list.pushed_at,
172
- source: list.source,
173
- githubUrl: `https://github.com/${list.repo}`,
174
- },
175
- null,
176
- 2
177
- ),
178
- },
179
- ],
180
- };
181
- }
182
- );
179
+ // Determine sort order
180
+ const effectiveSortBy = sortBy || (query ? "relevance" : "stars");
181
+
182
+ let results: any[];
183
+
184
+ if (query && effectiveSortBy === "relevance") {
185
+ // Search with relevance sorting
186
+ results = listSearch
187
+ .search(query)
188
+ .filter(r => r.stars >= minStars)
189
+ .slice(0, limit)
190
+ .map(r => ({
191
+ repo: r.repo,
192
+ name: r.name,
193
+ stars: r.stars,
194
+ description: r.description,
195
+ lastUpdated: r.pushed_at,
196
+ }));
197
+ } else {
198
+ // Filter and sort manually
199
+ let filtered = lists.filter(l => l.stars >= minStars);
200
+
201
+ if (query) {
202
+ const searchResults = new Set(listSearch.search(query).map(r => r.repo));
203
+ filtered = filtered.filter(l => searchResults.has(l.repo));
204
+ }
183
205
 
184
- // Tool: List top repos by stars
185
- server.tool(
186
- "top_lists",
187
- "Get top awesome lists by star count",
188
- {
189
- limit: z.number().optional().describe("Number of lists to return (default: 20)"),
190
- category: z.string().optional().describe("Optional keyword to filter by (e.g., 'python', 'web')"),
191
- },
192
- async ({ limit = 20, category }) => {
193
- let filtered = lists;
194
-
195
- if (category) {
196
- const categoryResults = search.search(category);
197
- filtered = categoryResults.map((r) => ({
198
- repo: r.repo,
199
- name: r.name,
200
- stars: r.stars,
201
- description: r.description,
202
- pushed_at: r.pushed_at,
203
- source: r.source,
204
- }));
205
- }
206
+ // Sort
207
+ if (effectiveSortBy === "updated") {
208
+ filtered.sort((a, b) => (b.pushed_at || "").localeCompare(a.pushed_at || ""));
209
+ } else {
210
+ filtered.sort((a, b) => b.stars - a.stars);
211
+ }
206
212
 
207
- const top = filtered
208
- .sort((a, b) => b.stars - a.stars)
209
- .slice(0, limit)
210
- .map((l) => ({
213
+ results = filtered.slice(0, limit).map(l => ({
211
214
  repo: l.repo,
212
215
  name: l.name,
213
216
  stars: l.stars,
214
- description: l.description?.slice(0, 100),
217
+ description: l.description,
218
+ lastUpdated: l.pushed_at,
215
219
  }));
220
+ }
216
221
 
217
222
  return {
218
- content: [
219
- {
220
- type: "text",
221
- text: JSON.stringify(top, null, 2),
222
- },
223
- ],
224
- };
225
- }
226
- );
227
-
228
- // Tool: Stats
229
- server.tool(
230
- "stats",
231
- "Get statistics about the curated awesome lists collection",
232
- {},
233
- async () => {
234
- const totalLists = lists.length;
235
- const totalStars = lists.reduce((sum, l) => sum + l.stars, 0);
236
- const avgStars = Math.round(totalStars / totalLists);
237
-
238
- const starBrackets = {
239
- "10000+": lists.filter((l) => l.stars >= 10000).length,
240
- "5000-9999": lists.filter((l) => l.stars >= 5000 && l.stars < 10000)
241
- .length,
242
- "1000-4999": lists.filter((l) => l.stars >= 1000 && l.stars < 5000)
243
- .length,
244
- "500-999": lists.filter((l) => l.stars >= 500 && l.stars < 1000).length,
245
- "<500": lists.filter((l) => l.stars < 500).length,
246
- };
247
-
248
- return {
249
- content: [
250
- {
251
- type: "text",
252
- text: JSON.stringify(
253
- {
254
- totalLists,
255
- totalStars,
256
- avgStars,
257
- starDistribution: starBrackets,
258
- },
259
- null,
260
- 2
261
- ),
262
- },
263
- ],
223
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
264
224
  };
265
225
  }
266
226
  );
267
227
 
268
- // Load items index
269
- let itemsIndex: ItemsIndex | null = null;
270
-
271
- try {
272
- itemsIndex = await loadGzippedData<ItemsIndex>("items.json.gz");
273
- console.error(`Loaded ${itemsIndex?.itemCount} items from ${itemsIndex?.listCount} lists`);
274
- } catch {
275
- console.error("No items.json found - get_items will be unavailable");
276
- }
277
-
278
- // Tool: Get items from a list
279
- if (itemsIndex) {
228
+ // Tool: Search items (global search with filters)
229
+ if (itemsIndex && itemSearch) {
280
230
  server.tool(
281
- "get_items",
282
- "Get resources/items from an awesome list. Returns tools, libraries, and resources curated in the list.",
231
+ "search_items",
232
+ "Search tools, libraries, and resources across all awesome lists. Supports global search or filtering within a specific list.",
283
233
  {
284
- repo: z.string().describe("Repository name (e.g., 'vinta/awesome-python')"),
285
- category: z.string().optional().describe("Filter by category/section name"),
286
- limit: z.number().optional().describe("Maximum items to return (default: 50)"),
234
+ query: z.string().optional().describe("Search item names/descriptions"),
235
+ repo: z.string().optional().describe("Limit to a specific list (e.g., 'vinta/awesome-python')"),
236
+ category: z.string().optional().describe("Filter by category"),
237
+ language: z.string().optional().describe("Filter by programming language"),
238
+ minStars: z.number().optional().describe("Minimum GitHub stars"),
239
+ sortBy: z.enum(["relevance", "stars", "updated"]).optional().describe("Sort order (default: 'stars', or 'relevance' if query provided)"),
240
+ limit: z.number().optional().describe("Maximum results (default: 50)"),
287
241
  },
288
- async ({ repo, category, limit = 50 }) => {
289
- const listEntry = itemsIndex!.lists[repo] || itemsIndex!.lists[repo.toLowerCase()];
290
-
291
- if (!listEntry) {
292
- return {
293
- content: [
294
- {
295
- type: "text",
296
- text: `List not found: ${repo}. Use the 'search' tool to find available lists.`,
297
- },
298
- ],
299
- };
242
+ async ({ query, repo, category, language, minStars = 0, sortBy, limit = 50 }) => {
243
+ const effectiveSortBy = sortBy || (query ? "relevance" : "stars");
244
+
245
+ let results: IndexedItem[];
246
+
247
+ if (query && effectiveSortBy === "relevance") {
248
+ // Search with relevance
249
+ results = itemSearch!.search(query).map(r => ({
250
+ id: r.id,
251
+ name: r.name,
252
+ url: r.url,
253
+ description: r.description,
254
+ category: r.category,
255
+ subcategory: r.subcategory,
256
+ github: r.github,
257
+ listRepo: r.listRepo,
258
+ })) as IndexedItem[];
259
+ } else {
260
+ // Start with all items or query results
261
+ if (query) {
262
+ const searchResultIds = new Set(itemSearch!.search(query).map(r => r.id));
263
+ results = allItems.filter(i => searchResultIds.has(i.id));
264
+ } else {
265
+ results = [...allItems];
266
+ }
300
267
  }
301
268
 
302
- let items = listEntry.items;
269
+ // Apply filters
270
+ if (repo) {
271
+ const repoLower = repo.toLowerCase();
272
+ results = results.filter(i => i.listRepo.toLowerCase() === repoLower);
273
+ }
303
274
 
304
- // Filter by category if provided
305
275
  if (category) {
306
- const categoryLower = category.toLowerCase();
307
- items = items.filter(
308
- (i) =>
309
- i.category.toLowerCase().includes(categoryLower) ||
310
- i.subcategory?.toLowerCase().includes(categoryLower)
276
+ const catLower = category.toLowerCase();
277
+ results = results.filter(i =>
278
+ i.category?.toLowerCase().includes(catLower) ||
279
+ i.subcategory?.toLowerCase().includes(catLower)
280
+ );
281
+ }
282
+
283
+ if (language) {
284
+ const langLower = language.toLowerCase();
285
+ results = results.filter(i =>
286
+ i.github?.language?.toLowerCase() === langLower
311
287
  );
312
288
  }
313
289
 
314
- // Apply limit
315
- items = items.slice(0, limit);
290
+ if (minStars > 0) {
291
+ results = results.filter(i => (i.github?.stars || 0) >= minStars);
292
+ }
316
293
 
317
- // Format output
318
- const result = {
319
- repo,
320
- totalItems: listEntry.items.length,
321
- returnedItems: items.length,
322
- lastParsed: listEntry.lastParsed,
323
- items: items.map((i) => ({
294
+ // Sort (if not already sorted by relevance)
295
+ if (effectiveSortBy === "stars") {
296
+ results.sort((a, b) => (b.github?.stars || 0) - (a.github?.stars || 0));
297
+ } else if (effectiveSortBy === "updated") {
298
+ results.sort((a, b) =>
299
+ (b.github?.pushedAt || "").localeCompare(a.github?.pushedAt || "")
300
+ );
301
+ }
302
+
303
+ // Apply limit and format
304
+ const limited = results.slice(0, limit);
305
+
306
+ const output = {
307
+ totalMatches: results.length,
308
+ returned: limited.length,
309
+ items: limited.map(i => ({
324
310
  name: i.name,
325
311
  url: i.url,
326
312
  description: i.description,
@@ -328,21 +314,92 @@ if (itemsIndex) {
328
314
  subcategory: i.subcategory,
329
315
  stars: i.github?.stars,
330
316
  language: i.github?.language,
317
+ lastUpdated: i.github?.pushedAt,
318
+ list: i.listRepo,
331
319
  })),
332
320
  };
333
321
 
334
322
  return {
335
- content: [
336
- {
337
- type: "text",
338
- text: JSON.stringify(result, null, 2),
339
- },
340
- ],
323
+ content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
341
324
  };
342
325
  }
343
326
  );
344
327
  }
345
328
 
329
+ // Tool: Stats (enhanced with item statistics)
330
+ server.tool(
331
+ "stats",
332
+ "Get statistics about the curated awesome lists collection and items",
333
+ {},
334
+ async () => {
335
+ const totalLists = lists.length;
336
+ const totalStars = lists.reduce((sum, l) => sum + l.stars, 0);
337
+ const avgStars = Math.round(totalStars / totalLists);
338
+
339
+ const listStarBrackets = {
340
+ "10000+": lists.filter(l => l.stars >= 10000).length,
341
+ "5000-9999": lists.filter(l => l.stars >= 5000 && l.stars < 10000).length,
342
+ "1000-4999": lists.filter(l => l.stars >= 1000 && l.stars < 5000).length,
343
+ "500-999": lists.filter(l => l.stars >= 500 && l.stars < 1000).length,
344
+ "<500": lists.filter(l => l.stars < 500).length,
345
+ };
346
+
347
+ // Item statistics
348
+ let itemStats: any = null;
349
+ if (itemsIndex && allItems.length > 0) {
350
+ const enrichedItems = allItems.filter(i => i.github);
351
+
352
+ // Count languages
353
+ const languageCounts = new Map<string, number>();
354
+ for (const item of enrichedItems) {
355
+ const lang = item.github?.language;
356
+ if (lang) {
357
+ languageCounts.set(lang, (languageCounts.get(lang) || 0) + 1);
358
+ }
359
+ }
360
+ const topLanguages = [...languageCounts.entries()]
361
+ .sort((a, b) => b[1] - a[1])
362
+ .slice(0, 10)
363
+ .reduce((acc, [lang, count]) => ({ ...acc, [lang]: count }), {});
364
+
365
+ // Count categories
366
+ const categoryCounts = new Map<string, number>();
367
+ for (const item of allItems) {
368
+ if (item.category) {
369
+ categoryCounts.set(item.category, (categoryCounts.get(item.category) || 0) + 1);
370
+ }
371
+ }
372
+ const topCategories = [...categoryCounts.entries()]
373
+ .sort((a, b) => b[1] - a[1])
374
+ .slice(0, 10)
375
+ .reduce((acc, [cat, count]) => ({ ...acc, [cat]: count }), {});
376
+
377
+ itemStats = {
378
+ totalItems: allItems.length,
379
+ enrichedItems: enrichedItems.length,
380
+ listsWithItems: itemsIndex.listCount,
381
+ topLanguages,
382
+ topCategories,
383
+ };
384
+ }
385
+
386
+ return {
387
+ content: [{
388
+ type: "text",
389
+ text: JSON.stringify({
390
+ lists: {
391
+ total: totalLists,
392
+ totalStars,
393
+ avgStars,
394
+ starDistribution: listStarBrackets,
395
+ },
396
+ items: itemStats,
397
+ }, null, 2),
398
+ }],
399
+ };
400
+ }
401
+ );
402
+
346
403
  // Start server
347
404
  const transport = new StdioServerTransport();
348
405
  await server.connect(transport);