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 +142 -0
- package/package.json +40 -0
- package/src/enricher.ts +73 -0
- package/src/index.ts +304 -0
- package/src/parser.ts +68 -0
- package/src/types.ts +28 -0
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
|
+
}
|
package/src/enricher.ts
ADDED
|
@@ -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
|
+
}
|