scrapeless-mcp-server 0.3.0 → 0.4.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 +97 -14
- package/build/cf.js +27 -0
- package/build/config.js +7 -12
- package/build/context.js +120 -0
- package/build/index.js +10 -13
- package/build/server.js +55 -97
- package/build/session-manager.js +55 -0
- package/build/tools/browser/browser.js +444 -0
- package/build/tools/deepserp/googleFlights.js +188 -0
- package/build/tools/deepserp/googleScholar.js +74 -0
- package/build/tools/deepserp/googleSearch.js +269 -0
- package/build/tools/deepserp/googleTrends.js +208 -0
- package/build/tools/index.js +7 -0
- package/build/tools/universal/scrapeHtml.js +22 -0
- package/build/tools/universal/scrapeMarkdown.js +22 -0
- package/build/tools/universal/scrapeScreenshot.js +48 -0
- package/build/tools/utils.js +86 -0
- package/build/transport.js +120 -0
- package/package.json +11 -2
- package/build/client.js +0 -40
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { customAlphabet } from "nanoid";
|
|
2
|
+
const notRecordRoleList = [
|
|
3
|
+
'Page Title',
|
|
4
|
+
'Root Web Area',
|
|
5
|
+
'RootWebArea',
|
|
6
|
+
'generic',
|
|
7
|
+
'none',
|
|
8
|
+
'paragraph',
|
|
9
|
+
'InlineTextBox',
|
|
10
|
+
'image'
|
|
11
|
+
];
|
|
12
|
+
function assembleSnapshot(snap, indent = 0) {
|
|
13
|
+
let str = '';
|
|
14
|
+
const spaces = ' '.repeat(indent);
|
|
15
|
+
for (const child of snap.children || []) {
|
|
16
|
+
if (child.role === 'Page Title')
|
|
17
|
+
continue;
|
|
18
|
+
if (child.role === 'Root Web Area')
|
|
19
|
+
continue;
|
|
20
|
+
if (!notRecordRoleList.includes(child.role)) {
|
|
21
|
+
str += `${spaces}- ${child.role}: ${child.name}\n`;
|
|
22
|
+
}
|
|
23
|
+
if (child.role === 'image') {
|
|
24
|
+
str += `${spaces} - image\n`;
|
|
25
|
+
}
|
|
26
|
+
if (child.children && child.children.length > 0) {
|
|
27
|
+
str += assembleSnapshot(child, !notRecordRoleList.includes(child.role) ? indent + 2 : indent);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return str;
|
|
31
|
+
}
|
|
32
|
+
export function defineTool(tool) {
|
|
33
|
+
return tool;
|
|
34
|
+
}
|
|
35
|
+
export async function wrapMcpResponse(request) {
|
|
36
|
+
try {
|
|
37
|
+
const data = await request();
|
|
38
|
+
return {
|
|
39
|
+
content: [
|
|
40
|
+
{
|
|
41
|
+
type: "text",
|
|
42
|
+
text: `Response:\n\n${JSON.stringify(data)}`,
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
return {
|
|
49
|
+
content: [
|
|
50
|
+
{
|
|
51
|
+
type: "text",
|
|
52
|
+
text: `Failed to fetch data. Error: ${error.message}`,
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export function wrapMcpBrowserResponse(content) {
|
|
59
|
+
return {
|
|
60
|
+
content: [
|
|
61
|
+
{
|
|
62
|
+
type: "text",
|
|
63
|
+
text: content,
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
export function uuid() {
|
|
69
|
+
const nanoid1 = customAlphabet("0123456789abcdefghijklmnopqrstuvwxyz", 4);
|
|
70
|
+
const nanoid2 = customAlphabet("0123456789abcdefghijklmnopqrstuvwxyz", 4);
|
|
71
|
+
const nanoid3 = customAlphabet("0123456789abcdefghijklmnopqrstuvwxyz", 4);
|
|
72
|
+
const nanoid4 = customAlphabet("0123456789abcdefghijklmnopqrstuvwxyz", 6);
|
|
73
|
+
return `${nanoid1()}-${nanoid2()}-${nanoid3()}-${nanoid4()}`;
|
|
74
|
+
}
|
|
75
|
+
export async function snapshot(page) {
|
|
76
|
+
const snap = await page.accessibility.snapshot({ interestingOnly: false, includeIframes: true });
|
|
77
|
+
if (!snap) {
|
|
78
|
+
return "No accessibility snapshot available.";
|
|
79
|
+
}
|
|
80
|
+
const url = page.url();
|
|
81
|
+
const title = await page.title();
|
|
82
|
+
let snapshotString = `- Page URL: ${url}\n`;
|
|
83
|
+
snapshotString += `- Page Title: ${title}\n`;
|
|
84
|
+
snapshotString += `\`\`\`yaml\n- Page Snapshot\n\`\`\`yaml`;
|
|
85
|
+
return snapshotString + assembleSnapshot(snap, 0);
|
|
86
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
6
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
7
|
+
import { createMcpServer, ServerList } from "./server.js";
|
|
8
|
+
import { API_KEY_NAME } from "./config.js";
|
|
9
|
+
export async function startStdioTransport() {
|
|
10
|
+
const serverList = new ServerList(async () => createMcpServer());
|
|
11
|
+
const server = await serverList.create();
|
|
12
|
+
await server.connect(new StdioServerTransport());
|
|
13
|
+
}
|
|
14
|
+
async function handleSSE(req, res, url, sessions) {
|
|
15
|
+
const serverList = createRemoteServerList(req, res);
|
|
16
|
+
if (!serverList)
|
|
17
|
+
return;
|
|
18
|
+
if (req.method === "POST") {
|
|
19
|
+
const sessionId = url.searchParams.get("sessionId");
|
|
20
|
+
if (!sessionId) {
|
|
21
|
+
res.statusCode = 400;
|
|
22
|
+
return res.end("Missing sessionId");
|
|
23
|
+
}
|
|
24
|
+
const transport = sessions.get(sessionId);
|
|
25
|
+
if (!transport) {
|
|
26
|
+
res.statusCode = 404;
|
|
27
|
+
return res.end("Session not found");
|
|
28
|
+
}
|
|
29
|
+
return await transport.handlePostMessage(req, res);
|
|
30
|
+
}
|
|
31
|
+
else if (req.method === "GET") {
|
|
32
|
+
const transport = new SSEServerTransport("/sse", res);
|
|
33
|
+
sessions.set(transport.sessionId, transport);
|
|
34
|
+
const server = await serverList.create();
|
|
35
|
+
res.on("close", () => {
|
|
36
|
+
sessions.delete(transport.sessionId);
|
|
37
|
+
serverList.close(server).catch((e) => {
|
|
38
|
+
// eslint-disable-next-line no-console
|
|
39
|
+
// console.error(e);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
return await server.connect(transport);
|
|
43
|
+
}
|
|
44
|
+
res.statusCode = 405;
|
|
45
|
+
res.end("Method not allowed");
|
|
46
|
+
}
|
|
47
|
+
async function handleStreamable(req, res, sessions) {
|
|
48
|
+
const serverList = createRemoteServerList(req, res);
|
|
49
|
+
if (!serverList)
|
|
50
|
+
return;
|
|
51
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
52
|
+
if (sessionId) {
|
|
53
|
+
const transport = sessions.get(sessionId);
|
|
54
|
+
if (!transport) {
|
|
55
|
+
res.statusCode = 404;
|
|
56
|
+
res.end("Session not found");
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
return await transport.handleRequest(req, res);
|
|
60
|
+
}
|
|
61
|
+
if (req.method === "POST") {
|
|
62
|
+
const transport = new StreamableHTTPServerTransport({
|
|
63
|
+
sessionIdGenerator: () => crypto.randomUUID(),
|
|
64
|
+
onsessioninitialized: (sessionId) => {
|
|
65
|
+
sessions.set(sessionId, transport);
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
transport.onclose = () => {
|
|
69
|
+
if (transport.sessionId)
|
|
70
|
+
sessions.delete(transport.sessionId);
|
|
71
|
+
};
|
|
72
|
+
const server = await serverList.create();
|
|
73
|
+
await server.connect(transport);
|
|
74
|
+
return await transport.handleRequest(req, res);
|
|
75
|
+
}
|
|
76
|
+
res.statusCode = 400;
|
|
77
|
+
res.end("Invalid request");
|
|
78
|
+
}
|
|
79
|
+
export function startHttpTransport(port, hostname) {
|
|
80
|
+
const sseSessions = new Map();
|
|
81
|
+
const streamableSessions = new Map();
|
|
82
|
+
const httpServer = http.createServer(async (req, res) => {
|
|
83
|
+
const url = new URL(`http://localhost${req.url}`);
|
|
84
|
+
if (url.pathname.startsWith("/mcp"))
|
|
85
|
+
await handleStreamable(req, res, streamableSessions);
|
|
86
|
+
else
|
|
87
|
+
await handleSSE(req, res, url, sseSessions);
|
|
88
|
+
});
|
|
89
|
+
httpServer.listen(port, hostname, () => {
|
|
90
|
+
const address = httpServer.address();
|
|
91
|
+
assert(address, "Could not bind server socket");
|
|
92
|
+
let url;
|
|
93
|
+
if (typeof address === "string") {
|
|
94
|
+
url = address;
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
const resolvedPort = address.port;
|
|
98
|
+
let resolvedHost = address.family === "IPv4" ? address.address : `[${address.address}]`;
|
|
99
|
+
if (resolvedHost === "0.0.0.0" || resolvedHost === "[::]")
|
|
100
|
+
resolvedHost = "localhost";
|
|
101
|
+
url = `http://${resolvedHost}:${resolvedPort}`;
|
|
102
|
+
}
|
|
103
|
+
const message = [
|
|
104
|
+
`Listening on ${url}`,
|
|
105
|
+
"If your client supports streamable HTTP, you can use the /mcp endpoint instead.",
|
|
106
|
+
].join("\n");
|
|
107
|
+
// eslint-disable-next-line no-console
|
|
108
|
+
console.log(message);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
function createRemoteServerList(req, res) {
|
|
112
|
+
const apiKey = req.headers[API_KEY_NAME];
|
|
113
|
+
if (!apiKey) {
|
|
114
|
+
res.statusCode = 401;
|
|
115
|
+
res.end("Unauthorized: Missing x-api-token header");
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
const serverList = new ServerList(async () => createMcpServer(apiKey));
|
|
119
|
+
return serverList;
|
|
120
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scrapeless-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"scrapeless-mcp-server": "./build/index.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"build": "tsc && chmod 755 build/index.js"
|
|
10
|
+
"build": "tsc && chmod 755 build/index.js",
|
|
11
|
+
"start:http": "node build/index.js --port=9593",
|
|
12
|
+
"dev:http": "tsc && node build/index.js --port=9593"
|
|
11
13
|
},
|
|
12
14
|
"files": [
|
|
13
15
|
"build"
|
|
@@ -29,10 +31,17 @@
|
|
|
29
31
|
"dependencies": {
|
|
30
32
|
"@chatmcp/sdk": "^1.0.5",
|
|
31
33
|
"@modelcontextprotocol/sdk": "^1.8.0",
|
|
34
|
+
"@scrapeless-ai/sdk": "^1.6.1",
|
|
35
|
+
"agents": "^0.0.101",
|
|
32
36
|
"axios": "^1.8.4",
|
|
37
|
+
"express": "^5.1.0",
|
|
38
|
+
"nanoid": "^5.1.5",
|
|
39
|
+
"puppeteer-core": "^24.12.0",
|
|
40
|
+
"yaml": "^2.8.0",
|
|
33
41
|
"zod": "^3.24.2"
|
|
34
42
|
},
|
|
35
43
|
"devDependencies": {
|
|
44
|
+
"@types/express": "^5.0.3",
|
|
36
45
|
"@types/node": "^22.13.14",
|
|
37
46
|
"typescript": "^5.8.2"
|
|
38
47
|
},
|
package/build/client.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import axios from "axios";
|
|
2
|
-
export class ScrapelessClient {
|
|
3
|
-
api;
|
|
4
|
-
constructor(config) {
|
|
5
|
-
this.api = axios.create({
|
|
6
|
-
baseURL: config.baseURL,
|
|
7
|
-
headers: {
|
|
8
|
-
"Content-Type": "application/json",
|
|
9
|
-
"x-api-token": config.token,
|
|
10
|
-
},
|
|
11
|
-
});
|
|
12
|
-
}
|
|
13
|
-
async sendRequest(endpoint, actor, inputData) {
|
|
14
|
-
try {
|
|
15
|
-
const response = await this.api.post(endpoint, {
|
|
16
|
-
actor,
|
|
17
|
-
input: inputData,
|
|
18
|
-
});
|
|
19
|
-
return {
|
|
20
|
-
content: [
|
|
21
|
-
{
|
|
22
|
-
type: "text",
|
|
23
|
-
text: `Response:\n\n${JSON.stringify(response.data)}`,
|
|
24
|
-
},
|
|
25
|
-
],
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
catch (error) {
|
|
29
|
-
console.error("Scrapeless API Error:", error);
|
|
30
|
-
return {
|
|
31
|
-
content: [
|
|
32
|
-
{
|
|
33
|
-
type: "text",
|
|
34
|
-
text: `Failed to fetch data. Error: ${error.message}`,
|
|
35
|
-
},
|
|
36
|
-
],
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|