@toolsdk.ai/registry 1.0.102 → 1.0.104
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 +4 -4
- package/dist/api/index.js +41 -7
- package/dist/api/package-handler.d.ts +4 -4
- package/dist/api/package-route.js +11 -6
- package/dist/api/package-so.js +2 -0
- package/dist/helper.d.ts +1 -0
- package/dist/helper.js +8 -1
- package/dist/search/search-route.d.ts +3 -0
- package/dist/search/search-route.js +305 -0
- package/dist/search/search-service.d.ts +120 -0
- package/dist/search/search-service.js +416 -0
- package/dist/search/search.test.d.ts +1 -0
- package/dist/search/search.test.js +100 -0
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +5 -0
- package/indexes/packages-list.json +20 -3
- package/package.json +585 -578
package/README.md
CHANGED
|
@@ -106,9 +106,9 @@ For more detail please see [the guide](./docs/guide.md).
|
|
|
106
106
|
|
|
107
107
|
# MCP Servers
|
|
108
108
|
|
|
109
|
-
✅: Validated and runnable tools (
|
|
109
|
+
✅: Validated and runnable tools (721)
|
|
110
110
|
|
|
111
|
-
❌: Cannot be run by the MCP client (with mock environments variables (
|
|
111
|
+
❌: Cannot be run by the MCP client (with mock environments variables (3383))
|
|
112
112
|
|
|
113
113
|
|
|
114
114
|
|
|
@@ -522,7 +522,7 @@ Tools for browsing, scraping, and automating web content in AI-compatible format
|
|
|
522
522
|
- [❌ puppeteer-browser-automation](https://github.com/twolven/mcp-server-puppeteer-py): Integrates with Puppeteer to provide browser automation capabilities for web navigation, interaction, and data extraction. (python)
|
|
523
523
|
- [❌ awesome-cursor](https://github.com/kleneway/awesome-cursor-mpc-server): Built for Cursor, integrates screenshot capture, web page structure analysis, and code review capabilities for automated UI testing, web scraping, and code quality checks. (node)
|
|
524
524
|
- [❌ screenshotone-mcp-server](https://github.com/mrgoonie/screenshotone-mcp-server): Enables AI to capture and process screenshots of webpages with customizable parameters through the ScreenshotOne API with Cloudflare CDN integration for image storage and retrieval. (node)
|
|
525
|
-
- [
|
|
525
|
+
- [✅ tavily-mcp](https://github.com/tavily-ai/tavily-mcp): Integrates with Tavily API to provide real-time web search and content extraction capabilities for research, aggregation, and fact-checking tasks. (4 tools) (node)
|
|
526
526
|
- [✅ mcp-jinaai-grounding](https://github.com/spences10/mcp-jinaai-grounding): Integrates JinaAI's content extraction and analysis capabilities for web scraping, documentation parsing, and text analysis tasks. (1 tools) (node)
|
|
527
527
|
- [❌ website-to-markdown-converter](https://github.com/tolik-unicornrider/mcp_scraper): Converts web content to high-quality Markdown using Mozilla's Readability and TurndownService, enabling clean extraction of meaningful content from URLs or HTML files for analysis and document conversion. (node)
|
|
528
528
|
- [❌ web-browser-(playwright)](https://github.com/random-robbie/mcp-web-browser): Integrates with Playwright to enable cross-browser web automation for tasks like scraping, testing, and content extraction. (python)
|
|
@@ -3110,7 +3110,7 @@ Find and extract data from various sources quickly and efficiently.
|
|
|
3110
3110
|
- [❌ geeknews](https://github.com/the0807/geeknews-mcp-server): Retrieves and extracts tech news, articles, and discussions from GeekNews using BeautifulSoup for more informed conversations about current technology developments. (python)
|
|
3111
3111
|
- [✅ @toolsdk.ai/tavily-mcp](https://github.com/tavily-ai/tavily-mcp): An MCP server that implements web search, extract, mapping, and crawling through the Tavily API. (4 tools) (node)
|
|
3112
3112
|
- [❌ goodnews](https://github.com/vectorinstitute/mcp-goodnews): Filters and ranks news articles from NewsAPI based on positive sentiment, delivering uplifting headlines amid potentially negative media cycles. (python)
|
|
3113
|
-
- [
|
|
3113
|
+
- [✅ tavily-mcp](https://github.com/tavily-ai/tavily-mcp): Integrates with Tavily API to provide real-time web search and content extraction capabilities for research, aggregation, and fact-checking tasks. (4 tools) (node)
|
|
3114
3114
|
- [✅ anilist-mcp](https://github.com/yuna0x0/anilist-mcp): MCP server that interfaces with the AniList API, allowing LLM clients to access and interact with anime, manga, character, staff, and user data from AniList (44 tools) (node)
|
|
3115
3115
|
- [❌ exa-web-search](https://github.com/mshojaei77/reactmcp): Integrates with Exa API to enable web search capabilities with filtering options for domains, text requirements, and date ranges, returning markdown-formatted results with titles, URLs, publication dates, and content summaries for real-time internet information access. (python)
|
|
3116
3116
|
- [❌ web-browser-mcp-server](https://github.com/blazickjp/web-browser-mcp-server): Integrates web browsing capabilities for realtime data retrieval, content extraction, and task automation using popular Python libraries. (python)
|
package/dist/api/index.js
CHANGED
|
@@ -1,20 +1,54 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import { serve } from "@hono/node-server";
|
|
3
4
|
import { swaggerUI } from "@hono/swagger-ui";
|
|
4
5
|
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
5
6
|
import dotenv from "dotenv";
|
|
7
|
+
import { searchRoutes } from "../search/search-route";
|
|
8
|
+
import searchService from "../search/search-service";
|
|
9
|
+
import { getDirname } from "../utils";
|
|
6
10
|
import { packageRoutes } from "./package-route";
|
|
11
|
+
const __dirname = getDirname(import.meta.url);
|
|
7
12
|
dotenv.config({ path: path.resolve(process.cwd(), ".env.local") });
|
|
8
13
|
dotenv.config({ path: path.resolve(process.cwd(), ".env") });
|
|
14
|
+
const initializeSearchService = async () => {
|
|
15
|
+
try {
|
|
16
|
+
await searchService.initialize();
|
|
17
|
+
console.log("🔍 Search service initialized");
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
console.warn("⚠️ Search service initialization failed:", error.message);
|
|
21
|
+
console.log("💡 Install and start MeiliSearch to enable enhanced search features");
|
|
22
|
+
}
|
|
23
|
+
};
|
|
9
24
|
const app = new OpenAPIHono();
|
|
10
25
|
app.route("/api/v1", packageRoutes);
|
|
11
|
-
|
|
12
|
-
|
|
26
|
+
if (process.env.ENABLE_SEARCH === "true") {
|
|
27
|
+
initializeSearchService().catch(console.error);
|
|
28
|
+
app.route("/api/v1", searchRoutes);
|
|
29
|
+
}
|
|
30
|
+
app.get("/", async (c) => {
|
|
31
|
+
try {
|
|
32
|
+
const htmlPath = path.join(__dirname, "..", "search", "search.html");
|
|
33
|
+
const html = await fs.readFile(htmlPath, "utf8");
|
|
34
|
+
return c.html(html);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
console.error("Failed to load home page:", error);
|
|
38
|
+
return c.text("MCP Registry API Server is running!");
|
|
39
|
+
}
|
|
13
40
|
});
|
|
14
|
-
app.get("/api/meta", (c) => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
41
|
+
app.get("/api/meta", async (c) => {
|
|
42
|
+
try {
|
|
43
|
+
const packageJson = await import("../../package.json", {
|
|
44
|
+
assert: { type: "json" },
|
|
45
|
+
});
|
|
46
|
+
return c.json({ version: packageJson.default.version });
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
console.error("Failed to load package.json:", error);
|
|
50
|
+
return c.json({ version: "unknown" });
|
|
51
|
+
}
|
|
18
52
|
});
|
|
19
53
|
app.doc("/api/v1/doc", {
|
|
20
54
|
openapi: "3.0.0",
|
|
@@ -31,7 +65,7 @@ app.onError((err, c) => {
|
|
|
31
65
|
console.error("Server Error:", err);
|
|
32
66
|
return c.json({ success: false, code: 500, message: "Internal server error" }, 500);
|
|
33
67
|
});
|
|
34
|
-
const port = process.env.MCP_SERVER_PORT ? parseInt(process.env.MCP_SERVER_PORT, 10) :
|
|
68
|
+
const port = process.env.MCP_SERVER_PORT ? parseInt(process.env.MCP_SERVER_PORT, 10) : 3003;
|
|
35
69
|
console.log(`Server is running on: http://localhost:${port}`);
|
|
36
70
|
serve({
|
|
37
71
|
fetch: app.fetch,
|
|
@@ -9,11 +9,11 @@ export declare const packageHandler: {
|
|
|
9
9
|
success: boolean;
|
|
10
10
|
code: number;
|
|
11
11
|
message: string;
|
|
12
|
-
},
|
|
12
|
+
}, 400, "json">) | (globalThis.Response & import("hono").TypedResponse<{
|
|
13
13
|
success: boolean;
|
|
14
14
|
code: number;
|
|
15
15
|
message: string;
|
|
16
|
-
},
|
|
16
|
+
}, 404, "json">) | (globalThis.Response & import("hono").TypedResponse<{
|
|
17
17
|
success: boolean;
|
|
18
18
|
code: number;
|
|
19
19
|
message: string;
|
|
@@ -77,11 +77,11 @@ export declare const packageHandler: {
|
|
|
77
77
|
success: boolean;
|
|
78
78
|
code: number;
|
|
79
79
|
message: string;
|
|
80
|
-
},
|
|
80
|
+
}, 400, "json">) | (globalThis.Response & import("hono").TypedResponse<{
|
|
81
81
|
success: boolean;
|
|
82
82
|
code: number;
|
|
83
83
|
message: string;
|
|
84
|
-
},
|
|
84
|
+
}, 404, "json">) | (globalThis.Response & import("hono").TypedResponse<{
|
|
85
85
|
success: boolean;
|
|
86
86
|
code: number;
|
|
87
87
|
message: string;
|
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
|
|
1
|
+
import path from "node:path";
|
|
2
2
|
import { createRoute, OpenAPIHono } from "@hono/zod-openapi";
|
|
3
3
|
import { getPythonDependencies } from "../helper";
|
|
4
4
|
import { CategoriesResponseSchema, ExecuteToolResponseSchema, FeaturedResponseSchema, PackageDetailResponseSchema, PackagesListResponseSchema, packageNameQuerySchema, ToolExecuteSchema, ToolsResponseSchema, } from "../schema";
|
|
5
|
-
import { createResponse, createRouteResponses } from "../utils";
|
|
5
|
+
import { createResponse, createRouteResponses, getDirname } from "../utils";
|
|
6
6
|
import { packageHandler } from "./package-handler";
|
|
7
|
+
const __dirname = getDirname(import.meta.url);
|
|
7
8
|
export const packageRoutes = new OpenAPIHono();
|
|
8
9
|
const featuredRoute = createRoute({
|
|
9
10
|
method: "get",
|
|
10
11
|
path: "/config/featured",
|
|
11
12
|
responses: createRouteResponses(FeaturedResponseSchema),
|
|
12
13
|
});
|
|
13
|
-
packageRoutes.openapi(featuredRoute, (c) => {
|
|
14
|
-
const
|
|
14
|
+
packageRoutes.openapi(featuredRoute, async (c) => {
|
|
15
|
+
const featuredPath = path.join(__dirname, "../../config/featured.mjs");
|
|
16
|
+
const featuredModule = await import(`file://${featuredPath}`);
|
|
17
|
+
const featured = featuredModule.default;
|
|
15
18
|
const response = createResponse(featured);
|
|
16
19
|
return c.json(response, 200);
|
|
17
20
|
});
|
|
@@ -20,8 +23,10 @@ const categoriesRoute = createRoute({
|
|
|
20
23
|
path: "/config/categories",
|
|
21
24
|
responses: createRouteResponses(CategoriesResponseSchema),
|
|
22
25
|
});
|
|
23
|
-
packageRoutes.openapi(categoriesRoute, (c) => {
|
|
24
|
-
const
|
|
26
|
+
packageRoutes.openapi(categoriesRoute, async (c) => {
|
|
27
|
+
const categoriesPath = path.join(__dirname, "../../config/categories.mjs");
|
|
28
|
+
const categoriesModule = await import(`file://${categoriesPath}`);
|
|
29
|
+
const categories = categoriesModule.default;
|
|
25
30
|
const response = createResponse(categories);
|
|
26
31
|
return c.json(response, 200);
|
|
27
32
|
});
|
package/dist/api/package-so.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { getMcpClient, getPackageConfigByKey, typedAllPackagesList } from "../helper.js";
|
|
4
|
+
import { getDirname } from "../utils";
|
|
5
|
+
const __dirname = getDirname(import.meta.url);
|
|
4
6
|
export class PackageSO {
|
|
5
7
|
async executeTool(request) {
|
|
6
8
|
const mcpServerConfig = getPackageConfigByKey(request.packageName);
|
package/dist/helper.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export declare const typedAllPackagesList: Record<string, {
|
|
|
11
11
|
}> | undefined;
|
|
12
12
|
}>;
|
|
13
13
|
export declare function getPackageConfigByKey(packageKey: string): MCPServerPackageConfig;
|
|
14
|
+
export declare function getPackageJSON(packageName: string): any;
|
|
14
15
|
export declare function getMcpClient(mcpServerConfig: MCPServerPackageConfig, env?: Record<string, string>): Promise<{
|
|
15
16
|
client: Client<{
|
|
16
17
|
method: string;
|
package/dist/helper.js
CHANGED
|
@@ -7,7 +7,9 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
|
|
|
7
7
|
import axios from "axios";
|
|
8
8
|
import semver from "semver";
|
|
9
9
|
import allPackagesList from "../indexes/packages-list.json";
|
|
10
|
+
import { getDirname } from "../src/utils";
|
|
10
11
|
import { MCPServerPackageConfigSchema, PackagesListSchema } from "./schema";
|
|
12
|
+
const __dirname = getDirname(import.meta.url);
|
|
11
13
|
export const typedAllPackagesList = PackagesListSchema.parse(allPackagesList);
|
|
12
14
|
export function getPackageConfigByKey(packageKey) {
|
|
13
15
|
const value = typedAllPackagesList[packageKey];
|
|
@@ -20,8 +22,12 @@ export function getPackageConfigByKey(packageKey) {
|
|
|
20
22
|
const mcpServerConfig = MCPServerPackageConfigSchema.parse(JSON.parse(jsonStr));
|
|
21
23
|
return mcpServerConfig;
|
|
22
24
|
}
|
|
23
|
-
function getPackageJSON(packageName) {
|
|
25
|
+
export function getPackageJSON(packageName) {
|
|
24
26
|
const packageJSONFilePath = `${__dirname}/../node_modules/${packageName}/package.json`;
|
|
27
|
+
// Check if the package exists in node_modules
|
|
28
|
+
if (!fs.existsSync(packageJSONFilePath)) {
|
|
29
|
+
throw new Error(`Package '${packageName}' not found in node_modules. Install it first with: pnpm add ${packageName}`);
|
|
30
|
+
}
|
|
25
31
|
const packageJSONStr = fs.readFileSync(packageJSONFilePath, "utf8");
|
|
26
32
|
const packageJSON = JSON.parse(packageJSONStr);
|
|
27
33
|
return packageJSON;
|
|
@@ -100,6 +106,7 @@ export function updatePackageJsonDependencies({ packageDeps, enableValidation =
|
|
|
100
106
|
"@hono/swagger-ui": "^0.5.2",
|
|
101
107
|
"@hono/zod-openapi": "^0.16.4",
|
|
102
108
|
"@iarna/toml": "^2.2.5",
|
|
109
|
+
meilisearch: "^0.33.0",
|
|
103
110
|
lodash: "^4.17.21",
|
|
104
111
|
zod: "^3.23.30",
|
|
105
112
|
axios: "^1.9.0",
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";
|
|
2
|
+
import { BaseResponseSchema } from "../schema";
|
|
3
|
+
import { createErrorResponse, createResponse, createRouteResponses } from "../utils";
|
|
4
|
+
import searchService from "./search-service";
|
|
5
|
+
const searchRoutes = new OpenAPIHono();
|
|
6
|
+
// Define search query parameter schema
|
|
7
|
+
const searchQuerySchema = z.object({
|
|
8
|
+
q: z.string().openapi({
|
|
9
|
+
description: "Search query",
|
|
10
|
+
example: "database",
|
|
11
|
+
}),
|
|
12
|
+
limit: z.coerce.number().min(1).max(100).optional().openapi({
|
|
13
|
+
description: "Limit the number of results",
|
|
14
|
+
example: 20,
|
|
15
|
+
}),
|
|
16
|
+
offset: z.coerce.number().min(0).optional().openapi({
|
|
17
|
+
description: "Offset for pagination",
|
|
18
|
+
example: 0,
|
|
19
|
+
}),
|
|
20
|
+
category: z.string().optional().openapi({
|
|
21
|
+
description: "Filter by category",
|
|
22
|
+
example: "databases",
|
|
23
|
+
}),
|
|
24
|
+
});
|
|
25
|
+
// Define search result response schema
|
|
26
|
+
const searchResultSchema = z
|
|
27
|
+
.object({
|
|
28
|
+
id: z.string(),
|
|
29
|
+
name: z.string(),
|
|
30
|
+
packageName: z.string(),
|
|
31
|
+
description: z.string(),
|
|
32
|
+
category: z.string(),
|
|
33
|
+
validated: z.boolean(),
|
|
34
|
+
author: z.string(),
|
|
35
|
+
toolCount: z.number(),
|
|
36
|
+
hasTools: z.boolean(),
|
|
37
|
+
popularity: z.number(),
|
|
38
|
+
})
|
|
39
|
+
.openapi("SearchResult");
|
|
40
|
+
const searchResponseSchema = BaseResponseSchema.extend({
|
|
41
|
+
data: z
|
|
42
|
+
.object({
|
|
43
|
+
hits: z.array(searchResultSchema),
|
|
44
|
+
query: z.string(),
|
|
45
|
+
processingTimeMs: z.number(),
|
|
46
|
+
limit: z.number(),
|
|
47
|
+
offset: z.number(),
|
|
48
|
+
estimatedTotalHits: z.number(),
|
|
49
|
+
})
|
|
50
|
+
.optional(),
|
|
51
|
+
}).openapi("SearchResponse");
|
|
52
|
+
// Search route definition
|
|
53
|
+
const searchRoute = createRoute({
|
|
54
|
+
method: "get",
|
|
55
|
+
path: "/search",
|
|
56
|
+
request: {
|
|
57
|
+
query: searchQuerySchema,
|
|
58
|
+
},
|
|
59
|
+
responses: createRouteResponses(searchResponseSchema, {
|
|
60
|
+
includeErrorResponses: true,
|
|
61
|
+
}),
|
|
62
|
+
});
|
|
63
|
+
searchRoutes.openapi(searchRoute, async (c) => {
|
|
64
|
+
try {
|
|
65
|
+
const { q, limit, offset, category } = c.req.valid("query");
|
|
66
|
+
// Build filter condition
|
|
67
|
+
const filter = category ? `category = '${category}'` : undefined;
|
|
68
|
+
// Execute search
|
|
69
|
+
const results = await searchService.search(q, {
|
|
70
|
+
limit,
|
|
71
|
+
offset,
|
|
72
|
+
filter,
|
|
73
|
+
});
|
|
74
|
+
const response = createResponse(results);
|
|
75
|
+
return c.json(response, 200);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
console.error("Search failed:", error.stack);
|
|
79
|
+
const errorResponse = createErrorResponse(error.message || "Search failed", 500);
|
|
80
|
+
return c.json(errorResponse, 500);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
// Define suggest query parameter schema
|
|
84
|
+
const suggestQuerySchema = z.object({
|
|
85
|
+
q: z.string().openapi({
|
|
86
|
+
description: "Suggestion query",
|
|
87
|
+
example: "data",
|
|
88
|
+
}),
|
|
89
|
+
limit: z.coerce.number().min(1).max(20).optional().openapi({
|
|
90
|
+
description: "Limit the number of results",
|
|
91
|
+
example: 10,
|
|
92
|
+
}),
|
|
93
|
+
});
|
|
94
|
+
// Define suggest result response schema
|
|
95
|
+
const suggestResultSchema = z
|
|
96
|
+
.object({
|
|
97
|
+
name: z.string(),
|
|
98
|
+
packageName: z.string(),
|
|
99
|
+
category: z.string(),
|
|
100
|
+
highlighted: z.string(),
|
|
101
|
+
})
|
|
102
|
+
.openapi("SuggestResult");
|
|
103
|
+
const suggestResponseSchema = BaseResponseSchema.extend({
|
|
104
|
+
data: z
|
|
105
|
+
.object({
|
|
106
|
+
suggestions: z.array(suggestResultSchema),
|
|
107
|
+
})
|
|
108
|
+
.optional(),
|
|
109
|
+
}).openapi("SuggestResponse");
|
|
110
|
+
// Search suggest route definition
|
|
111
|
+
const suggestRoute = createRoute({
|
|
112
|
+
method: "get",
|
|
113
|
+
path: "/search/suggest",
|
|
114
|
+
request: {
|
|
115
|
+
query: suggestQuerySchema,
|
|
116
|
+
},
|
|
117
|
+
responses: createRouteResponses(suggestResponseSchema, {
|
|
118
|
+
includeErrorResponses: true,
|
|
119
|
+
}),
|
|
120
|
+
});
|
|
121
|
+
searchRoutes.openapi(suggestRoute, async (c) => {
|
|
122
|
+
try {
|
|
123
|
+
const { q, limit } = c.req.valid("query");
|
|
124
|
+
const suggestions = await searchService.suggest(q, limit);
|
|
125
|
+
const response = createResponse({ suggestions });
|
|
126
|
+
return c.json(response, 200);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
console.error("Failed to get suggestions:", error.stack);
|
|
130
|
+
const errorResponse = createErrorResponse(error.message || "Failed to get suggestions");
|
|
131
|
+
return c.json(errorResponse, 400);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
// Define facets response schema
|
|
135
|
+
const facetsResponseSchema = BaseResponseSchema.extend({
|
|
136
|
+
data: z
|
|
137
|
+
.object({
|
|
138
|
+
categories: z.record(z.string(), z.number()).optional(),
|
|
139
|
+
authors: z.record(z.string(), z.number()).optional(),
|
|
140
|
+
validated: z.record(z.string(), z.number()).optional(),
|
|
141
|
+
})
|
|
142
|
+
.optional(),
|
|
143
|
+
}).openapi("FacetsResponse");
|
|
144
|
+
// Facets route definition
|
|
145
|
+
const facetsRoute = createRoute({
|
|
146
|
+
method: "get",
|
|
147
|
+
path: "/search/facets",
|
|
148
|
+
responses: createRouteResponses(facetsResponseSchema, {
|
|
149
|
+
includeErrorResponses: true,
|
|
150
|
+
}),
|
|
151
|
+
});
|
|
152
|
+
searchRoutes.openapi(facetsRoute, async (c) => {
|
|
153
|
+
try {
|
|
154
|
+
// Get facets information
|
|
155
|
+
const facets = await searchService.getFacets();
|
|
156
|
+
const response = createResponse({
|
|
157
|
+
categories: facets === null || facets === void 0 ? void 0 : facets.category,
|
|
158
|
+
authors: facets === null || facets === void 0 ? void 0 : facets.author,
|
|
159
|
+
validated: facets === null || facets === void 0 ? void 0 : facets.validated,
|
|
160
|
+
});
|
|
161
|
+
return c.json(response, 200);
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
console.error("Failed to get facets:", error.stack);
|
|
165
|
+
const errorResponse = createErrorResponse(error.message || "Failed to get facets", 500);
|
|
166
|
+
return c.json(errorResponse, 500);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
// Define health check response schema
|
|
170
|
+
const healthResponseSchema = BaseResponseSchema.extend({
|
|
171
|
+
data: z
|
|
172
|
+
.object({
|
|
173
|
+
status: z.string(),
|
|
174
|
+
host: z.string(),
|
|
175
|
+
initialized: z.boolean(),
|
|
176
|
+
indexName: z.string(),
|
|
177
|
+
documentCount: z.number(),
|
|
178
|
+
})
|
|
179
|
+
.optional(),
|
|
180
|
+
}).openapi("HealthResponse");
|
|
181
|
+
// Health check route definition
|
|
182
|
+
const healthRoute = createRoute({
|
|
183
|
+
method: "get",
|
|
184
|
+
path: "/search/health",
|
|
185
|
+
responses: createRouteResponses(healthResponseSchema, {
|
|
186
|
+
includeErrorResponses: true,
|
|
187
|
+
}),
|
|
188
|
+
});
|
|
189
|
+
searchRoutes.openapi(healthRoute, async (c) => {
|
|
190
|
+
try {
|
|
191
|
+
// Perform health check
|
|
192
|
+
const health = await searchService.healthCheck();
|
|
193
|
+
const response = createResponse(health);
|
|
194
|
+
return c.json(response, 200);
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
console.error("Health check failed:", error.stack);
|
|
198
|
+
const errorResponse = createErrorResponse(error.message || "Health check failed", 500);
|
|
199
|
+
return c.json(errorResponse, 500);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
// Define management response schema
|
|
203
|
+
const managementResponseSchema = BaseResponseSchema.extend({
|
|
204
|
+
data: z
|
|
205
|
+
.object({
|
|
206
|
+
message: z.string(),
|
|
207
|
+
details: z.any().optional(),
|
|
208
|
+
})
|
|
209
|
+
.optional(),
|
|
210
|
+
}).openapi("ManagementResponse");
|
|
211
|
+
// Initialize search service route definition
|
|
212
|
+
const initRoute = createRoute({
|
|
213
|
+
method: "post",
|
|
214
|
+
path: "/search/manage/init",
|
|
215
|
+
responses: createRouteResponses(managementResponseSchema, {
|
|
216
|
+
includeErrorResponses: true,
|
|
217
|
+
}),
|
|
218
|
+
});
|
|
219
|
+
searchRoutes.openapi(initRoute, async (c) => {
|
|
220
|
+
try {
|
|
221
|
+
// Initialize search service
|
|
222
|
+
await searchService.initialize();
|
|
223
|
+
const response = createResponse({
|
|
224
|
+
message: "Search service initialized successfully",
|
|
225
|
+
});
|
|
226
|
+
return c.json(response, 200);
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
console.error("Failed to initialize search service:", error.stack);
|
|
230
|
+
const errorResponse = createErrorResponse(error.message || "Failed to initialize search service", 500);
|
|
231
|
+
return c.json(errorResponse, 500);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
// Index packages route definition
|
|
235
|
+
const indexRoute = createRoute({
|
|
236
|
+
method: "post",
|
|
237
|
+
path: "/search/manage/index",
|
|
238
|
+
responses: createRouteResponses(managementResponseSchema, {
|
|
239
|
+
includeErrorResponses: true,
|
|
240
|
+
}),
|
|
241
|
+
});
|
|
242
|
+
searchRoutes.openapi(indexRoute, async (c) => {
|
|
243
|
+
try {
|
|
244
|
+
// Index all packages
|
|
245
|
+
const stats = await searchService.indexPackages();
|
|
246
|
+
const response = createResponse({
|
|
247
|
+
message: `Indexed ${stats.numberOfDocuments} documents`,
|
|
248
|
+
details: stats,
|
|
249
|
+
});
|
|
250
|
+
return c.json(response, 200);
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
console.error("Failed to index packages:", error.stack);
|
|
254
|
+
const errorResponse = createErrorResponse(error.message || "Failed to index packages", 500);
|
|
255
|
+
return c.json(errorResponse, 500);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
// Clear index route definition
|
|
259
|
+
const clearRoute = createRoute({
|
|
260
|
+
method: "post",
|
|
261
|
+
path: "/search/manage/clear",
|
|
262
|
+
responses: createRouteResponses(managementResponseSchema, {
|
|
263
|
+
includeErrorResponses: true,
|
|
264
|
+
}),
|
|
265
|
+
});
|
|
266
|
+
searchRoutes.openapi(clearRoute, async (c) => {
|
|
267
|
+
try {
|
|
268
|
+
// Clear the index
|
|
269
|
+
await searchService.clearIndex();
|
|
270
|
+
const response = createResponse({
|
|
271
|
+
message: "Index cleared successfully",
|
|
272
|
+
});
|
|
273
|
+
return c.json(response, 200);
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
console.error("Failed to clear index:", error.stack);
|
|
277
|
+
const errorResponse = createErrorResponse(error.message || "Failed to clear index", 500);
|
|
278
|
+
return c.json(errorResponse, 500);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
// Get index stats route definition
|
|
282
|
+
const statsRoute = createRoute({
|
|
283
|
+
method: "get",
|
|
284
|
+
path: "/search/manage/stats",
|
|
285
|
+
responses: createRouteResponses(managementResponseSchema, {
|
|
286
|
+
includeErrorResponses: true,
|
|
287
|
+
}),
|
|
288
|
+
});
|
|
289
|
+
searchRoutes.openapi(statsRoute, async (c) => {
|
|
290
|
+
try {
|
|
291
|
+
// Get index statistics
|
|
292
|
+
const stats = await searchService.getStats();
|
|
293
|
+
const response = createResponse({
|
|
294
|
+
message: "Index statistics retrieved successfully",
|
|
295
|
+
details: stats,
|
|
296
|
+
});
|
|
297
|
+
return c.json(response, 200);
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
console.error("Failed to get index statistics:", error.stack);
|
|
301
|
+
const errorResponse = createErrorResponse(error.message || "Failed to get index statistics", 500);
|
|
302
|
+
return c.json(errorResponse, 500);
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
export { searchRoutes };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MeiliSearch Service for Awesome MCP Registry
|
|
3
|
+
* Handles search indexing and querying for MCP packages
|
|
4
|
+
*/
|
|
5
|
+
import { type Index, MeiliSearch } from "meilisearch";
|
|
6
|
+
interface PackageData {
|
|
7
|
+
name?: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
category?: string;
|
|
10
|
+
validated?: boolean;
|
|
11
|
+
tools?: Record<string, {
|
|
12
|
+
description?: string;
|
|
13
|
+
}>;
|
|
14
|
+
path?: string;
|
|
15
|
+
}
|
|
16
|
+
interface SearchOptions {
|
|
17
|
+
limit?: number;
|
|
18
|
+
offset?: number;
|
|
19
|
+
filter?: string | string[];
|
|
20
|
+
sort?: string[];
|
|
21
|
+
matchingStrategy?: string;
|
|
22
|
+
}
|
|
23
|
+
interface SearchResults {
|
|
24
|
+
hits: unknown[];
|
|
25
|
+
query: string;
|
|
26
|
+
processingTimeMs: number;
|
|
27
|
+
limit: number;
|
|
28
|
+
offset: number;
|
|
29
|
+
estimatedTotalHits: number;
|
|
30
|
+
}
|
|
31
|
+
interface IndexStats {
|
|
32
|
+
numberOfDocuments: number;
|
|
33
|
+
isIndexing: boolean;
|
|
34
|
+
fieldDistribution: Record<string, number>;
|
|
35
|
+
}
|
|
36
|
+
declare class SearchService {
|
|
37
|
+
private host;
|
|
38
|
+
private apiKey;
|
|
39
|
+
private indexName;
|
|
40
|
+
protected client: MeiliSearch;
|
|
41
|
+
protected _index: Index | null;
|
|
42
|
+
protected isInitialized: boolean;
|
|
43
|
+
constructor(indexName?: string);
|
|
44
|
+
/**
|
|
45
|
+
* Get the initialization status
|
|
46
|
+
*/
|
|
47
|
+
getIsInitialized(): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Get the MeiliSearch client
|
|
50
|
+
*/
|
|
51
|
+
getClient(): MeiliSearch;
|
|
52
|
+
get index(): Index | null;
|
|
53
|
+
set index(value: Index | null);
|
|
54
|
+
initialize(): Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* Configure search index settings for optimal MCP package search
|
|
57
|
+
*/
|
|
58
|
+
configureIndex(): Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* Transform package data for search indexing
|
|
61
|
+
*/
|
|
62
|
+
transformPackageForIndex(packageName: string, packageData: PackageData): Record<string, unknown>;
|
|
63
|
+
/**
|
|
64
|
+
* Create a safe ID for MeiliSearch (alphanumeric, hyphens, underscores only)
|
|
65
|
+
*/
|
|
66
|
+
createSafeId(packageName: string): string;
|
|
67
|
+
/**
|
|
68
|
+
* Calculate a popularity score for ranking
|
|
69
|
+
*/
|
|
70
|
+
calculatePopularityScore(packageData: PackageData): number;
|
|
71
|
+
/**
|
|
72
|
+
* Extract author from package name
|
|
73
|
+
*/
|
|
74
|
+
extractAuthor(packageName: string): string;
|
|
75
|
+
/**
|
|
76
|
+
* Extract relevant keywords from package data
|
|
77
|
+
*/
|
|
78
|
+
extractKeywords(packageData: PackageData, packageName: string): string;
|
|
79
|
+
/**
|
|
80
|
+
* Index all packages from the packages-list.json file
|
|
81
|
+
*/
|
|
82
|
+
indexPackages(): Promise<IndexStats>;
|
|
83
|
+
/**
|
|
84
|
+
* Search packages with advanced options
|
|
85
|
+
*/
|
|
86
|
+
search(query: string, options?: SearchOptions): Promise<SearchResults>;
|
|
87
|
+
/**
|
|
88
|
+
* Get search suggestions/autocomplete
|
|
89
|
+
*/
|
|
90
|
+
suggest(query: string, limit?: number): Promise<{
|
|
91
|
+
name: string;
|
|
92
|
+
packageName: string;
|
|
93
|
+
category: string;
|
|
94
|
+
highlighted: string;
|
|
95
|
+
}[]>;
|
|
96
|
+
/**
|
|
97
|
+
* Get faceted search results (for filters)
|
|
98
|
+
*/
|
|
99
|
+
getFacets(): Promise<Record<string, unknown> | undefined>;
|
|
100
|
+
/**
|
|
101
|
+
* Get index statistics
|
|
102
|
+
*/
|
|
103
|
+
getStats(): Promise<IndexStats>;
|
|
104
|
+
/**
|
|
105
|
+
* Clear the index
|
|
106
|
+
*/
|
|
107
|
+
clearIndex(): Promise<void>;
|
|
108
|
+
/**
|
|
109
|
+
* Health check for the search service
|
|
110
|
+
*/
|
|
111
|
+
healthCheck(): Promise<{
|
|
112
|
+
status: string;
|
|
113
|
+
host: string;
|
|
114
|
+
initialized: boolean;
|
|
115
|
+
indexName: string;
|
|
116
|
+
documentCount: number;
|
|
117
|
+
}>;
|
|
118
|
+
}
|
|
119
|
+
declare const searchService: SearchService;
|
|
120
|
+
export default searchService;
|