awess 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/README.md ADDED
@@ -0,0 +1,142 @@
1
+ # Awesome Search Services
2
+
3
+ MCP server for searching 631+ curated awesome lists and 164,000+ tools/libraries within them.
4
+
5
+ ## Features
6
+
7
+ - **Search lists** - Find awesome lists by keyword (e.g., "machine learning", "rust")
8
+ - **Browse items** - Get tools/libraries from within any list
9
+ - **GitHub metadata** - Star counts, languages, last updated
10
+ - **Zero setup** - Data loaded from CDN, no cloning required
11
+
12
+ ## Quick Start
13
+
14
+ ### Claude Code
15
+
16
+ ```bash
17
+ claude mcp add awess -- bunx awess
18
+ ```
19
+
20
+ ### Claude Desktop
21
+
22
+ Add to your config file:
23
+
24
+ ```json
25
+ {
26
+ "mcpServers": {
27
+ "awess": {
28
+ "command": "bunx",
29
+ "args": ["awess"]
30
+ }
31
+ }
32
+ }
33
+ ```
34
+
35
+ **Config locations:**
36
+ - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
37
+ - Windows: `%APPDATA%\Claude\claude_desktop_config.json`
38
+ - Linux: `~/.config/Claude/claude_desktop_config.json`
39
+
40
+ ### Run directly
41
+
42
+ ```bash
43
+ bunx awess
44
+ ```
45
+
46
+ ### Test with MCP Inspector
47
+
48
+ ```bash
49
+ npx @modelcontextprotocol/inspector bunx awess
50
+ ```
51
+
52
+ ## Development
53
+
54
+ ```bash
55
+ git clone https://github.com/arimxyer/ass.git
56
+ cd ass
57
+ bun install
58
+ bun run start
59
+ ```
60
+
61
+ ## Tools
62
+
63
+ ### search
64
+
65
+ Search curated awesome lists by keyword. Returns matching lists sorted by relevance and stars.
66
+
67
+ **Parameters:**
68
+ - `query` (required): Search query (e.g., "rust", "machine learning", "react")
69
+ - `limit` (optional): Maximum results to return (default: 10)
70
+ - `minStars` (optional): Minimum star count filter (default: 0)
71
+
72
+ **Example:**
73
+ ```json
74
+ {
75
+ "query": "machine learning",
76
+ "limit": 5,
77
+ "minStars": 1000
78
+ }
79
+ ```
80
+
81
+ ### get_list
82
+
83
+ Get details for a specific awesome list by repository name.
84
+
85
+ **Parameters:**
86
+ - `repo` (required): Repository name (e.g., "sindresorhus/awesome")
87
+
88
+ **Example:**
89
+ ```json
90
+ {
91
+ "repo": "sindresorhus/awesome"
92
+ }
93
+ ```
94
+
95
+ ### top_lists
96
+
97
+ Get top awesome lists by star count.
98
+
99
+ **Parameters:**
100
+ - `limit` (optional): Number of lists to return (default: 20)
101
+ - `category` (optional): Optional keyword to filter by (e.g., "python", "web")
102
+
103
+ **Example:**
104
+ ```json
105
+ {
106
+ "limit": 10,
107
+ "category": "python"
108
+ }
109
+ ```
110
+
111
+ ### stats
112
+
113
+ Get statistics about the curated awesome lists collection.
114
+
115
+ **Parameters:** None
116
+
117
+ **Example:**
118
+ ```json
119
+ {}
120
+ ```
121
+
122
+ ### get_items
123
+
124
+ Get resources/items from an awesome list.
125
+
126
+ **Parameters:**
127
+ - `repo` (required): Repository name (e.g., "vinta/awesome-python")
128
+ - `category` (optional): Filter by category/section name
129
+ - `limit` (optional): Maximum items to return (default: 50)
130
+
131
+ **Example:**
132
+ ```json
133
+ {
134
+ "repo": "vinta/awesome-python",
135
+ "category": "Web Frameworks",
136
+ "limit": 10
137
+ }
138
+ ```
139
+
140
+ ## License
141
+
142
+ MIT
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "awess",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for searching curated awesome lists and their contents",
5
+ "type": "module",
6
+ "main": "src/index.ts",
7
+ "bin": {
8
+ "awess": "src/index.ts"
9
+ },
10
+ "files": [
11
+ "src/**/*.ts",
12
+ "!src/**/*.test.ts"
13
+ ],
14
+ "scripts": {
15
+ "start": "bun run src/index.ts",
16
+ "dev": "bun run --watch src/index.ts"
17
+ },
18
+ "keywords": [
19
+ "mcp",
20
+ "awesome-lists",
21
+ "search",
22
+ "claude",
23
+ "model-context-protocol"
24
+ ],
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/arimxyer/ass.git"
28
+ },
29
+ "license": "MIT",
30
+ "devDependencies": {
31
+ "@types/bun": "latest"
32
+ },
33
+ "dependencies": {
34
+ "@modelcontextprotocol/sdk": "^1.0.0",
35
+ "mdast-util-from-markdown": "^2.0.2",
36
+ "minisearch": "^7.2.0",
37
+ "octokit": "^5.0.5",
38
+ "zod": "^4.3.5"
39
+ }
40
+ }
@@ -0,0 +1,73 @@
1
+ // src/enricher.ts
2
+ import { Octokit } from "octokit";
3
+ import type { Item } from "./types";
4
+
5
+ export function extractGitHubRepo(url: string): string | null {
6
+ const match = url.match(/github\.com\/([^\/]+\/[^\/]+)/);
7
+ return match ? match[1].replace(/\.git$/, "") : null;
8
+ }
9
+
10
+ export async function batchEnrichItems(items: Item[]): Promise<Item[]> {
11
+ const token = process.env.GITHUB_TOKEN;
12
+ if (!token) {
13
+ console.warn("No GITHUB_TOKEN - skipping enrichment");
14
+ return items;
15
+ }
16
+
17
+ const octokit = new Octokit({ auth: token });
18
+
19
+ // Extract unique GitHub repos
20
+ const repoMap = new Map<string, Item[]>();
21
+ for (const item of items) {
22
+ const repo = extractGitHubRepo(item.url);
23
+ if (repo) {
24
+ if (!repoMap.has(repo)) repoMap.set(repo, []);
25
+ repoMap.get(repo)!.push(item);
26
+ }
27
+ }
28
+
29
+ const repos = Array.from(repoMap.keys());
30
+ console.log(`Enriching ${repos.length} unique repos...`);
31
+
32
+ // Batch query using GraphQL (100 at a time)
33
+ const batchSize = 100;
34
+ for (let i = 0; i < repos.length; i += batchSize) {
35
+ const batch = repos.slice(i, i + batchSize);
36
+
37
+ const query = `
38
+ query {
39
+ ${batch.map((repo, idx) => {
40
+ const [owner, name] = repo.split("/");
41
+ return `repo${idx}: repository(owner: "${owner}", name: "${name}") {
42
+ stargazerCount
43
+ primaryLanguage { name }
44
+ pushedAt
45
+ }`;
46
+ }).join("\n")}
47
+ }
48
+ `;
49
+
50
+ try {
51
+ const result: any = await octokit.graphql(query);
52
+
53
+ for (let j = 0; j < batch.length; j++) {
54
+ const data = result[`repo${j}`];
55
+ if (data) {
56
+ const itemsForRepo = repoMap.get(batch[j])!;
57
+ for (const item of itemsForRepo) {
58
+ item.github = {
59
+ stars: data.stargazerCount,
60
+ language: data.primaryLanguage?.name || null,
61
+ pushedAt: data.pushedAt,
62
+ };
63
+ item.lastEnriched = new Date().toISOString();
64
+ }
65
+ }
66
+ }
67
+ } catch (error: any) {
68
+ console.error(`Error enriching batch ${i}-${i + batchSize}:`, error.message);
69
+ }
70
+ }
71
+
72
+ return items;
73
+ }
package/src/index.ts ADDED
@@ -0,0 +1,304 @@
1
+ #!/usr/bin/env bun
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import MiniSearch from "minisearch";
5
+ import { z } from "zod";
6
+ import type { ItemsIndex } from "./types";
7
+
8
+ interface AwesomeList {
9
+ repo: string;
10
+ name: string;
11
+ stars: number;
12
+ description: string;
13
+ pushed_at?: string;
14
+ source: string;
15
+ }
16
+
17
+ // Data URL - GitHub raw (no file size limits, 5 min cache)
18
+ const DATA_URL = "https://raw.githubusercontent.com/arimxyer/ass/main/data";
19
+
20
+ // Load data from GitHub, fallback to local for development
21
+ async function loadData<T>(filename: string): Promise<T> {
22
+ // Try remote first
23
+ try {
24
+ const res = await fetch(`${DATA_URL}/${filename}`);
25
+ if (res.ok) {
26
+ return res.json();
27
+ }
28
+ } catch {
29
+ // Remote failed, try local
30
+ }
31
+
32
+ // Fallback to local file
33
+ const localPath = new URL(`../data/${filename}`, import.meta.url);
34
+ return Bun.file(localPath).json();
35
+ }
36
+
37
+ // Load curated data
38
+ const lists: AwesomeList[] = await loadData("lists.json");
39
+
40
+ // Initialize search index
41
+ const search = new MiniSearch<AwesomeList>({
42
+ fields: ["name", "repo", "description"],
43
+ storeFields: ["repo", "name", "stars", "description", "pushed_at", "source"],
44
+ searchOptions: {
45
+ boost: { name: 2, repo: 1.5, description: 1 },
46
+ fuzzy: 0.2,
47
+ prefix: true,
48
+ },
49
+ });
50
+
51
+ // Index all lists with repo as id
52
+ search.addAll(lists.map((list, i) => ({ id: i, ...list })));
53
+
54
+ // Create MCP server
55
+ const server = new McpServer({
56
+ name: "ass",
57
+ version: "0.1.0",
58
+ });
59
+
60
+ // Tool: Search awesome lists
61
+ server.tool(
62
+ "search",
63
+ "Search curated awesome lists by keyword. Returns matching lists sorted by relevance and stars.",
64
+ {
65
+ query: z.string().describe("Search query (e.g., 'rust', 'machine learning', 'react')"),
66
+ limit: z.number().optional().describe("Maximum results to return (default: 10)"),
67
+ minStars: z.number().optional().describe("Minimum star count filter (default: 0)"),
68
+ },
69
+ async ({ query, limit = 10, minStars = 0 }) => {
70
+ const results = search
71
+ .search(query)
72
+ .filter((r) => r.stars >= minStars)
73
+ .slice(0, limit)
74
+ .map((r) => ({
75
+ repo: r.repo,
76
+ name: r.name,
77
+ stars: r.stars,
78
+ description: r.description,
79
+ lastUpdated: r.pushed_at,
80
+ score: r.score,
81
+ }));
82
+
83
+ return {
84
+ content: [
85
+ {
86
+ type: "text",
87
+ text: JSON.stringify(results, null, 2),
88
+ },
89
+ ],
90
+ };
91
+ }
92
+ );
93
+
94
+ // Tool: Get list details
95
+ server.tool(
96
+ "get_list",
97
+ "Get details for a specific awesome list by repository name",
98
+ {
99
+ repo: z.string().describe("Repository name (e.g., 'sindresorhus/awesome')"),
100
+ },
101
+ async ({ repo }) => {
102
+ const list = lists.find(
103
+ (l) => l.repo.toLowerCase() === repo.toLowerCase()
104
+ );
105
+
106
+ if (!list) {
107
+ return {
108
+ content: [
109
+ {
110
+ type: "text",
111
+ text: `List not found: ${repo}`,
112
+ },
113
+ ],
114
+ };
115
+ }
116
+
117
+ return {
118
+ content: [
119
+ {
120
+ type: "text",
121
+ text: JSON.stringify(
122
+ {
123
+ repo: list.repo,
124
+ name: list.name,
125
+ stars: list.stars,
126
+ description: list.description,
127
+ lastUpdated: list.pushed_at,
128
+ source: list.source,
129
+ githubUrl: `https://github.com/${list.repo}`,
130
+ },
131
+ null,
132
+ 2
133
+ ),
134
+ },
135
+ ],
136
+ };
137
+ }
138
+ );
139
+
140
+ // Tool: List top repos by stars
141
+ server.tool(
142
+ "top_lists",
143
+ "Get top awesome lists by star count",
144
+ {
145
+ limit: z.number().optional().describe("Number of lists to return (default: 20)"),
146
+ category: z.string().optional().describe("Optional keyword to filter by (e.g., 'python', 'web')"),
147
+ },
148
+ async ({ limit = 20, category }) => {
149
+ let filtered = lists;
150
+
151
+ if (category) {
152
+ const categoryResults = search.search(category);
153
+ filtered = categoryResults.map((r) => ({
154
+ repo: r.repo,
155
+ name: r.name,
156
+ stars: r.stars,
157
+ description: r.description,
158
+ pushed_at: r.pushed_at,
159
+ source: r.source,
160
+ }));
161
+ }
162
+
163
+ const top = filtered
164
+ .sort((a, b) => b.stars - a.stars)
165
+ .slice(0, limit)
166
+ .map((l) => ({
167
+ repo: l.repo,
168
+ name: l.name,
169
+ stars: l.stars,
170
+ description: l.description?.slice(0, 100),
171
+ }));
172
+
173
+ return {
174
+ content: [
175
+ {
176
+ type: "text",
177
+ text: JSON.stringify(top, null, 2),
178
+ },
179
+ ],
180
+ };
181
+ }
182
+ );
183
+
184
+ // Tool: Stats
185
+ server.tool(
186
+ "stats",
187
+ "Get statistics about the curated awesome lists collection",
188
+ {},
189
+ async () => {
190
+ const totalLists = lists.length;
191
+ const totalStars = lists.reduce((sum, l) => sum + l.stars, 0);
192
+ const avgStars = Math.round(totalStars / totalLists);
193
+
194
+ const starBrackets = {
195
+ "10000+": lists.filter((l) => l.stars >= 10000).length,
196
+ "5000-9999": lists.filter((l) => l.stars >= 5000 && l.stars < 10000)
197
+ .length,
198
+ "1000-4999": lists.filter((l) => l.stars >= 1000 && l.stars < 5000)
199
+ .length,
200
+ "500-999": lists.filter((l) => l.stars >= 500 && l.stars < 1000).length,
201
+ "<500": lists.filter((l) => l.stars < 500).length,
202
+ };
203
+
204
+ return {
205
+ content: [
206
+ {
207
+ type: "text",
208
+ text: JSON.stringify(
209
+ {
210
+ totalLists,
211
+ totalStars,
212
+ avgStars,
213
+ starDistribution: starBrackets,
214
+ },
215
+ null,
216
+ 2
217
+ ),
218
+ },
219
+ ],
220
+ };
221
+ }
222
+ );
223
+
224
+ // Load items index
225
+ let itemsIndex: ItemsIndex | null = null;
226
+
227
+ try {
228
+ itemsIndex = await loadData<ItemsIndex>("items.json");
229
+ console.error(`Loaded ${itemsIndex?.itemCount} items from ${itemsIndex?.listCount} lists`);
230
+ } catch {
231
+ console.error("No items.json found - get_items will be unavailable");
232
+ }
233
+
234
+ // Tool: Get items from a list
235
+ if (itemsIndex) {
236
+ server.tool(
237
+ "get_items",
238
+ "Get resources/items from an awesome list. Returns tools, libraries, and resources curated in the list.",
239
+ {
240
+ repo: z.string().describe("Repository name (e.g., 'vinta/awesome-python')"),
241
+ category: z.string().optional().describe("Filter by category/section name"),
242
+ limit: z.number().optional().describe("Maximum items to return (default: 50)"),
243
+ },
244
+ async ({ repo, category, limit = 50 }) => {
245
+ const listEntry = itemsIndex!.lists[repo] || itemsIndex!.lists[repo.toLowerCase()];
246
+
247
+ if (!listEntry) {
248
+ return {
249
+ content: [
250
+ {
251
+ type: "text",
252
+ text: `List not found: ${repo}. Use the 'search' tool to find available lists.`,
253
+ },
254
+ ],
255
+ };
256
+ }
257
+
258
+ let items = listEntry.items;
259
+
260
+ // Filter by category if provided
261
+ if (category) {
262
+ const categoryLower = category.toLowerCase();
263
+ items = items.filter(
264
+ (i) =>
265
+ i.category.toLowerCase().includes(categoryLower) ||
266
+ i.subcategory?.toLowerCase().includes(categoryLower)
267
+ );
268
+ }
269
+
270
+ // Apply limit
271
+ items = items.slice(0, limit);
272
+
273
+ // Format output
274
+ const result = {
275
+ repo,
276
+ totalItems: listEntry.items.length,
277
+ returnedItems: items.length,
278
+ lastParsed: listEntry.lastParsed,
279
+ items: items.map((i) => ({
280
+ name: i.name,
281
+ url: i.url,
282
+ description: i.description,
283
+ category: i.category,
284
+ subcategory: i.subcategory,
285
+ stars: i.github?.stars,
286
+ language: i.github?.language,
287
+ })),
288
+ };
289
+
290
+ return {
291
+ content: [
292
+ {
293
+ type: "text",
294
+ text: JSON.stringify(result, null, 2),
295
+ },
296
+ ],
297
+ };
298
+ }
299
+ );
300
+ }
301
+
302
+ // Start server
303
+ const transport = new StdioServerTransport();
304
+ await server.connect(transport);
package/src/parser.ts ADDED
@@ -0,0 +1,68 @@
1
+ // src/parser.ts
2
+ import { fromMarkdown } from "mdast-util-from-markdown";
3
+ import type { Item } from "./types";
4
+
5
+ export function parseReadme(markdown: string): Item[] {
6
+ const tree = fromMarkdown(markdown);
7
+ const items: Item[] = [];
8
+ let currentCategory = "";
9
+ let currentSubcategory = "";
10
+
11
+ function walk(node: any) {
12
+ // Track h2 headings as categories
13
+ if (node.type === "heading" && node.depth === 2) {
14
+ const text = node.children?.find((c: any) => c.type === "text")?.value || "";
15
+ currentCategory = text;
16
+ currentSubcategory = "";
17
+ }
18
+
19
+ // Track h3 headings as subcategories
20
+ if (node.type === "heading" && node.depth === 3) {
21
+ const text = node.children?.find((c: any) => c.type === "text")?.value || "";
22
+ currentSubcategory = text;
23
+ }
24
+
25
+ // Extract list items with links
26
+ if (node.type === "listItem" && currentCategory) {
27
+ const paragraph = node.children?.find((c: any) => c.type === "paragraph");
28
+ if (paragraph) {
29
+ const link = paragraph.children?.find((c: any) => c.type === "link");
30
+ if (link) {
31
+ const name = link.children?.find((c: any) => c.type === "text")?.value || "";
32
+ const url = link.url || "";
33
+
34
+ // Get description (text after the link)
35
+ const linkIndex = paragraph.children.indexOf(link);
36
+ const afterLink = paragraph.children.slice(linkIndex + 1);
37
+ const description = afterLink
38
+ .filter((c: any) => c.type === "text")
39
+ .map((c: any) => c.value)
40
+ .join("")
41
+ .replace(/^\s*[-–—]\s*/, "")
42
+ .trim();
43
+
44
+ // Skip anchor links and empty names
45
+ if (name && url && !url.startsWith("#")) {
46
+ items.push({
47
+ name,
48
+ url,
49
+ description,
50
+ category: currentCategory,
51
+ ...(currentSubcategory && { subcategory: currentSubcategory }),
52
+ });
53
+ }
54
+ }
55
+ }
56
+ }
57
+
58
+ // Recurse into children
59
+ if (node.children) {
60
+ for (const child of node.children) {
61
+ walk(child);
62
+ }
63
+ }
64
+ }
65
+
66
+ walk(tree);
67
+ return items;
68
+ }
package/src/types.ts ADDED
@@ -0,0 +1,28 @@
1
+ // src/types.ts
2
+
3
+ export interface Item {
4
+ name: string;
5
+ url: string;
6
+ description: string;
7
+ category: string;
8
+ subcategory?: string;
9
+ lastEnriched?: string;
10
+ github?: {
11
+ stars: number;
12
+ language: string | null;
13
+ pushedAt: string;
14
+ };
15
+ }
16
+
17
+ export interface ListEntry {
18
+ lastParsed: string;
19
+ pushedAt: string;
20
+ items: Item[];
21
+ }
22
+
23
+ export interface ItemsIndex {
24
+ generatedAt: string;
25
+ listCount: number;
26
+ itemCount: number;
27
+ lists: Record<string, ListEntry>;
28
+ }