scrapeless-mcp-server 0.5.0 → 0.6.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 +1 -0
- package/build/cf.js +27 -0
- package/build/config.js +11 -0
- package/build/context-manager.js +40 -0
- package/build/context.js +120 -0
- package/build/index.js +23 -0
- package/build/server.js +60 -0
- package/build/session-manager.js +76 -0
- package/build/tools/browser/browser.js +488 -0
- package/build/tools/crawl/api.js +26 -0
- package/build/tools/crawl/crawlCancel.js +19 -0
- package/build/tools/crawl/crawlResult.js +72 -0
- package/build/tools/crawl/crawlStart.js +138 -0
- package/build/tools/deepserp/googleSearch.js +37 -0
- package/build/tools/deepserp/googleTrends.js +208 -0
- package/build/tools/index.js +9 -0
- package/build/tools/llm_chat_scraper/api.js +27 -0
- package/build/tools/llm_chat_scraper/llmChatScraper.js +634 -0
- package/build/tools/universal/scrapeHtml.js +22 -0
- package/build/tools/universal/scrapeMarkdown.js +29 -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 +3 -2
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import z from "zod";
|
|
3
|
+
import { BASE_URL } from "../../config.js";
|
|
4
|
+
import { defineTool } from "../utils.js";
|
|
5
|
+
import { getLlmChatScraperApi, LLM_CHAT_SCRAPER_REQUEST_ENDPOINT, LLM_CHAT_SCRAPER_RESULT_ENDPOINT, } from "./api.js";
|
|
6
|
+
const POLL_INTERVAL_MS = 5000;
|
|
7
|
+
const DEFAULT_TIMEOUT_SECONDS = 180;
|
|
8
|
+
const MIN_TIMEOUT_SECONDS = 60;
|
|
9
|
+
const MAX_TIMEOUT_SECONDS = 600;
|
|
10
|
+
const ACTOR_OPTIONS = [
|
|
11
|
+
"scraper.chatgpt",
|
|
12
|
+
"scraper.gemini",
|
|
13
|
+
"scraper.perplexity",
|
|
14
|
+
"scraper.copilot",
|
|
15
|
+
"scraper.aimode",
|
|
16
|
+
"scraper.overview",
|
|
17
|
+
"scraper.grok",
|
|
18
|
+
"scraper.alexa",
|
|
19
|
+
];
|
|
20
|
+
const MODE_OPTIONS = [
|
|
21
|
+
"search",
|
|
22
|
+
"smart",
|
|
23
|
+
"chat",
|
|
24
|
+
"reasoning",
|
|
25
|
+
"study",
|
|
26
|
+
"MODEL_MODE_FAST",
|
|
27
|
+
"MODEL_MODE_EXPERT",
|
|
28
|
+
"MODEL_MODE_AUTO",
|
|
29
|
+
];
|
|
30
|
+
const ACTOR_CONFIG = {
|
|
31
|
+
"scraper.chatgpt": {
|
|
32
|
+
displayName: "ChatGPT",
|
|
33
|
+
supportsWebSearch: true,
|
|
34
|
+
supportsShopping: true,
|
|
35
|
+
},
|
|
36
|
+
"scraper.gemini": {
|
|
37
|
+
displayName: "Gemini",
|
|
38
|
+
},
|
|
39
|
+
"scraper.perplexity": {
|
|
40
|
+
displayName: "Perplexity",
|
|
41
|
+
supportsWebSearch: true,
|
|
42
|
+
},
|
|
43
|
+
"scraper.copilot": {
|
|
44
|
+
displayName: "Microsoft Copilot",
|
|
45
|
+
supportsMode: true,
|
|
46
|
+
requiresMode: true,
|
|
47
|
+
},
|
|
48
|
+
"scraper.aimode": {
|
|
49
|
+
displayName: "Google AI Mode",
|
|
50
|
+
supportsShopping: true,
|
|
51
|
+
supportsGoogleLocation: true,
|
|
52
|
+
},
|
|
53
|
+
"scraper.overview": {
|
|
54
|
+
displayName: "Google AI Overview",
|
|
55
|
+
supportsShopping: true,
|
|
56
|
+
supportsGoogleLocation: true,
|
|
57
|
+
},
|
|
58
|
+
"scraper.grok": {
|
|
59
|
+
displayName: "Grok",
|
|
60
|
+
supportsMode: true,
|
|
61
|
+
requiresMode: true,
|
|
62
|
+
},
|
|
63
|
+
"scraper.alexa": {
|
|
64
|
+
displayName: "Alexa",
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
const RESULT_FIELDS_BY_ACTOR = {
|
|
68
|
+
"scraper.chatgpt": [
|
|
69
|
+
"prompt",
|
|
70
|
+
"result_text",
|
|
71
|
+
"model",
|
|
72
|
+
"web_search",
|
|
73
|
+
"links",
|
|
74
|
+
"search_result",
|
|
75
|
+
"content_references",
|
|
76
|
+
"products",
|
|
77
|
+
"ads",
|
|
78
|
+
"map",
|
|
79
|
+
"search_model_queries",
|
|
80
|
+
],
|
|
81
|
+
"scraper.gemini": [
|
|
82
|
+
"result_text",
|
|
83
|
+
"prompt",
|
|
84
|
+
"citations",
|
|
85
|
+
"related_queries",
|
|
86
|
+
],
|
|
87
|
+
"scraper.perplexity": [
|
|
88
|
+
"prompt",
|
|
89
|
+
"result_text",
|
|
90
|
+
"related_prompt",
|
|
91
|
+
"web_results",
|
|
92
|
+
"media_items",
|
|
93
|
+
],
|
|
94
|
+
"scraper.copilot": [
|
|
95
|
+
"result_text",
|
|
96
|
+
"prompt",
|
|
97
|
+
"mode",
|
|
98
|
+
"links",
|
|
99
|
+
"citations",
|
|
100
|
+
],
|
|
101
|
+
"scraper.aimode": [
|
|
102
|
+
"result_text",
|
|
103
|
+
"result_md",
|
|
104
|
+
"result_html",
|
|
105
|
+
"raw_url",
|
|
106
|
+
"citations",
|
|
107
|
+
"search_result",
|
|
108
|
+
"products",
|
|
109
|
+
],
|
|
110
|
+
"scraper.overview": [
|
|
111
|
+
"content",
|
|
112
|
+
"rawtext",
|
|
113
|
+
"metadata",
|
|
114
|
+
"is_overview_shopping",
|
|
115
|
+
"products",
|
|
116
|
+
"source",
|
|
117
|
+
"web_source",
|
|
118
|
+
"ads",
|
|
119
|
+
],
|
|
120
|
+
"scraper.grok": [
|
|
121
|
+
"conversation",
|
|
122
|
+
"create_time",
|
|
123
|
+
"follow_up_suggestions",
|
|
124
|
+
"full_response",
|
|
125
|
+
"tool_usages",
|
|
126
|
+
"user_model",
|
|
127
|
+
"user_query",
|
|
128
|
+
"web_search_results",
|
|
129
|
+
"footnotes",
|
|
130
|
+
"x_search_results",
|
|
131
|
+
],
|
|
132
|
+
"scraper.alexa": [
|
|
133
|
+
"user_text",
|
|
134
|
+
"md_text",
|
|
135
|
+
"raw_text",
|
|
136
|
+
"completed",
|
|
137
|
+
"answer_fragment_uri",
|
|
138
|
+
"answer_revision",
|
|
139
|
+
"dialog_request_id",
|
|
140
|
+
"endpoint_id",
|
|
141
|
+
"fragment_count",
|
|
142
|
+
"conversation",
|
|
143
|
+
"directives",
|
|
144
|
+
"references",
|
|
145
|
+
"sources",
|
|
146
|
+
"suggestions",
|
|
147
|
+
"products",
|
|
148
|
+
],
|
|
149
|
+
};
|
|
150
|
+
const llmChatScraperSchema = z.object({
|
|
151
|
+
prompt: z
|
|
152
|
+
.string()
|
|
153
|
+
.min(1)
|
|
154
|
+
.describe("Question or prompt to send to the selected LLM Chat Scraper actor."),
|
|
155
|
+
actor: z
|
|
156
|
+
.enum(ACTOR_OPTIONS)
|
|
157
|
+
.describe("Scrapeless actor to run. Choose one explicitly: scraper.chatgpt, scraper.gemini, scraper.perplexity, scraper.copilot, scraper.aimode, scraper.overview, scraper.grok, or scraper.alexa."),
|
|
158
|
+
country: z
|
|
159
|
+
.string()
|
|
160
|
+
.optional()
|
|
161
|
+
.default("US")
|
|
162
|
+
.describe("Country or region code used by the actor. Defaults to US."),
|
|
163
|
+
web_search: z
|
|
164
|
+
.boolean()
|
|
165
|
+
.optional()
|
|
166
|
+
.describe("Enable web search for scraper.chatgpt or scraper.perplexity when explicitly provided."),
|
|
167
|
+
shopping: z
|
|
168
|
+
.boolean()
|
|
169
|
+
.optional()
|
|
170
|
+
.describe("Fetch shopping/product data for scraper.chatgpt, scraper.aimode, or scraper.overview when explicitly provided."),
|
|
171
|
+
mode: z
|
|
172
|
+
.enum(MODE_OPTIONS)
|
|
173
|
+
.optional()
|
|
174
|
+
.describe("Required for scraper.copilot or scraper.grok. Copilot: search, smart, chat, reasoning, study. Grok: MODEL_MODE_FAST, MODEL_MODE_EXPERT, MODEL_MODE_AUTO."),
|
|
175
|
+
location: z
|
|
176
|
+
.string()
|
|
177
|
+
.optional()
|
|
178
|
+
.describe("Google canonical location for Google AI Mode or Google AI Overview. Mutually exclusive with uule."),
|
|
179
|
+
uule: z
|
|
180
|
+
.string()
|
|
181
|
+
.optional()
|
|
182
|
+
.describe("Pre-encoded Google UULE for Google AI Mode or Google AI Overview. Mutually exclusive with location."),
|
|
183
|
+
webhook: z
|
|
184
|
+
.object({
|
|
185
|
+
url: z
|
|
186
|
+
.string()
|
|
187
|
+
.url()
|
|
188
|
+
.describe("Webhook URL Scrapeless calls when the task completes."),
|
|
189
|
+
})
|
|
190
|
+
.optional()
|
|
191
|
+
.describe("Optional webhook object passed to the Scrapeless task request."),
|
|
192
|
+
timeout_seconds: z
|
|
193
|
+
.number()
|
|
194
|
+
.int()
|
|
195
|
+
.min(MIN_TIMEOUT_SECONDS)
|
|
196
|
+
.max(MAX_TIMEOUT_SECONDS)
|
|
197
|
+
.optional()
|
|
198
|
+
.default(DEFAULT_TIMEOUT_SECONDS)
|
|
199
|
+
.describe("Maximum polling time in seconds. Defaults to 180. Minimum is 60 and maximum is 600."),
|
|
200
|
+
});
|
|
201
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
202
|
+
function textResponse(data) {
|
|
203
|
+
return {
|
|
204
|
+
content: [
|
|
205
|
+
{
|
|
206
|
+
type: "text",
|
|
207
|
+
text: `Response:\n\n${JSON.stringify(data)}`,
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function normalizeCountry(country) {
|
|
213
|
+
const normalized = (country ?? "US").trim().toUpperCase();
|
|
214
|
+
return normalized || "US";
|
|
215
|
+
}
|
|
216
|
+
function validateActorParams(params) {
|
|
217
|
+
const config = ACTOR_CONFIG[params.actor];
|
|
218
|
+
if (params.location && params.uule) {
|
|
219
|
+
return {
|
|
220
|
+
code: "invalid_input",
|
|
221
|
+
message: "location and uule are mutually exclusive.",
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
if (params.web_search !== undefined && !config.supportsWebSearch) {
|
|
225
|
+
return {
|
|
226
|
+
code: "invalid_input",
|
|
227
|
+
message: `web_search is not a supported option for ${params.actor}.`,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
if (params.shopping !== undefined && !config.supportsShopping) {
|
|
231
|
+
return {
|
|
232
|
+
code: "invalid_input",
|
|
233
|
+
message: `shopping is not a supported top-level option for ${params.actor}.`,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
if ((params.location || params.uule) && !config.supportsGoogleLocation) {
|
|
237
|
+
return {
|
|
238
|
+
code: "invalid_input",
|
|
239
|
+
message: `location and uule are only supported for scraper.aimode and scraper.overview.`,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
if (params.mode !== undefined && !config.supportsMode) {
|
|
243
|
+
return {
|
|
244
|
+
code: "invalid_input",
|
|
245
|
+
message: `mode is only supported for scraper.copilot and scraper.grok.`,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
if (params.actor === "scraper.copilot" && params.mode !== undefined) {
|
|
249
|
+
const allowedModes = ["search", "smart", "chat", "reasoning", "study"];
|
|
250
|
+
if (!allowedModes.includes(params.mode)) {
|
|
251
|
+
return {
|
|
252
|
+
code: "invalid_input",
|
|
253
|
+
message: `mode for scraper.copilot must be one of: ${allowedModes.join(", ")}.`,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (params.actor === "scraper.grok" && params.mode !== undefined) {
|
|
258
|
+
const allowedModes = [
|
|
259
|
+
"MODEL_MODE_FAST",
|
|
260
|
+
"MODEL_MODE_EXPERT",
|
|
261
|
+
"MODEL_MODE_AUTO",
|
|
262
|
+
];
|
|
263
|
+
if (!allowedModes.includes(params.mode)) {
|
|
264
|
+
return {
|
|
265
|
+
code: "invalid_input",
|
|
266
|
+
message: `mode for scraper.grok must be one of: ${allowedModes.join(", ")}.`,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (config.requiresMode &&
|
|
271
|
+
params.mode === undefined) {
|
|
272
|
+
return {
|
|
273
|
+
code: "invalid_input",
|
|
274
|
+
message: `mode is required for ${params.actor}.`,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
function buildActorInput(params, country) {
|
|
280
|
+
const actorInput = {
|
|
281
|
+
prompt: params.prompt,
|
|
282
|
+
country,
|
|
283
|
+
};
|
|
284
|
+
if (params.web_search !== undefined) {
|
|
285
|
+
actorInput.web_search = params.web_search;
|
|
286
|
+
}
|
|
287
|
+
if (params.shopping !== undefined) {
|
|
288
|
+
actorInput.shopping = params.shopping;
|
|
289
|
+
}
|
|
290
|
+
if (params.mode !== undefined) {
|
|
291
|
+
actorInput.mode = params.mode;
|
|
292
|
+
}
|
|
293
|
+
if (params.location)
|
|
294
|
+
actorInput.location = params.location;
|
|
295
|
+
if (params.uule)
|
|
296
|
+
actorInput.uule = params.uule;
|
|
297
|
+
return actorInput;
|
|
298
|
+
}
|
|
299
|
+
function isRecord(value) {
|
|
300
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
301
|
+
}
|
|
302
|
+
function getStringField(source, field) {
|
|
303
|
+
if (!isRecord(source))
|
|
304
|
+
return undefined;
|
|
305
|
+
const value = source[field];
|
|
306
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
307
|
+
}
|
|
308
|
+
function extractTaskId(createResponse) {
|
|
309
|
+
return getStringField(createResponse, "task_id");
|
|
310
|
+
}
|
|
311
|
+
function extractRequestId(createResponse) {
|
|
312
|
+
return getStringField(createResponse, "request_id");
|
|
313
|
+
}
|
|
314
|
+
function extractTaskStatus(resultResponse) {
|
|
315
|
+
return getStringField(resultResponse, "status")?.toLowerCase();
|
|
316
|
+
}
|
|
317
|
+
function extractTaskMessage(resultResponse) {
|
|
318
|
+
return getStringField(resultResponse, "message");
|
|
319
|
+
}
|
|
320
|
+
function extractTaskResult(resultResponse) {
|
|
321
|
+
return isRecord(resultResponse) ? resultResponse.task_result : undefined;
|
|
322
|
+
}
|
|
323
|
+
function pickActorResult(actor, result) {
|
|
324
|
+
if (!isRecord(result))
|
|
325
|
+
return result;
|
|
326
|
+
return RESULT_FIELDS_BY_ACTOR[actor].reduce((actorResult, field) => {
|
|
327
|
+
if (field in result)
|
|
328
|
+
actorResult[field] = result[field];
|
|
329
|
+
return actorResult;
|
|
330
|
+
}, {});
|
|
331
|
+
}
|
|
332
|
+
function formatPollHttpError(statusCode, responseData) {
|
|
333
|
+
return {
|
|
334
|
+
status_code: statusCode,
|
|
335
|
+
code: "api_error",
|
|
336
|
+
message: getStringField(responseData, "message") ||
|
|
337
|
+
getStringField(responseData, "error") ||
|
|
338
|
+
`LLM chat scraper result request failed with HTTP ${statusCode}.`,
|
|
339
|
+
response: responseData,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
function formatTaskResultError(code, message, response) {
|
|
343
|
+
return {
|
|
344
|
+
code,
|
|
345
|
+
message,
|
|
346
|
+
response,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
async function pollTask(api, taskId, timeoutSeconds) {
|
|
350
|
+
const startedAtMs = Date.now();
|
|
351
|
+
const startedAt = new Date(startedAtMs).toISOString();
|
|
352
|
+
const deadline = startedAtMs + timeoutSeconds * 1000;
|
|
353
|
+
let latestResult;
|
|
354
|
+
let latestStatus = "processing";
|
|
355
|
+
let latestHttpStatus;
|
|
356
|
+
while (true) {
|
|
357
|
+
const response = await api.get(`${LLM_CHAT_SCRAPER_RESULT_ENDPOINT}/${encodeURIComponent(taskId)}`, {
|
|
358
|
+
validateStatus: () => true,
|
|
359
|
+
});
|
|
360
|
+
latestResult = response.data;
|
|
361
|
+
latestHttpStatus = response.status;
|
|
362
|
+
if (response.status === 429) {
|
|
363
|
+
latestStatus = "rate_limited_retrying";
|
|
364
|
+
}
|
|
365
|
+
else if (response.status < 200 || response.status >= 300) {
|
|
366
|
+
const finishedAtMs = Date.now();
|
|
367
|
+
return {
|
|
368
|
+
taskId,
|
|
369
|
+
status: `http_${response.status}`,
|
|
370
|
+
terminal: true,
|
|
371
|
+
ok: false,
|
|
372
|
+
timedOut: false,
|
|
373
|
+
startedAt,
|
|
374
|
+
finishedAt: new Date(finishedAtMs).toISOString(),
|
|
375
|
+
elapsedMs: finishedAtMs - startedAtMs,
|
|
376
|
+
latestResult,
|
|
377
|
+
httpStatus: response.status,
|
|
378
|
+
error: formatPollHttpError(response.status, response.data),
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
const taskStatus = extractTaskStatus(response.data);
|
|
383
|
+
if (taskStatus === "success") {
|
|
384
|
+
const taskResult = extractTaskResult(response.data);
|
|
385
|
+
const finishedAtMs = Date.now();
|
|
386
|
+
if (!isRecord(taskResult)) {
|
|
387
|
+
return {
|
|
388
|
+
taskId,
|
|
389
|
+
status: "invalid_response",
|
|
390
|
+
terminal: true,
|
|
391
|
+
ok: false,
|
|
392
|
+
timedOut: false,
|
|
393
|
+
startedAt,
|
|
394
|
+
finishedAt: new Date(finishedAtMs).toISOString(),
|
|
395
|
+
elapsedMs: finishedAtMs - startedAtMs,
|
|
396
|
+
latestResult,
|
|
397
|
+
httpStatus: response.status,
|
|
398
|
+
error: formatTaskResultError("invalid_response", "Scrapeless result response status is success but task_result is missing or is not an object.", response.data),
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
return {
|
|
402
|
+
taskId,
|
|
403
|
+
status: taskStatus,
|
|
404
|
+
terminal: true,
|
|
405
|
+
ok: true,
|
|
406
|
+
timedOut: false,
|
|
407
|
+
startedAt,
|
|
408
|
+
finishedAt: new Date(finishedAtMs).toISOString(),
|
|
409
|
+
elapsedMs: finishedAtMs - startedAtMs,
|
|
410
|
+
latestResult,
|
|
411
|
+
taskResult,
|
|
412
|
+
httpStatus: response.status,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
if (taskStatus === "failed") {
|
|
416
|
+
const finishedAtMs = Date.now();
|
|
417
|
+
return {
|
|
418
|
+
taskId,
|
|
419
|
+
status: taskStatus,
|
|
420
|
+
terminal: true,
|
|
421
|
+
ok: false,
|
|
422
|
+
timedOut: false,
|
|
423
|
+
startedAt,
|
|
424
|
+
finishedAt: new Date(finishedAtMs).toISOString(),
|
|
425
|
+
elapsedMs: finishedAtMs - startedAtMs,
|
|
426
|
+
latestResult,
|
|
427
|
+
httpStatus: response.status,
|
|
428
|
+
error: formatTaskResultError("task_failed", extractTaskMessage(response.data) || "LLM chat scraper task failed.", response.data),
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
if (taskStatus === "pending" || taskStatus === "running") {
|
|
432
|
+
latestStatus = taskStatus;
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
const finishedAtMs = Date.now();
|
|
436
|
+
return {
|
|
437
|
+
taskId,
|
|
438
|
+
status: "invalid_response",
|
|
439
|
+
terminal: true,
|
|
440
|
+
ok: false,
|
|
441
|
+
timedOut: false,
|
|
442
|
+
startedAt,
|
|
443
|
+
finishedAt: new Date(finishedAtMs).toISOString(),
|
|
444
|
+
elapsedMs: finishedAtMs - startedAtMs,
|
|
445
|
+
latestResult,
|
|
446
|
+
httpStatus: response.status,
|
|
447
|
+
error: formatTaskResultError("invalid_response", "Scrapeless result response did not include a valid status: pending, running, success, or failed.", response.data),
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
const remainingMs = deadline - Date.now();
|
|
452
|
+
if (remainingMs <= 0) {
|
|
453
|
+
const finishedAtMs = Date.now();
|
|
454
|
+
return {
|
|
455
|
+
taskId,
|
|
456
|
+
status: latestStatus,
|
|
457
|
+
terminal: false,
|
|
458
|
+
ok: false,
|
|
459
|
+
timedOut: true,
|
|
460
|
+
startedAt,
|
|
461
|
+
finishedAt: new Date(finishedAtMs).toISOString(),
|
|
462
|
+
elapsedMs: finishedAtMs - startedAtMs,
|
|
463
|
+
latestResult,
|
|
464
|
+
httpStatus: latestHttpStatus,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
await sleep(Math.min(POLL_INTERVAL_MS, remainingMs));
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
function timeoutHelp(taskId) {
|
|
471
|
+
return {
|
|
472
|
+
message: "The LLM chat scraper task did not finish before the timeout. Fetch the result manually with the Scrapeless API.",
|
|
473
|
+
manual_result_request: {
|
|
474
|
+
method: "GET",
|
|
475
|
+
url: `${BASE_URL}${LLM_CHAT_SCRAPER_RESULT_ENDPOINT}/${taskId}`,
|
|
476
|
+
headers: {
|
|
477
|
+
"x-api-token": "YOUR_SCRAPELESS_KEY",
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
function formatApiError(error) {
|
|
483
|
+
if (axios.isAxiosError(error)) {
|
|
484
|
+
const statusCode = error.response?.status;
|
|
485
|
+
const responseData = error.response?.data;
|
|
486
|
+
return {
|
|
487
|
+
status_code: statusCode,
|
|
488
|
+
code: statusCode === 429 ? "rate_limited" : "api_error",
|
|
489
|
+
message: getStringField(responseData, "message") ||
|
|
490
|
+
getStringField(responseData, "error") ||
|
|
491
|
+
error.message,
|
|
492
|
+
response: responseData,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
return {
|
|
496
|
+
code: "unknown_error",
|
|
497
|
+
message: error.message,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
export const llmChatScraper = defineTool({
|
|
501
|
+
name: "llm_chat_scraper",
|
|
502
|
+
description: `Create an LLM Chat Scraper task for an explicit Scrapeless actor, then poll every 5 seconds until the answer is ready or the timeout is reached.
|
|
503
|
+
Supports ChatGPT, Gemini, Perplexity, Copilot, Google AI Mode, Google AI Overview, Grok, and Alexa.
|
|
504
|
+
Defaults to a 3 minute timeout. The timeout can be set from 60 to 600 seconds.
|
|
505
|
+
On timeout, returns the task_id and instructions for manually fetching the result.`,
|
|
506
|
+
inputSchema: llmChatScraperSchema.shape,
|
|
507
|
+
handle: async (rawParams, client, headers) => {
|
|
508
|
+
const params = llmChatScraperSchema.parse(rawParams);
|
|
509
|
+
const country = normalizeCountry(params.country);
|
|
510
|
+
const actorConfig = ACTOR_CONFIG[params.actor];
|
|
511
|
+
const startedAtMs = Date.now();
|
|
512
|
+
const startedAt = new Date(startedAtMs).toISOString();
|
|
513
|
+
const validationError = validateActorParams(params);
|
|
514
|
+
if (validationError) {
|
|
515
|
+
return textResponse({
|
|
516
|
+
status: "failed",
|
|
517
|
+
actor: params.actor,
|
|
518
|
+
actor_display_name: actorConfig.displayName,
|
|
519
|
+
error: validationError,
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
const actorInput = buildActorInput(params, country);
|
|
523
|
+
const taskRequest = {
|
|
524
|
+
actor: params.actor,
|
|
525
|
+
input: actorInput,
|
|
526
|
+
};
|
|
527
|
+
if (params.webhook) {
|
|
528
|
+
taskRequest.webhook = params.webhook;
|
|
529
|
+
}
|
|
530
|
+
let taskId;
|
|
531
|
+
let requestId;
|
|
532
|
+
let createResponseBody;
|
|
533
|
+
try {
|
|
534
|
+
const api = getLlmChatScraperApi(client, headers);
|
|
535
|
+
const createResponse = await api.post(LLM_CHAT_SCRAPER_REQUEST_ENDPOINT, taskRequest);
|
|
536
|
+
createResponseBody = createResponse.data;
|
|
537
|
+
taskId = extractTaskId(createResponseBody);
|
|
538
|
+
requestId = extractRequestId(createResponseBody);
|
|
539
|
+
if (!taskId) {
|
|
540
|
+
return textResponse({
|
|
541
|
+
status: "failed",
|
|
542
|
+
actor: params.actor,
|
|
543
|
+
actor_display_name: actorConfig.displayName,
|
|
544
|
+
request_id: requestId,
|
|
545
|
+
error: {
|
|
546
|
+
code: "missing_task_id",
|
|
547
|
+
message: "Scrapeless did not return a task id from /api/v2/scraper/request.",
|
|
548
|
+
},
|
|
549
|
+
create_response: createResponseBody,
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
const pollResult = await pollTask(api, taskId, params.timeout_seconds);
|
|
553
|
+
const finishedAtMs = Date.now();
|
|
554
|
+
if (pollResult.timedOut) {
|
|
555
|
+
return textResponse({
|
|
556
|
+
status: "timeout",
|
|
557
|
+
task_id: taskId,
|
|
558
|
+
request_id: requestId,
|
|
559
|
+
actor: params.actor,
|
|
560
|
+
actor_display_name: actorConfig.displayName,
|
|
561
|
+
timeout_seconds: params.timeout_seconds,
|
|
562
|
+
poll_interval_seconds: POLL_INTERVAL_MS / 1000,
|
|
563
|
+
execution_time: {
|
|
564
|
+
started_at: startedAt,
|
|
565
|
+
finished_at: new Date(finishedAtMs).toISOString(),
|
|
566
|
+
elapsed_ms: finishedAtMs - startedAtMs,
|
|
567
|
+
},
|
|
568
|
+
latest_status: pollResult.status,
|
|
569
|
+
latest_http_status: pollResult.httpStatus,
|
|
570
|
+
latest_result: pollResult.latestResult,
|
|
571
|
+
help: timeoutHelp(taskId),
|
|
572
|
+
create_response: createResponseBody,
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
if (!pollResult.ok) {
|
|
576
|
+
return textResponse({
|
|
577
|
+
status: "failed",
|
|
578
|
+
task_id: taskId,
|
|
579
|
+
request_id: requestId,
|
|
580
|
+
actor: params.actor,
|
|
581
|
+
actor_display_name: actorConfig.displayName,
|
|
582
|
+
timeout_seconds: params.timeout_seconds,
|
|
583
|
+
poll_interval_seconds: POLL_INTERVAL_MS / 1000,
|
|
584
|
+
execution_time: {
|
|
585
|
+
started_at: startedAt,
|
|
586
|
+
finished_at: new Date(finishedAtMs).toISOString(),
|
|
587
|
+
elapsed_ms: finishedAtMs - startedAtMs,
|
|
588
|
+
},
|
|
589
|
+
task_status: pollResult.status,
|
|
590
|
+
result_http_status: pollResult.httpStatus,
|
|
591
|
+
error: pollResult.error,
|
|
592
|
+
latest_result: pollResult.latestResult,
|
|
593
|
+
create_response: createResponseBody,
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
return textResponse({
|
|
597
|
+
status: "completed",
|
|
598
|
+
task_id: taskId,
|
|
599
|
+
request_id: requestId,
|
|
600
|
+
actor: params.actor,
|
|
601
|
+
actor_display_name: actorConfig.displayName,
|
|
602
|
+
timeout_seconds: params.timeout_seconds,
|
|
603
|
+
poll_interval_seconds: POLL_INTERVAL_MS / 1000,
|
|
604
|
+
execution_time: {
|
|
605
|
+
started_at: startedAt,
|
|
606
|
+
finished_at: new Date(finishedAtMs).toISOString(),
|
|
607
|
+
elapsed_ms: finishedAtMs - startedAtMs,
|
|
608
|
+
},
|
|
609
|
+
task_status: pollResult.status,
|
|
610
|
+
result_http_status: pollResult.httpStatus,
|
|
611
|
+
result: pickActorResult(params.actor, pollResult.taskResult),
|
|
612
|
+
create_response: createResponseBody,
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
catch (error) {
|
|
616
|
+
const finishedAtMs = Date.now();
|
|
617
|
+
return textResponse({
|
|
618
|
+
status: "failed",
|
|
619
|
+
task_id: taskId,
|
|
620
|
+
request_id: requestId,
|
|
621
|
+
actor: params.actor,
|
|
622
|
+
actor_display_name: actorConfig.displayName,
|
|
623
|
+
execution_time: {
|
|
624
|
+
started_at: startedAt,
|
|
625
|
+
finished_at: new Date(finishedAtMs).toISOString(),
|
|
626
|
+
elapsed_ms: finishedAtMs - startedAtMs,
|
|
627
|
+
},
|
|
628
|
+
error: formatApiError(error),
|
|
629
|
+
help: taskId ? timeoutHelp(taskId) : undefined,
|
|
630
|
+
create_response: createResponseBody,
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
},
|
|
634
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { defineTool, wrapMcpResponse } from "../utils.js";
|
|
2
|
+
import z from "zod";
|
|
3
|
+
export const scrapeHtml = defineTool({
|
|
4
|
+
name: 'scrape_html',
|
|
5
|
+
description: `Scrape a URL and return its full HTML content.
|
|
6
|
+
Restrictions: Activated for URLs that require JavaScript rendering or bot protection.
|
|
7
|
+
Valid: Get HTML from a dynamic, JS-heavy single-page application.
|
|
8
|
+
Invalid: Fetching a simple static page (use a standard HTTP client).`,
|
|
9
|
+
inputSchema: {
|
|
10
|
+
url: z.string().url().describe('target URL'),
|
|
11
|
+
},
|
|
12
|
+
handle: async (params, client) => {
|
|
13
|
+
return wrapMcpResponse(() => client.universal.scrape({
|
|
14
|
+
actor: 'unlocker.webunlocker',
|
|
15
|
+
input: {
|
|
16
|
+
url: params.url,
|
|
17
|
+
js_render: true,
|
|
18
|
+
response_type: 'html'
|
|
19
|
+
}
|
|
20
|
+
}));
|
|
21
|
+
}
|
|
22
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { defineTool, wrapMcpResponse } from "../utils.js";
|
|
2
|
+
import z from "zod";
|
|
3
|
+
export const scrapeMarkdown = defineTool({
|
|
4
|
+
name: "scrape_markdown",
|
|
5
|
+
description: `Scrape a URL and return its content as Markdown.
|
|
6
|
+
Restrictions: Best for articles, blog posts, and other text-heavy pages.
|
|
7
|
+
Valid: Scrape a news article to get its readable content.
|
|
8
|
+
Invalid: Scrape a complex web application dashboard.`,
|
|
9
|
+
inputSchema: {
|
|
10
|
+
url: z.string().url().describe("target URL"),
|
|
11
|
+
},
|
|
12
|
+
handle: async (params, client) => {
|
|
13
|
+
return wrapMcpResponse(() => client.universal.scrape({
|
|
14
|
+
actor: "unlocker.webunlocker",
|
|
15
|
+
input: {
|
|
16
|
+
url: params.url,
|
|
17
|
+
redirect: true,
|
|
18
|
+
jsRender: {
|
|
19
|
+
enabled: true,
|
|
20
|
+
headless: true,
|
|
21
|
+
waitUntil: "domcontentloaded",
|
|
22
|
+
instructions: [],
|
|
23
|
+
block: { resources: [], urls: [] },
|
|
24
|
+
response: { type: "markdown", options: { selector: "" } },
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
}));
|
|
28
|
+
},
|
|
29
|
+
});
|