qwksearch-mcp-server 1.0.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/bin/qwksearch-mcp.ts +2 -0
- package/package.json +29 -0
- package/src/index.ts +19 -0
- package/src/lib/search-instance.ts +10 -0
- package/src/tools/extract-page.ts +50 -0
- package/src/tools/render-page.ts +71 -0
- package/src/tools/web-search.ts +51 -0
- package/tsconfig.json +13 -0
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "qwksearch-mcp-server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server exposing web search and content extraction tools via stdio transport",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/OpenSourceAGI/qwksearch-research-agent.git",
|
|
8
|
+
"directory": "packages/qwksearch-mcp-server"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"bin": {
|
|
12
|
+
"qwksearch-mcp": "./bin/qwksearch-mcp.ts"
|
|
13
|
+
},
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"import": "./src/index.ts"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"start": "bun run bin/qwksearch-mcp.ts",
|
|
21
|
+
"dev": "bun --watch run bin/qwksearch-mcp.ts"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
25
|
+
"search-web-api": "^1.0.76",
|
|
26
|
+
"extract-webpage": "^1.2.140",
|
|
27
|
+
"zod": "^4.3.6"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { registerWebSearchTool } from "./tools/web-search.js";
|
|
4
|
+
import { registerExtractPageTool } from "./tools/extract-page.js";
|
|
5
|
+
import { registerRenderPageTool } from "./tools/render-page.js";
|
|
6
|
+
|
|
7
|
+
const server = new McpServer({
|
|
8
|
+
name: "qwksearch",
|
|
9
|
+
version: "1.0.0",
|
|
10
|
+
}, {
|
|
11
|
+
capabilities: { tools: {} },
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
registerWebSearchTool(server);
|
|
15
|
+
registerExtractPageTool(server);
|
|
16
|
+
registerRenderPageTool(server);
|
|
17
|
+
|
|
18
|
+
const transport = new StdioServerTransport();
|
|
19
|
+
await server.connect(transport);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { extractContent } from "extract-webpage/url-to-content/url-to-content";
|
|
4
|
+
|
|
5
|
+
export function registerExtractPageTool(server: McpServer) {
|
|
6
|
+
server.registerTool("extract_page", {
|
|
7
|
+
description:
|
|
8
|
+
"Extract content from a web page, PDF, or YouTube video. Uses Mozilla Readability and Postlight Mercury algorithms with 100+ custom adapters. Returns structured content with title, author, date, and citation.",
|
|
9
|
+
inputSchema: {
|
|
10
|
+
url: z.string().describe("URL of the page to extract"),
|
|
11
|
+
images: z.boolean().optional().default(true).describe("Include images in output"),
|
|
12
|
+
links: z.boolean().optional().default(true).describe("Include links in output"),
|
|
13
|
+
formatting: z.boolean().optional().default(true).describe("Preserve formatting"),
|
|
14
|
+
timeout: z.number().optional().default(10).describe("HTTP request timeout in seconds"),
|
|
15
|
+
},
|
|
16
|
+
}, async ({ url, images, links, formatting, timeout }) => {
|
|
17
|
+
try {
|
|
18
|
+
const result = await extractContent(url, {
|
|
19
|
+
images,
|
|
20
|
+
links,
|
|
21
|
+
formatting,
|
|
22
|
+
absoluteURLs: true,
|
|
23
|
+
timeout,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
if (!result || result.error) {
|
|
27
|
+
return {
|
|
28
|
+
content: [{ type: "text" as const, text: `Could not extract content from "${url}".` }],
|
|
29
|
+
isError: true,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let text = `Content extracted from: ${result.url || url}\n\n`;
|
|
34
|
+
if (result.title) text += `Title: ${result.title}\n`;
|
|
35
|
+
if (result.author) text += `Author: ${result.author}\n`;
|
|
36
|
+
if (result.date) text += `Date: ${result.date}\n`;
|
|
37
|
+
if (result.source) text += `Source: ${result.source}\n`;
|
|
38
|
+
if (result.word_count) text += `Word Count: ${result.word_count}\n`;
|
|
39
|
+
if (result.cite) text += `Citation: ${result.cite}\n`;
|
|
40
|
+
if (result.html) text += `\nContent:\n${result.html}\n`;
|
|
41
|
+
|
|
42
|
+
return { content: [{ type: "text" as const, text }] };
|
|
43
|
+
} catch (error: any) {
|
|
44
|
+
return {
|
|
45
|
+
content: [{ type: "text" as const, text: `Extraction failed for "${url}": ${error.message}` }],
|
|
46
|
+
isError: true,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
|
|
4
|
+
export function registerRenderPageTool(server: McpServer) {
|
|
5
|
+
server.registerTool("render_page_with_javascript", {
|
|
6
|
+
description:
|
|
7
|
+
"Render a web page with full JavaScript execution using Cloudflare Browser Rendering. Use for JavaScript-heavy sites (SPAs, React apps) or pages behind bot protection. Slower but more complete than extract_page.",
|
|
8
|
+
inputSchema: {
|
|
9
|
+
url: z.string().describe("URL of the page to render"),
|
|
10
|
+
wait: z.number().optional().default(0).describe("Additional wait time in ms after page load"),
|
|
11
|
+
timeout: z.number().optional().default(30000).describe("Timeout in ms"),
|
|
12
|
+
waitUntil: z
|
|
13
|
+
.enum(["domcontentloaded", "load", "networkidle0", "networkidle2"])
|
|
14
|
+
.optional()
|
|
15
|
+
.default("networkidle2")
|
|
16
|
+
.describe("When to consider navigation complete"),
|
|
17
|
+
},
|
|
18
|
+
}, async ({ url, wait, timeout, waitUntil }) => {
|
|
19
|
+
const scraperUrl = process.env.SCRAPER_URL || "https://scraper.qwksearch.workers.dev";
|
|
20
|
+
const scraperApiKey = process.env.SCRAPER_API_KEY;
|
|
21
|
+
|
|
22
|
+
if (!scraperApiKey) {
|
|
23
|
+
return {
|
|
24
|
+
content: [{ type: "text" as const, text: "render_page_with_javascript is unavailable: SCRAPER_API_KEY environment variable not set." }],
|
|
25
|
+
isError: true,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const requestUrl = new URL("/api/render", scraperUrl);
|
|
31
|
+
const response = await fetch(requestUrl.toString(), {
|
|
32
|
+
method: "POST",
|
|
33
|
+
headers: {
|
|
34
|
+
"Content-Type": "application/json",
|
|
35
|
+
Authorization: `Bearer ${scraperApiKey}`,
|
|
36
|
+
},
|
|
37
|
+
body: JSON.stringify({
|
|
38
|
+
url,
|
|
39
|
+
blockImages: true,
|
|
40
|
+
wait,
|
|
41
|
+
timeout,
|
|
42
|
+
waitUntil,
|
|
43
|
+
bypassCaptcha: true,
|
|
44
|
+
sessionId: "default",
|
|
45
|
+
format: "json",
|
|
46
|
+
}),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
const errorText = await response.text();
|
|
51
|
+
return {
|
|
52
|
+
content: [{ type: "text" as const, text: `Render failed: ${errorText}` }],
|
|
53
|
+
isError: true,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const data = await response.json() as any;
|
|
58
|
+
let text = `Page rendered: ${data.url || url}\n`;
|
|
59
|
+
if (data.title) text += `Title: ${data.title}\n`;
|
|
60
|
+
if (data.loadTime) text += `Load Time: ${data.loadTime}ms\n`;
|
|
61
|
+
text += `\n${data.html || ""}`;
|
|
62
|
+
|
|
63
|
+
return { content: [{ type: "text" as const, text }] };
|
|
64
|
+
} catch (error: any) {
|
|
65
|
+
return {
|
|
66
|
+
content: [{ type: "text" as const, text: `Render failed for "${url}": ${error.message}` }],
|
|
67
|
+
isError: true,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { getSearchInstance } from "../lib/search-instance.js";
|
|
4
|
+
|
|
5
|
+
export function registerWebSearchTool(server: McpServer) {
|
|
6
|
+
server.registerTool("web_search", {
|
|
7
|
+
description:
|
|
8
|
+
"Search the web for information on any topic. Returns relevant search results with titles, descriptions, and URLs from 75+ search engines across 10 categories.",
|
|
9
|
+
inputSchema: {
|
|
10
|
+
query: z.string().describe("Search query string"),
|
|
11
|
+
category: z
|
|
12
|
+
.enum(["general", "news", "videos", "images", "science", "files", "it", "academic", "torrents", "social", "maps", "shopping"])
|
|
13
|
+
.optional()
|
|
14
|
+
.default("general")
|
|
15
|
+
.describe("Search category to focus results"),
|
|
16
|
+
page: z
|
|
17
|
+
.number()
|
|
18
|
+
.optional()
|
|
19
|
+
.default(1)
|
|
20
|
+
.describe("Result page number"),
|
|
21
|
+
},
|
|
22
|
+
}, async ({ query, category, page }) => {
|
|
23
|
+
try {
|
|
24
|
+
const search = getSearchInstance();
|
|
25
|
+
const results = await search.search(query, page, undefined, [category]);
|
|
26
|
+
|
|
27
|
+
if (!results || results.length === 0) {
|
|
28
|
+
return {
|
|
29
|
+
content: [{ type: "text" as const, text: `No search results found for "${query}".` }],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let text = `Search results for "${query}" (${category}):\n\n`;
|
|
34
|
+
for (const [i, r] of results.entries()) {
|
|
35
|
+
text += `${i + 1}. ${r.title}\n`;
|
|
36
|
+
text += ` URL: ${r.url || r.link}\n`;
|
|
37
|
+
if (r.content) text += ` ${r.content}\n`;
|
|
38
|
+
if (r.engines?.length) text += ` Sources: ${r.engines.join(", ")}\n`;
|
|
39
|
+
text += "\n";
|
|
40
|
+
}
|
|
41
|
+
text += `Found ${results.length} results.`;
|
|
42
|
+
|
|
43
|
+
return { content: [{ type: "text" as const, text }] };
|
|
44
|
+
} catch (error: any) {
|
|
45
|
+
return {
|
|
46
|
+
content: [{ type: "text" as const, text: `Search failed: ${error.message}` }],
|
|
47
|
+
isError: true,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"outDir": "./dist",
|
|
10
|
+
"rootDir": "./src"
|
|
11
|
+
},
|
|
12
|
+
"include": ["src/**/*", "bin/**/*"]
|
|
13
|
+
}
|