@webcrawl/mcp 1.4.3 → 1.4.5
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/dist/core/engine/chrome-cdp.d.ts +29 -0
- package/dist/core/engine/chrome-cdp.js +339 -0
- package/dist/core/engine/chrome-launch.d.ts +49 -0
- package/dist/core/engine/chrome-launch.js +244 -0
- package/dist/core/engine/chrome.d.ts +26 -7
- package/dist/core/engine/chrome.js +155 -78
- package/dist/core/extract/index.d.ts +1 -1
- package/dist/core/extract/index.js +1 -1
- package/dist/core/extract/structured.d.ts +19 -0
- package/dist/core/extract/structured.js +192 -20
- package/dist/core/local-engine.d.ts +6 -0
- package/dist/core/local-engine.js +138 -79
- package/dist/core/search/catalog.d.ts +1 -1
- package/dist/core/search/catalog.js +1 -1
- package/dist/core/search/pipeline.js +5 -3
- package/dist/core/search/providers.d.ts +15 -3
- package/dist/core/search/providers.js +177 -39
- package/dist/core/targets/registry.js +1 -1
- package/dist/server.js +3 -0
- package/package.json +3 -3
|
@@ -115,7 +115,9 @@ export async function searchWeb(req) {
|
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
const perEngineLimit = Math.min(Math.max(limit + 4, 8), 15);
|
|
118
|
-
|
|
118
|
+
// Must outlast DDG/Brave impersonation (2200ms) so a slow SERP is not cut off
|
|
119
|
+
// after Wikipedia's 2 hits — the v1.4.3 keyless-search failure mode.
|
|
120
|
+
const quorumTimeoutMs = req.rankOptions?.quorumTimeoutMs ?? 2800;
|
|
119
121
|
// Stage 1: Hedged Multi-Engine Native Federation
|
|
120
122
|
const collectedOffers = [];
|
|
121
123
|
const offerByUrl = new Map();
|
|
@@ -257,7 +259,7 @@ export async function searchWeb(req) {
|
|
|
257
259
|
filterContext,
|
|
258
260
|
};
|
|
259
261
|
let ranked = await rankDocuments(localizedSearchQuery, rankCandidates, rankOpts);
|
|
260
|
-
// Thin result set after ranking and filtering: ask
|
|
262
|
+
// Thin result set after ranking and filtering: ask the keyed search provider once, then re-rank.
|
|
261
263
|
const minResults = Math.min(3, limit);
|
|
262
264
|
if ((!req.provider || req.provider === "auto") &&
|
|
263
265
|
ranked.results.length < minResults &&
|
|
@@ -649,7 +651,7 @@ export async function searchWeb(req) {
|
|
|
649
651
|
...(matchedCves?.length ? { cves: matchedCves } : {}),
|
|
650
652
|
};
|
|
651
653
|
});
|
|
652
|
-
// Charge the fallback surcharge only when a
|
|
654
|
+
// Charge the fallback surcharge only when a keyed-provider result actually reached the caller.
|
|
653
655
|
// An explicitly chosen provider is not a fallback.
|
|
654
656
|
const usedJinaFallback = (!req.provider || req.provider === "auto") &&
|
|
655
657
|
finalItems.some((item) => item.sources?.includes("jina"));
|
|
@@ -14,18 +14,30 @@ export declare function isEntityQuery(q: string): boolean;
|
|
|
14
14
|
* preventing any single slow engine from inflating tail latency.
|
|
15
15
|
*/
|
|
16
16
|
export declare function fetchHedgedNativeFederation(rawQuery: string, localizedQuery: string, perEngineLimit: number, req: SearchRequest, userLocation: UserLocationContext, quorumTimeoutMs: number, targetSites: string[], onOffer: (offer: ProductOffer) => void, onCve?: (cve: CveRecord) => void): Promise<RawCandidateDocument[]>;
|
|
17
|
-
/** Below this many unique URLs from the free engines, the
|
|
17
|
+
/** Below this many unique URLs from the free engines, the keyed search fallback runs. */
|
|
18
18
|
export declare const JINA_FALLBACK_MIN_URLS = 3;
|
|
19
19
|
/** True when a scraped SERP is a bot-check / captcha rather than results. */
|
|
20
20
|
export declare function isSerpChallengeHtml(html: string, status?: number): boolean;
|
|
21
21
|
export declare const EMPTY_SEARCH_ERROR: string;
|
|
22
|
+
/** Canonical URL for search dedupe (host lowercased, trailing slash / tracking params stripped). */
|
|
23
|
+
export declare function canonicalSearchUrl(raw: string | undefined): string;
|
|
24
|
+
/** Round-robin merge so one source (e.g. Wikipedia) cannot fill `-n` by itself. */
|
|
25
|
+
export declare function mergeSearchCandidates(groups: RawCandidateDocument[][], limit: number): RawCandidateDocument[];
|
|
26
|
+
export declare function withRetry<T>(fn: () => Promise<T>, opts?: {
|
|
27
|
+
retries?: number;
|
|
28
|
+
delayMs?: number;
|
|
29
|
+
isEmpty?: (v: T) => boolean;
|
|
30
|
+
}): Promise<T>;
|
|
22
31
|
/** Parse Bing's `format=rss` web-search feed into candidate docs. */
|
|
23
32
|
export declare function parseBingWebRss(xml: string, limit: number): RawCandidateDocument[];
|
|
24
33
|
/**
|
|
25
|
-
* Last-resort keyless engines used when DDG/Brave HTML SERPs come back empty
|
|
26
|
-
* Wikipedia OpenSearch, HN Algolia,
|
|
34
|
+
* Last-resort keyless engines used when DDG/Brave HTML SERPs come back empty
|
|
35
|
+
* or thinner than `limit`. Wikipedia OpenSearch, HN Algolia, Bing RSS, and
|
|
36
|
+
* DuckDuckGo Instant Answer do not require a JS challenge or API key.
|
|
27
37
|
*/
|
|
28
38
|
export declare function fetchKeylessSearchFallbacks(query: string, limit: number): Promise<RawCandidateDocument[]>;
|
|
39
|
+
/** DuckDuckGo Instant Answer API — keyless JSON, unrelated to the HTML SERP challenge. */
|
|
40
|
+
export declare function fetchDuckDuckGoInstantAnswer(query: string, limit: number): Promise<RawCandidateDocument[]>;
|
|
29
41
|
export declare function fetchJinaSearch(query: string, limit: number, req?: {
|
|
30
42
|
country?: string;
|
|
31
43
|
lang?: string;
|
|
@@ -89,11 +89,15 @@ export async function fetchHedgedNativeFederation(rawQuery, localizedQuery, perE
|
|
|
89
89
|
},
|
|
90
90
|
wikipedia: {
|
|
91
91
|
primary: false,
|
|
92
|
-
run: () => fetchWikipediaOpenSearch(localizedQuery,
|
|
92
|
+
run: () => fetchWikipediaOpenSearch(localizedQuery, Math.min(perEngineLimit, 5)),
|
|
93
93
|
},
|
|
94
94
|
"hn-algolia": {
|
|
95
95
|
primary: false,
|
|
96
|
-
run: () => fetchHackerNewsAlgolia(localizedQuery,
|
|
96
|
+
run: () => fetchHackerNewsAlgolia(localizedQuery, perEngineLimit),
|
|
97
|
+
},
|
|
98
|
+
"bing-rss": {
|
|
99
|
+
primary: false,
|
|
100
|
+
run: () => fetchBingWebRss(localizedQuery, perEngineLimit),
|
|
97
101
|
},
|
|
98
102
|
arxiv: {
|
|
99
103
|
primary: category === "academic",
|
|
@@ -159,14 +163,17 @@ export async function fetchHedgedNativeFederation(rawQuery, localizedQuery, perE
|
|
|
159
163
|
resolve();
|
|
160
164
|
return;
|
|
161
165
|
}
|
|
162
|
-
|
|
166
|
+
const uniqueCount = new Set(collected.map((c) => canonicalSearchUrl(c.url)).filter(Boolean)).size;
|
|
167
|
+
const fillTarget = Math.max(1, req.limit ?? 5);
|
|
168
|
+
// Early-exit quorum: at least 1 primary web engine + 2 total engines + enough
|
|
169
|
+
// *unique* URLs to fill `limit` (not just Wikipedia's 2 hits).
|
|
163
170
|
if (completedShoppingEngine &&
|
|
164
171
|
completedNewsEngine &&
|
|
165
172
|
completedNvdEngine &&
|
|
166
173
|
completedArxivEngine &&
|
|
167
174
|
completedPrimaryEngines >= 1 &&
|
|
168
175
|
totalCompletedEngines >= 2 &&
|
|
169
|
-
|
|
176
|
+
uniqueCount >= fillTarget &&
|
|
170
177
|
!graceTimer) {
|
|
171
178
|
graceTimer = setTimeout(() => {
|
|
172
179
|
if (!settled) {
|
|
@@ -209,7 +216,7 @@ export async function fetchHedgedNativeFederation(rawQuery, localizedQuery, perE
|
|
|
209
216
|
}
|
|
210
217
|
});
|
|
211
218
|
// Fallback: the free engines are scraped and can be blocked or rate limited. When they
|
|
212
|
-
// come back thin, fill in from
|
|
219
|
+
// come back thin, fill in from a keyed search provider so the caller still gets an answer.
|
|
213
220
|
const uniqueUrls = new Set(collected.map((c) => c.url).filter(Boolean));
|
|
214
221
|
if (uniqueUrls.size < JINA_FALLBACK_MIN_URLS && process.env.JINA_API_KEY) {
|
|
215
222
|
const t0 = performance.now();
|
|
@@ -220,14 +227,17 @@ export async function fetchHedgedNativeFederation(rawQuery, localizedQuery, perE
|
|
|
220
227
|
collected.push(...jina);
|
|
221
228
|
}
|
|
222
229
|
// HTML SERPs (DDG, Brave) are frequently challenge/429 pages. When they
|
|
223
|
-
// yield nothing
|
|
224
|
-
|
|
225
|
-
|
|
230
|
+
// yield nothing *or come back thinner than `limit`*, fill from keyless APIs
|
|
231
|
+
// (Wikipedia, HN Algolia, Bing RSS, DDG Instant Answer) with retry/backoff.
|
|
232
|
+
const fillTarget = Math.max(1, req.limit ?? 5);
|
|
233
|
+
const uniqueAfter = new Set(collected.map((c) => canonicalSearchUrl(c.url)).filter(Boolean));
|
|
234
|
+
if (uniqueAfter.size < fillTarget && (!req.provider || req.provider === "auto")) {
|
|
235
|
+
const extras = await fetchKeylessSearchFallbacks(localizedQuery, Math.max(fillTarget, perEngineLimit));
|
|
226
236
|
collected.push(...extras);
|
|
227
237
|
}
|
|
228
238
|
return collected;
|
|
229
239
|
}
|
|
230
|
-
/** Below this many unique URLs from the free engines, the
|
|
240
|
+
/** Below this many unique URLs from the free engines, the keyed search fallback runs. */
|
|
231
241
|
export const JINA_FALLBACK_MIN_URLS = 3;
|
|
232
242
|
/** True when a scraped SERP is a bot-check / captcha rather than results. */
|
|
233
243
|
export function isSerpChallengeHtml(html, status = 200) {
|
|
@@ -242,8 +252,64 @@ export function isSerpChallengeHtml(html, status = 200) {
|
|
|
242
252
|
lower.includes("enable javascript to function"));
|
|
243
253
|
}
|
|
244
254
|
export const EMPTY_SEARCH_ERROR = "Keyless search engines returned no results (DuckDuckGo and Brave often serve a bot challenge). " +
|
|
245
|
-
"Tried Wikipedia, Hacker News,
|
|
255
|
+
"Tried Wikipedia, Hacker News, Bing RSS, and DuckDuckGo Instant Answer as well. " +
|
|
246
256
|
"Set BRAVE_SEARCH_API_KEY, SERPER_API_KEY, or JINA_API_KEY, or retry later.";
|
|
257
|
+
/** Canonical URL for search dedupe (host lowercased, trailing slash / tracking params stripped). */
|
|
258
|
+
export function canonicalSearchUrl(raw) {
|
|
259
|
+
if (!raw)
|
|
260
|
+
return "";
|
|
261
|
+
try {
|
|
262
|
+
const u = new URL(raw);
|
|
263
|
+
u.hash = "";
|
|
264
|
+
u.hostname = u.hostname.toLowerCase();
|
|
265
|
+
for (const key of [...u.searchParams.keys()]) {
|
|
266
|
+
if (/^(utm_|ref|fbclid|gclid|mc_)/i.test(key))
|
|
267
|
+
u.searchParams.delete(key);
|
|
268
|
+
}
|
|
269
|
+
let path = u.pathname.replace(/\/+$/, "") || "/";
|
|
270
|
+
u.pathname = path;
|
|
271
|
+
return u.toString();
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
return raw.replace(/\/+$/, "").toLowerCase();
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/** Round-robin merge so one source (e.g. Wikipedia) cannot fill `-n` by itself. */
|
|
278
|
+
export function mergeSearchCandidates(groups, limit) {
|
|
279
|
+
const seen = new Set();
|
|
280
|
+
const out = [];
|
|
281
|
+
const queues = groups.map((g) => [...g]);
|
|
282
|
+
let progressed = true;
|
|
283
|
+
while (out.length < limit && progressed) {
|
|
284
|
+
progressed = false;
|
|
285
|
+
for (const q of queues) {
|
|
286
|
+
while (q.length > 0) {
|
|
287
|
+
const item = q.shift();
|
|
288
|
+
const key = canonicalSearchUrl(item.url);
|
|
289
|
+
if (!key || seen.has(key))
|
|
290
|
+
continue;
|
|
291
|
+
seen.add(key);
|
|
292
|
+
out.push(item);
|
|
293
|
+
progressed = true;
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
if (out.length >= limit)
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return out;
|
|
301
|
+
}
|
|
302
|
+
export async function withRetry(fn, opts = {}) {
|
|
303
|
+
const retries = opts.retries ?? 1;
|
|
304
|
+
const delayMs = opts.delayMs ?? 250;
|
|
305
|
+
const isEmpty = opts.isEmpty ?? ((v) => (Array.isArray(v) ? v.length === 0 : !v));
|
|
306
|
+
let last = await fn();
|
|
307
|
+
for (let i = 0; i < retries && isEmpty(last); i++) {
|
|
308
|
+
await new Promise((r) => setTimeout(r, delayMs * (i + 1)));
|
|
309
|
+
last = await fn();
|
|
310
|
+
}
|
|
311
|
+
return last;
|
|
312
|
+
}
|
|
247
313
|
/** Parse Bing's `format=rss` web-search feed into candidate docs. */
|
|
248
314
|
export function parseBingWebRss(xml, limit) {
|
|
249
315
|
const items = [];
|
|
@@ -296,27 +362,83 @@ async function fetchBingWebRss(query, limit) {
|
|
|
296
362
|
}
|
|
297
363
|
}
|
|
298
364
|
/**
|
|
299
|
-
* Last-resort keyless engines used when DDG/Brave HTML SERPs come back empty
|
|
300
|
-
* Wikipedia OpenSearch, HN Algolia,
|
|
365
|
+
* Last-resort keyless engines used when DDG/Brave HTML SERPs come back empty
|
|
366
|
+
* or thinner than `limit`. Wikipedia OpenSearch, HN Algolia, Bing RSS, and
|
|
367
|
+
* DuckDuckGo Instant Answer do not require a JS challenge or API key.
|
|
301
368
|
*/
|
|
302
369
|
export async function fetchKeylessSearchFallbacks(query, limit) {
|
|
303
370
|
const per = Math.max(limit, 5);
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
371
|
+
const empty = (v) => v.length === 0;
|
|
372
|
+
const run = () => Promise.all([
|
|
373
|
+
withRetry(() => fetchHackerNewsAlgolia(query, per), { retries: 1, delayMs: 280, isEmpty: empty }),
|
|
374
|
+
withRetry(() => fetchWikipediaOpenSearch(query, per), { retries: 1, delayMs: 280, isEmpty: empty }),
|
|
375
|
+
withRetry(() => fetchBingWebRss(query, per), { retries: 1, delayMs: 280, isEmpty: empty }),
|
|
376
|
+
withRetry(() => fetchDuckDuckGoInstantAnswer(query, per), { retries: 1, delayMs: 280, isEmpty: empty }),
|
|
308
377
|
]);
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
378
|
+
let groups = await run();
|
|
379
|
+
if (groups.every((g) => g.length === 0)) {
|
|
380
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
381
|
+
groups = await run();
|
|
382
|
+
}
|
|
383
|
+
return mergeSearchCandidates(groups, Math.max(limit, 8));
|
|
384
|
+
}
|
|
385
|
+
/** DuckDuckGo Instant Answer API — keyless JSON, unrelated to the HTML SERP challenge. */
|
|
386
|
+
export async function fetchDuckDuckGoInstantAnswer(query, limit) {
|
|
387
|
+
try {
|
|
388
|
+
const url = `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json&no_redirect=1&no_html=1&skip_disambig=1`;
|
|
389
|
+
const res = await fetch(url, {
|
|
390
|
+
headers: {
|
|
391
|
+
Accept: "application/json",
|
|
392
|
+
"User-Agent": "WebCrawl/1.4 (https://webcrawl.app; search@webcrawl.app)",
|
|
393
|
+
},
|
|
394
|
+
signal: AbortSignal.timeout(2500),
|
|
395
|
+
});
|
|
396
|
+
if (!res.ok)
|
|
397
|
+
return [];
|
|
398
|
+
const json = (await res.json());
|
|
399
|
+
const items = [];
|
|
400
|
+
const push = (title, href, description) => {
|
|
401
|
+
if (!href.startsWith("http") || items.length >= limit)
|
|
402
|
+
return;
|
|
403
|
+
if (items.some((i) => canonicalSearchUrl(i.url) === canonicalSearchUrl(href)))
|
|
404
|
+
return;
|
|
405
|
+
items.push({
|
|
406
|
+
title: title || href,
|
|
407
|
+
url: href,
|
|
408
|
+
description: description || title,
|
|
409
|
+
engine: "ddg-lite",
|
|
410
|
+
engineRank: items.length + 1,
|
|
411
|
+
});
|
|
412
|
+
};
|
|
413
|
+
if (json.AbstractURL && (json.Heading || json.AbstractText)) {
|
|
414
|
+
push(json.Heading || json.AbstractSource || json.AbstractURL, json.AbstractURL, json.AbstractText || "");
|
|
415
|
+
}
|
|
416
|
+
for (const row of json.Results ?? []) {
|
|
417
|
+
if (row.FirstURL && row.Text)
|
|
418
|
+
push(row.Text, row.FirstURL, row.Text);
|
|
419
|
+
}
|
|
420
|
+
const walk = (node) => {
|
|
421
|
+
if (items.length >= limit)
|
|
422
|
+
return;
|
|
423
|
+
if (Array.isArray(node)) {
|
|
424
|
+
for (const n of node)
|
|
425
|
+
walk(n);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (!node || typeof node !== "object")
|
|
429
|
+
return;
|
|
430
|
+
const rec = node;
|
|
431
|
+
if (rec.FirstURL && rec.Text)
|
|
432
|
+
push(rec.Text, rec.FirstURL, rec.Text);
|
|
433
|
+
if (rec.Topics)
|
|
434
|
+
walk(rec.Topics);
|
|
435
|
+
};
|
|
436
|
+
walk(json.RelatedTopics);
|
|
437
|
+
return items;
|
|
438
|
+
}
|
|
439
|
+
catch {
|
|
440
|
+
return [];
|
|
318
441
|
}
|
|
319
|
-
return out;
|
|
320
442
|
}
|
|
321
443
|
export async function fetchJinaSearch(query, limit, req) {
|
|
322
444
|
const key = process.env.JINA_API_KEY;
|
|
@@ -639,17 +761,33 @@ async function fetchDuckDuckGoLiteOnly(query, limit, userLocation) {
|
|
|
639
761
|
const countryCode = (userLocation?.country || "").toUpperCase();
|
|
640
762
|
const kl = ddgRegion(countryCode, undefined);
|
|
641
763
|
const klParam = kl ? `&kl=${encodeURIComponent(kl)}` : "";
|
|
642
|
-
const
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
|
|
764
|
+
const endpoints = [
|
|
765
|
+
`https://lite.duckduckgo.com/lite/?q=${encodeURIComponent(query)}${klParam}`,
|
|
766
|
+
`https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}${klParam}`,
|
|
767
|
+
];
|
|
768
|
+
for (const endpoint of endpoints) {
|
|
769
|
+
if (items.length >= limit)
|
|
770
|
+
break;
|
|
771
|
+
try {
|
|
772
|
+
const liteRes = await fetchWithTlsImpersonation(endpoint, 2200);
|
|
773
|
+
if (!liteRes?.html || isSerpChallengeHtml(liteRes.html, liteRes.status)) {
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
const html = liteRes.html;
|
|
777
|
+
const linkMatches = Array.from(html.matchAll(/<a\s+[^>]*href=["']([^"']+)["'][^>]*class=["']result-link["'][^>]*>([\s\S]*?)<\/a>|<a\s+[^>]*class=["']result-link["'][^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi));
|
|
778
|
+
const snippetMatches = Array.from(html.matchAll(/<td\s+[^>]*class=["']result-snippet["'][^>]*>([\s\S]*?)<\/td>/gi));
|
|
779
|
+
for (let i = 0; i < linkMatches.length && items.length < limit; i++) {
|
|
780
|
+
const m = linkMatches[i];
|
|
781
|
+
const rawUrl = m[1] || m[3] || "";
|
|
782
|
+
const rawTitle = m[2] || m[4] || "";
|
|
783
|
+
const rawSnippet = snippetMatches[i]?.[1] || "";
|
|
784
|
+
addResult(rawTitle, rawUrl, rawSnippet);
|
|
785
|
+
}
|
|
786
|
+
if (items.length > 0)
|
|
787
|
+
break;
|
|
788
|
+
}
|
|
789
|
+
catch {
|
|
790
|
+
// try the next DDG HTML endpoint
|
|
653
791
|
}
|
|
654
792
|
}
|
|
655
793
|
}
|
|
@@ -724,7 +862,7 @@ async function fetchWikipediaOpenSearch(query, limit) {
|
|
|
724
862
|
"User-Agent": "WebCrawl/1.0 (https://webcrawl.app; search@webcrawl.app)",
|
|
725
863
|
Accept: "application/json",
|
|
726
864
|
},
|
|
727
|
-
signal: AbortSignal.timeout(
|
|
865
|
+
signal: AbortSignal.timeout(2200),
|
|
728
866
|
});
|
|
729
867
|
if (!res.ok) {
|
|
730
868
|
return [];
|
|
@@ -761,7 +899,7 @@ async function fetchHackerNewsAlgolia(query, limit) {
|
|
|
761
899
|
const url = `https://hn.algolia.com/api/v1/search?query=${encodeURIComponent(cleanQuery)}&tags=story&hitsPerPage=${limit}`;
|
|
762
900
|
const res = await fetch(url, {
|
|
763
901
|
headers: { Accept: "application/json" },
|
|
764
|
-
signal: AbortSignal.timeout(
|
|
902
|
+
signal: AbortSignal.timeout(2200),
|
|
765
903
|
});
|
|
766
904
|
if (!res.ok) {
|
|
767
905
|
return [];
|
|
@@ -62,7 +62,7 @@ export const pdfOfficeTargetPlugin = {
|
|
|
62
62
|
name: "PDFs & Office Documents (DOCX, XLSX, PPTX)",
|
|
63
63
|
category: "documents",
|
|
64
64
|
returns: "Page-numbered Markdown, tables, native text extraction, automatic OCR on scanned pages, and skipped-page diagnostics",
|
|
65
|
-
requirements: "Built-in
|
|
65
|
+
requirements: "Built-in PDF inspector, mammoth, and officeparser; optional OCR provider for scanned pages",
|
|
66
66
|
examples: [
|
|
67
67
|
"npx webcrawl scrape https://arxiv.org/pdf/1706.03762.pdf",
|
|
68
68
|
],
|
package/dist/server.js
CHANGED
|
@@ -164,6 +164,9 @@ export function createWebcrawlMcpServer(client, versionOrOpts = "0.1.0") {
|
|
|
164
164
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
165
165
|
}, ({ url, prompt, schema }) => run(async () => {
|
|
166
166
|
const res = await client.extract({ url, prompt, schema });
|
|
167
|
+
if (res.success === false && typeof res.error === "string") {
|
|
168
|
+
throw new Error(res.error + formatCredits(res));
|
|
169
|
+
}
|
|
167
170
|
return JSON.stringify(res.data ?? res, null, 2) + formatCredits(res);
|
|
168
171
|
}));
|
|
169
172
|
server.registerTool("web_rerank", {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webcrawl/mcp",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.5",
|
|
4
4
|
"mcpName": "io.github.brainfish-ai/webcrawl",
|
|
5
5
|
"description": "MCP server for Webcrawl Anything: search, scrape, map, crawl, parse and monitor the web from your agent. Runs locally with no API key, or on webcrawl.app cloud.",
|
|
6
6
|
"type": "module",
|
|
@@ -56,8 +56,8 @@
|
|
|
56
56
|
"@firecrawl/pdf-inspector": "^1.25.2",
|
|
57
57
|
"@modelcontextprotocol/sdk": "^1.32.2",
|
|
58
58
|
"@vercel/sandbox": "^1.0.0",
|
|
59
|
-
"@webcrawl/client": "^1.4.
|
|
60
|
-
"@webcrawl/curl-impersonate": "^1.4.
|
|
59
|
+
"@webcrawl/client": "^1.4.5",
|
|
60
|
+
"@webcrawl/curl-impersonate": "^1.4.5",
|
|
61
61
|
"ai": "^7.0.150",
|
|
62
62
|
"impit": "^0.14.7",
|
|
63
63
|
"mammoth": "^1.13.0",
|