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.
- package/package.json +1 -1
- package/src/index.ts +260 -203
package/package.json
CHANGED
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
|
|
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
|
|
96
|
-
|
|
100
|
+
// Index all lists
|
|
101
|
+
listSearch.addAll(lists.map((list, i) => ({ id: i, ...list })));
|
|
97
102
|
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
105
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
//
|
|
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
|
-
"
|
|
141
|
-
"
|
|
145
|
+
"search_lists",
|
|
146
|
+
"Search curated awesome lists. Returns lists sorted by relevance (if query provided) or stars.",
|
|
142
147
|
{
|
|
143
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
155
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
269
|
-
|
|
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
|
-
"
|
|
282
|
-
"
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
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
|
|
307
|
-
|
|
308
|
-
(
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
315
|
-
|
|
290
|
+
if (minStars > 0) {
|
|
291
|
+
results = results.filter(i => (i.github?.stars || 0) >= minStars);
|
|
292
|
+
}
|
|
316
293
|
|
|
317
|
-
//
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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);
|