mcp-scraper 0.3.24 → 0.3.25

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.
Files changed (65) hide show
  1. package/dist/bin/api-server.cjs +8140 -5941
  2. package/dist/bin/api-server.cjs.map +1 -1
  3. package/dist/bin/api-server.js +3 -3
  4. package/dist/bin/mcp-scraper-cli.cjs +320 -58
  5. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  6. package/dist/bin/mcp-scraper-cli.js +4 -3
  7. package/dist/bin/mcp-scraper-cli.js.map +1 -1
  8. package/dist/bin/mcp-scraper-install.cjs +3 -3
  9. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  10. package/dist/bin/mcp-scraper-install.js +2 -2
  11. package/dist/bin/mcp-stdio-server.cjs +121 -24
  12. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  13. package/dist/bin/mcp-stdio-server.js +5 -5
  14. package/dist/bin/paa-harvest.cjs +284 -10
  15. package/dist/bin/paa-harvest.cjs.map +1 -1
  16. package/dist/bin/paa-harvest.js +5 -3
  17. package/dist/bin/paa-harvest.js.map +1 -1
  18. package/dist/{chunk-ZAP7MYVO.js → chunk-3PRO376E.js} +2 -3
  19. package/dist/chunk-3PRO376E.js.map +1 -0
  20. package/dist/{chunk-LFATOGDF.js → chunk-BP27CZ5Q.js} +4 -3
  21. package/dist/chunk-BP27CZ5Q.js.map +1 -0
  22. package/dist/{chunk-DE7WP4UU.js → chunk-DOBQN3EY.js} +3 -3
  23. package/dist/chunk-DOBQN3EY.js.map +1 -0
  24. package/dist/{chunk-GZ4W72WF.js → chunk-FE2WC4JR.js} +119 -23
  25. package/dist/chunk-FE2WC4JR.js.map +1 -0
  26. package/dist/{chunk-IGOEAKC7.js → chunk-G7PQ64LM.js} +274 -9
  27. package/dist/chunk-G7PQ64LM.js.map +1 -0
  28. package/dist/chunk-K443GQY5.js +24 -0
  29. package/dist/chunk-K443GQY5.js.map +1 -0
  30. package/dist/{chunk-H2R232HK.js → chunk-NNEIXK5L.js} +307 -62
  31. package/dist/chunk-NNEIXK5L.js.map +1 -0
  32. package/dist/{chunk-3KYRG7O7.js → chunk-TRNSXR46.js} +185 -2
  33. package/dist/chunk-TRNSXR46.js.map +1 -0
  34. package/dist/chunk-WPHAMNT7.js +7 -0
  35. package/dist/chunk-WPHAMNT7.js.map +1 -0
  36. package/dist/{chunk-JTWQGPKF.js → chunk-WYCER2HW.js} +33 -10
  37. package/dist/chunk-WYCER2HW.js.map +1 -0
  38. package/dist/{db-EG5ETPTY.js → db-FP2ABUU4.js} +32 -2
  39. package/dist/index.cjs +284 -10
  40. package/dist/index.cjs.map +1 -1
  41. package/dist/index.d.cts +7 -3
  42. package/dist/index.d.ts +7 -3
  43. package/dist/index.js +5 -3
  44. package/dist/index.js.map +1 -1
  45. package/dist/{server-2SRARAO5.js → server-RRCKVBHG.js} +1563 -265
  46. package/dist/server-RRCKVBHG.js.map +1 -0
  47. package/dist/{worker-UM5UM6O5.js → worker-UC6D2756.js} +7 -6
  48. package/dist/{worker-UM5UM6O5.js.map → worker-UC6D2756.js.map} +1 -1
  49. package/docs/kernel-proxy-future-enhancements.md +80 -0
  50. package/docs/spec-browser-routing.md +108 -0
  51. package/docs/spec-mcp-tools.md +146 -0
  52. package/docs/specs/audit-visual-demo.md +309 -0
  53. package/package.json +5 -2
  54. package/dist/chunk-3KYRG7O7.js.map +0 -1
  55. package/dist/chunk-DE7WP4UU.js.map +0 -1
  56. package/dist/chunk-GZ4W72WF.js.map +0 -1
  57. package/dist/chunk-H2R232HK.js.map +0 -1
  58. package/dist/chunk-H7R4PPMG.js +0 -7
  59. package/dist/chunk-H7R4PPMG.js.map +0 -1
  60. package/dist/chunk-IGOEAKC7.js.map +0 -1
  61. package/dist/chunk-JTWQGPKF.js.map +0 -1
  62. package/dist/chunk-LFATOGDF.js.map +0 -1
  63. package/dist/chunk-ZAP7MYVO.js.map +0 -1
  64. package/dist/server-2SRARAO5.js.map +0 -1
  65. /package/dist/{db-EG5ETPTY.js.map → db-FP2ABUU4.js.map} +0 -0
@@ -5,18 +5,18 @@ import {
5
5
  exportFanout,
6
6
  registerPaaExtractorMcpTools,
7
7
  sanitizeOutboundDiagnostics
8
- } from "../chunk-GZ4W72WF.js";
8
+ } from "../chunk-FE2WC4JR.js";
9
9
  import {
10
10
  renderInstallTerminal
11
- } from "../chunk-DE7WP4UU.js";
11
+ } from "../chunk-DOBQN3EY.js";
12
12
  import {
13
13
  PACKAGE_VERSION
14
- } from "../chunk-H7R4PPMG.js";
14
+ } from "../chunk-WPHAMNT7.js";
15
15
  import {
16
16
  browserServiceProfileName,
17
17
  browserServiceProfileSaveChanges
18
- } from "../chunk-ZAP7MYVO.js";
19
- import "../chunk-LFATOGDF.js";
18
+ } from "../chunk-3PRO376E.js";
19
+ import "../chunk-BP27CZ5Q.js";
20
20
 
21
21
  // bin/mcp-stdio-server.ts
22
22
  import { readFileSync } from "fs";
@@ -32,13 +32,12 @@ function browserServiceApiKey() {
32
32
  return value || void 0;
33
33
  }
34
34
  function browserServiceProxyId() {
35
- const value = (process.env.BROWSER_SERVICE_PROXY_ID ?? process.env.KERNEL_PROXY_ID)?.trim();
36
- return value || void 0;
35
+ return void 0;
37
36
  }
38
37
 
39
38
  // src/schemas.ts
40
39
  var import_zod = require("zod");
41
- var DEFAULT_PROXY_MODE = "configured";
40
+ var DEFAULT_PROXY_MODE = "none";
42
41
  var DEFAULT_MAPS_PROXY_MODE = "location";
43
42
  var HarvestOptionsSchema = import_zod.z.object({
44
43
  query: import_zod.z.string().min(1),
@@ -60,7 +59,8 @@ var HarvestOptionsSchema = import_zod.z.object({
60
59
  outputDir: import_zod.z.string().default("./paa-output"),
61
60
  format: import_zod.z.enum(["json", "csv", "both"]).default("both"),
62
61
  serpOnly: import_zod.z.boolean().default(false),
63
- pages: import_zod.z.number().int().min(1).max(2).default(1)
62
+ pages: import_zod.z.number().int().min(1).max(2).default(1),
63
+ softDeadlineMs: import_zod.z.number().optional()
64
64
  });
65
65
  var MapsPlaceOptionsSchema = import_zod.z.object({
66
66
  businessName: import_zod.z.string().min(1),
@@ -257,6 +257,192 @@ var LocationMismatchError = class extends Error {
257
257
  }
258
258
  };
259
259
 
260
+ // src/api/cost-telemetry.ts
261
+ var import_node_crypto2 = require("crypto");
262
+
263
+ // src/api/db.ts
264
+ var import_http = require("@libsql/client/http");
265
+ var import_node_crypto = require("crypto");
266
+ var import_zod2 = require("zod");
267
+ var DB_URL = process.env.TURSO_DATABASE_URL ?? "file:./paa-api.db";
268
+ var DB_TOKEN = process.env.TURSO_AUTH_TOKEN;
269
+ var _db = null;
270
+ function getDb() {
271
+ if (!_db) _db = (0, import_http.createClient)({ url: DB_URL, authToken: DB_TOKEN });
272
+ return _db;
273
+ }
274
+ var SiteAuditJobRowSchema = import_zod2.z.object({
275
+ id: import_zod2.z.string(),
276
+ user_id: import_zod2.z.number(),
277
+ status: import_zod2.z.string().default("pending"),
278
+ client_domain: import_zod2.z.string(),
279
+ session_path: import_zod2.z.string(),
280
+ request: import_zod2.z.string(),
281
+ result: import_zod2.z.string().nullable(),
282
+ error: import_zod2.z.string().nullable(),
283
+ created_at: import_zod2.z.string(),
284
+ updated_at: import_zod2.z.string()
285
+ });
286
+ var SiteAuditPhaseLogRowSchema = import_zod2.z.object({
287
+ id: import_zod2.z.number(),
288
+ job_id: import_zod2.z.string(),
289
+ phase: import_zod2.z.string(),
290
+ completed_at: import_zod2.z.string(),
291
+ output_summary: import_zod2.z.string()
292
+ });
293
+
294
+ // src/api/cost-context.ts
295
+ var import_node_async_hooks = require("async_hooks");
296
+ var storage = new import_node_async_hooks.AsyncLocalStorage();
297
+ function runWithCostContext(ctx, fn) {
298
+ return storage.run(ctx, fn);
299
+ }
300
+ function currentCostContext() {
301
+ return storage.getStore();
302
+ }
303
+
304
+ // src/api/cost-rates.ts
305
+ var KERNEL_HEADLESS_USD_PER_SEC = 166667e-10;
306
+ var KERNEL_HEADFUL_USD_PER_SEC = 1333336e-10;
307
+ var HEADLESS_OPS = /* @__PURE__ */ new Set(["harvest", "maps_search", "yt_channel", "fb_search", "fb_ad", "instagram"]);
308
+ function kernelCostUsd(ms, headful) {
309
+ const sec = Math.max(0, ms) / 1e3;
310
+ return sec * (headful ? KERNEL_HEADFUL_USD_PER_SEC : KERNEL_HEADLESS_USD_PER_SEC);
311
+ }
312
+
313
+ // src/api/cost-telemetry.ts
314
+ var _ready = false;
315
+ async function migrateCostTelemetry() {
316
+ if (_ready) return;
317
+ const db = getDb();
318
+ await db.execute(`
319
+ CREATE TABLE IF NOT EXISTS kernel_session_log (
320
+ id TEXT PRIMARY KEY,
321
+ kernel_session_id TEXT,
322
+ op TEXT,
323
+ source TEXT NOT NULL,
324
+ probe_run_id TEXT,
325
+ user_id INTEGER,
326
+ stealth INTEGER,
327
+ headless_sent INTEGER,
328
+ proxy_used INTEGER,
329
+ fallback INTEGER NOT NULL DEFAULT 0,
330
+ opened_at TEXT,
331
+ closed_at TEXT,
332
+ duration_ms INTEGER NOT NULL DEFAULT 0,
333
+ est_cost_usd_headless REAL NOT NULL DEFAULT 0,
334
+ est_cost_usd_headful REAL NOT NULL DEFAULT 0,
335
+ error TEXT,
336
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
337
+ )
338
+ `);
339
+ await db.execute(`CREATE INDEX IF NOT EXISTS kernel_session_log_op ON kernel_session_log(op)`);
340
+ await db.execute(`CREATE INDEX IF NOT EXISTS kernel_session_log_probe ON kernel_session_log(probe_run_id)`);
341
+ await db.execute(`CREATE INDEX IF NOT EXISTS kernel_session_log_created ON kernel_session_log(created_at)`);
342
+ try {
343
+ await db.execute(`ALTER TABLE kernel_session_log ADD COLUMN proxy_source TEXT`);
344
+ } catch {
345
+ }
346
+ try {
347
+ await db.execute(`ALTER TABLE kernel_session_log ADD COLUMN proxy_type TEXT`);
348
+ } catch {
349
+ }
350
+ await db.execute(`
351
+ CREATE TABLE IF NOT EXISTS vendor_usage_log (
352
+ id TEXT PRIMARY KEY,
353
+ op TEXT,
354
+ probe_run_id TEXT,
355
+ user_id INTEGER,
356
+ vendor TEXT NOT NULL,
357
+ model TEXT,
358
+ units REAL NOT NULL DEFAULT 0,
359
+ unit_type TEXT,
360
+ est_cost_usd REAL NOT NULL DEFAULT 0,
361
+ error TEXT,
362
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
363
+ )
364
+ `);
365
+ await db.execute(`CREATE INDEX IF NOT EXISTS vendor_usage_log_op ON vendor_usage_log(op)`);
366
+ await db.execute(`CREATE INDEX IF NOT EXISTS vendor_usage_log_probe ON vendor_usage_log(probe_run_id)`);
367
+ await db.execute(`CREATE INDEX IF NOT EXISTS vendor_usage_log_created ON vendor_usage_log(created_at)`);
368
+ await db.execute(`
369
+ CREATE TABLE IF NOT EXISTS cost_probe_runs (
370
+ id TEXT PRIMARY KEY,
371
+ tool TEXT NOT NULL,
372
+ target TEXT,
373
+ started_at TEXT NOT NULL DEFAULT (datetime('now')),
374
+ wall_ms INTEGER NOT NULL DEFAULT 0,
375
+ success INTEGER NOT NULL DEFAULT 0,
376
+ error TEXT,
377
+ http_only INTEGER NOT NULL DEFAULT 0,
378
+ kernel_used INTEGER NOT NULL DEFAULT 0,
379
+ kernel_sessions INTEGER NOT NULL DEFAULT 0,
380
+ kernel_seconds_total REAL NOT NULL DEFAULT 0,
381
+ kernel_tier_observed TEXT,
382
+ est_kernel_cost_usd_headless REAL NOT NULL DEFAULT 0,
383
+ est_kernel_cost_usd_headful REAL NOT NULL DEFAULT 0,
384
+ vendor_cost_usd REAL NOT NULL DEFAULT 0,
385
+ charged_credits REAL NOT NULL DEFAULT 0,
386
+ margin_usd_headless_starter REAL NOT NULL DEFAULT 0,
387
+ margin_usd_headful_starter REAL NOT NULL DEFAULT 0,
388
+ notes TEXT
389
+ )
390
+ `);
391
+ await db.execute(`CREATE INDEX IF NOT EXISTS cost_probe_runs_tool ON cost_probe_runs(tool)`);
392
+ try {
393
+ await db.execute(`ALTER TABLE cost_probe_runs ADD COLUMN units REAL`);
394
+ } catch {
395
+ }
396
+ try {
397
+ await db.execute(`ALTER TABLE cost_probe_runs ADD COLUMN unit_type TEXT`);
398
+ } catch {
399
+ }
400
+ try {
401
+ await db.execute(`ALTER TABLE cost_probe_runs ADD COLUMN mode TEXT`);
402
+ } catch {
403
+ }
404
+ _ready = true;
405
+ }
406
+ async function recordKernelSession(r) {
407
+ try {
408
+ await migrateCostTelemetry();
409
+ const ctx = currentCostContext();
410
+ const durationMs = Math.max(0, r.closedAtMs - r.openedAtMs);
411
+ const db = getDb();
412
+ await db.execute({
413
+ sql: `INSERT INTO kernel_session_log
414
+ (id, kernel_session_id, op, source, probe_run_id, user_id, stealth, headless_sent, proxy_used, proxy_source, proxy_type, fallback, opened_at, closed_at, duration_ms, est_cost_usd_headless, est_cost_usd_headful, error)
415
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
416
+ args: [
417
+ (0, import_node_crypto2.randomUUID)(),
418
+ r.kernelSessionId ?? null,
419
+ ctx?.op ?? null,
420
+ r.source,
421
+ ctx?.probeRunId ?? null,
422
+ ctx?.userId ?? null,
423
+ boolToInt(r.stealth),
424
+ boolToInt(r.headlessSent),
425
+ boolToInt(r.proxyUsed),
426
+ r.proxySource ?? null,
427
+ r.proxyType ?? null,
428
+ r.fallback ? 1 : 0,
429
+ new Date(r.openedAtMs).toISOString(),
430
+ new Date(r.closedAtMs).toISOString(),
431
+ durationMs,
432
+ kernelCostUsd(durationMs, false),
433
+ kernelCostUsd(durationMs, true),
434
+ r.error ?? null
435
+ ]
436
+ });
437
+ } catch (err) {
438
+ console.warn("[cost-telemetry] recordKernelSession failed:", err instanceof Error ? err.message : String(err));
439
+ }
440
+ }
441
+ function boolToInt(v) {
442
+ if (v === null || v === void 0) return null;
443
+ return v ? 1 : 0;
444
+ }
445
+
260
446
  // src/driver/BrowserDriver.ts
261
447
  import_playwright_extra.chromium.use((0, import_puppeteer_extra_plugin_stealth.default)());
262
448
  var DESKTOP_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
@@ -344,6 +530,12 @@ var BrowserDriver = class {
344
530
  page = null;
345
531
  kernelClient = null;
346
532
  kernelSessionId = null;
533
+ kernelOpenedAtMs = null;
534
+ kernelStealth = null;
535
+ kernelProxyUsed = null;
536
+ kernelProxySource = null;
537
+ kernelProxyType = null;
538
+ kernelHeadlessSent = null;
347
539
  debugEnabled = false;
348
540
  debugSnapshot = {
349
541
  kernel: null,
@@ -375,9 +567,16 @@ var BrowserDriver = class {
375
567
  if (config.kernelProfileName && config.kernelProfileSaveChanges === true) {
376
568
  await ensureKernelProfile(this.kernelClient, config.kernelProfileName);
377
569
  }
570
+ const costCtx = currentCostContext();
571
+ let useHeadless;
572
+ if (config.headlessMode === "headful" || costCtx?.forceHeadful === true) useHeadless = false;
573
+ else if (config.headlessMode === "headless") useHeadless = true;
574
+ else useHeadless = costCtx?.forceHeadless === true || process.env.KERNEL_HEADLESS_DISABLED !== "true" && !!costCtx?.op && HEADLESS_OPS.has(costCtx.op);
575
+ this.kernelHeadlessSent = useHeadless ? true : null;
378
576
  const kernelBrowser = await this.kernelClient.browsers.create({
379
577
  stealth: true,
380
578
  timeout_seconds: timeoutSeconds,
579
+ ...useHeadless ? { headless: true } : {},
381
580
  ...config.kernelProxyId ? { proxy_id: config.kernelProxyId } : {},
382
581
  ...config.kernelProfileName ? {
383
582
  profile: {
@@ -387,9 +586,14 @@ var BrowserDriver = class {
387
586
  } : {}
388
587
  });
389
588
  this.kernelSessionId = kernelBrowser.session_id;
589
+ this.kernelOpenedAtMs = Date.now();
590
+ this.kernelStealth = typeof kernelBrowser.stealth === "boolean" ? kernelBrowser.stealth : null;
591
+ this.kernelProxyUsed = typeof kernelBrowser.proxy_id === "string" ? Boolean(kernelBrowser.proxy_id) : Boolean(config.kernelProxyId);
592
+ this.kernelProxySource = config.kernelProxyResolution?.source ?? null;
593
+ this.kernelProxyType = config.kernelProxyResolution?.proxyType ?? null;
390
594
  let defaultProxyDisabled = null;
391
595
  let defaultProxyDisableError = null;
392
- if (proxyMode === "none") {
596
+ if (!config.kernelProxyId) {
393
597
  try {
394
598
  await withTimeout(
395
599
  this.kernelClient.browsers.update(this.kernelSessionId, { disable_default_proxy: true }),
@@ -740,11 +944,24 @@ var BrowserDriver = class {
740
944
  const b = this.browser;
741
945
  const sessionId = this.kernelSessionId;
742
946
  const client = this.kernelClient;
947
+ const openedAtMs = this.kernelOpenedAtMs;
948
+ const stealth = this.kernelStealth;
949
+ const proxyUsed = this.kernelProxyUsed;
950
+ const proxySource = this.kernelProxySource;
951
+ const proxyType = this.kernelProxyType;
952
+ const headlessSent = this.kernelHeadlessSent;
743
953
  this.browser = null;
744
954
  this.context = null;
745
955
  this.page = null;
746
956
  this.kernelSessionId = null;
747
957
  this.kernelClient = null;
958
+ this.kernelOpenedAtMs = null;
959
+ this.kernelHeadlessSent = null;
960
+ this.kernelProxySource = null;
961
+ this.kernelProxyType = null;
962
+ if (client && sessionId && openedAtMs != null) {
963
+ void recordKernelSession({ kernelSessionId: sessionId, source: "browser_driver", stealth, headlessSent, proxyUsed, proxySource, proxyType, openedAtMs, closedAtMs: Date.now() });
964
+ }
748
965
  if (client && sessionId) {
749
966
  console.info(JSON.stringify({
750
967
  event: "kernel_browser_delete_started",
@@ -1459,6 +1676,7 @@ var PAAExtractor = class {
1459
1676
  );
1460
1677
  let round = 0;
1461
1678
  while (seenQs.size < options.maxQuestions) {
1679
+ if (options.softDeadlineMs && Date.now() >= options.softDeadlineMs) break;
1462
1680
  this.throwIfAborted(signal);
1463
1681
  await this.throwIfCaptcha(page, "Google PAA expansion");
1464
1682
  const beforeQs = await readAllQs();
@@ -1468,6 +1686,7 @@ var PAAExtractor = class {
1468
1686
  if (unexpandedCount === 0) break;
1469
1687
  this.reporter.onDepth(++round);
1470
1688
  for (let ci = 0; ci < unexpandedCount; ci++) {
1689
+ if (options.softDeadlineMs && Date.now() >= options.softDeadlineMs) break;
1471
1690
  this.throwIfAborted(signal);
1472
1691
  try {
1473
1692
  const btn = page.locator(unexpandedSel).first();
@@ -2131,6 +2350,28 @@ var PAAExtractor = class {
2131
2350
  var import_node_fs = require("fs");
2132
2351
  var import_node_path = __toESM(require("path"), 1);
2133
2352
  var import_papaparse = __toESM(require("papaparse"), 1);
2353
+
2354
+ // src/output/memory-library-sink.ts
2355
+ async function postToMemoryLibrary(args) {
2356
+ const url = process.env.MCP_MEMORY_INGEST_URL;
2357
+ const key = process.env.MCP_MEMORY_KEY;
2358
+ if (!url || !key) return;
2359
+ try {
2360
+ const res = await fetch(url, {
2361
+ method: "POST",
2362
+ headers: { "content-type": "application/json" },
2363
+ body: JSON.stringify({ data: { apiKey: key, title: args.title, content: args.content, source: args.source } })
2364
+ });
2365
+ const body = await res.json().catch(() => null);
2366
+ if (!res.ok || body?.result?.error) {
2367
+ console.warn("[memory-library-sink] ingest not accepted:", res.status, body?.result?.message ?? "");
2368
+ }
2369
+ } catch (err) {
2370
+ console.warn("[memory-library-sink] ingest failed:", err?.message);
2371
+ }
2372
+ }
2373
+
2374
+ // src/output/OutputSerializer.ts
2134
2375
  var OutputSerializer = class {
2135
2376
  async writeJSON(result, outputDir) {
2136
2377
  await import_node_fs.promises.mkdir(outputDir, { recursive: true });
@@ -2138,6 +2379,7 @@ var OutputSerializer = class {
2138
2379
  const filename = `${slug}-${Date.now()}.json`;
2139
2380
  const fullPath = import_node_path.default.join(outputDir, filename);
2140
2381
  await import_node_fs.promises.writeFile(fullPath, JSON.stringify(result, null, 2), "utf8");
2382
+ await postToMemoryLibrary({ title: `${result.seed} scrape`, content: JSON.stringify(result), source: `mcp-scraper:${fullPath}` });
2141
2383
  return fullPath;
2142
2384
  }
2143
2385
  async writeCSV(rows, outputDir) {
@@ -2335,11 +2577,13 @@ function proxyIdSuffix2(proxyId) {
2335
2577
  return proxyId ? proxyId.slice(-6) : null;
2336
2578
  }
2337
2579
  function resolution(source, proxyMode, proxyId, target, error, disposable = false) {
2580
+ const proxyType = source === "disabled" ? "none" : source === "configured_fallback" ? "configured" : source === "isp_created" ? "isp" : source === "location_created" || source === "location_reused" ? "residential" : "unknown";
2338
2581
  return {
2339
2582
  kernelProxyId: proxyId,
2340
2583
  ...disposable && proxyId ? { disposableProxyId: proxyId } : {},
2341
2584
  resolution: {
2342
2585
  source,
2586
+ proxyType,
2343
2587
  proxyMode,
2344
2588
  proxyIdPresent: Boolean(proxyId),
2345
2589
  proxyIdSuffix: proxyIdSuffix2(proxyId),
@@ -2534,6 +2778,22 @@ async function createFreshLocationProxy(kernel, options, target) {
2534
2778
  createErrors.push(`${candidate.proxyName}: ${errorText2(err)}`);
2535
2779
  }
2536
2780
  }
2781
+ if (process.env.KERNEL_PROXY_TYPE !== "residential") {
2782
+ const ispTarget = stateTarget(target);
2783
+ try {
2784
+ const created = await kernel.proxies.create({
2785
+ type: "isp",
2786
+ name: freshProxyName(`mcp-serp-isp-${ispTarget.country.toLowerCase()}-${(ispTarget.state ?? "").toLowerCase()}`, options.attemptIndex),
2787
+ config: { country: ispTarget.country, ...ispTarget.state ? { state: ispTarget.state } : {} }
2788
+ });
2789
+ if (created.id) {
2790
+ return resolution("isp_created", options.proxyMode, created.id, ispTarget, null, true);
2791
+ }
2792
+ createErrors.push("isp: Kernel did not return a proxy id");
2793
+ } catch (err) {
2794
+ createErrors.push(`isp: ${errorText2(err)}`);
2795
+ }
2796
+ }
2537
2797
  return resolution("configured_fallback", options.proxyMode, options.configuredKernelProxyId, target, createErrors.join(" | "));
2538
2798
  }
2539
2799
  async function deleteKernelProxyId(kernelApiKey, proxyId) {
@@ -2817,9 +3077,11 @@ async function harvest(rawOptions) {
2817
3077
  };
2818
3078
  const requestedDebug = typeof raw.debug === "boolean" ? raw.debug : false;
2819
3079
  const needsLocationEvidence = proxyMode === "location" && Boolean(proxyOpts.location);
2820
- const maxAttempts = maxAttemptsForProxyMode(proxyMode);
3080
+ const maxAttempts = maxAttemptsForProxyMode(proxyMode) + 1;
2821
3081
  const serializer = new OutputSerializer();
2822
3082
  let lastError = null;
3083
+ let effectiveProxyMode = proxyMode;
3084
+ let proxyFailures = 0;
2823
3085
  for (let i = 0; i < maxAttempts; i++) {
2824
3086
  const attemptNumber = i + 1;
2825
3087
  const startedAtMs = Date.now();
@@ -2827,18 +3089,19 @@ async function harvest(rawOptions) {
2827
3089
  if (signal?.aborted) throw abortReason(signal);
2828
3090
  const resolution2 = await resolveKernelProxyId({
2829
3091
  ...proxyOpts,
3092
+ proxyMode: effectiveProxyMode,
2830
3093
  attemptIndex: i,
2831
- fresh: proxyMode === "location"
3094
+ fresh: effectiveProxyMode === "location"
2832
3095
  });
2833
3096
  const mergedAttempt = {
2834
3097
  ...raw,
2835
3098
  kernelApiKey,
2836
3099
  kernelProxyId: resolution2.kernelProxyId,
2837
3100
  kernelProxyResolution: resolution2.resolution,
2838
- proxyMode,
3101
+ proxyMode: effectiveProxyMode,
2839
3102
  debug: requestedDebug || needsLocationEvidence
2840
3103
  };
2841
- if (proxyMode === "none") mergedAttempt.kernelProxyId = void 0;
3104
+ if (effectiveProxyMode === "none") mergedAttempt.kernelProxyId = void 0;
2842
3105
  const attemptOptions = HarvestOptionsSchema.parse(mergedAttempt);
2843
3106
  await emitAttemptEvent(onAttemptEvent, {
2844
3107
  type: "started",
@@ -2857,10 +3120,17 @@ async function harvest(rawOptions) {
2857
3120
  location: attemptOptions.location ?? null,
2858
3121
  max_questions: attemptOptions.maxQuestions
2859
3122
  }));
2860
- const attempt = await extractOnce(attemptOptions, signal);
3123
+ const isHeadfulEscalation = i === maxAttempts - 1;
3124
+ const needHeadful = isHeadfulEscalation || !attemptOptions.serpOnly;
3125
+ const baseCtx = currentCostContext();
3126
+ const attempt = needHeadful && baseCtx ? await runWithCostContext({ ...baseCtx, forceHeadful: true }, () => extractOnce(attemptOptions, signal)) : await extractOnce(attemptOptions, signal);
2861
3127
  if (attempt.error) {
2862
3128
  const err = attempt.error;
2863
3129
  const outcome = classifyAttemptError(err);
3130
+ if ((outcome === "captcha" || outcome === "proxy_tunnel_failed") && effectiveProxyMode === "location") {
3131
+ proxyFailures++;
3132
+ if (proxyFailures >= 2) effectiveProxyMode = "none";
3133
+ }
2864
3134
  const willRetry = i < maxAttempts - 1 && (outcome === "captcha" || proxyMode === "location" && retryableLocationProxyError(outcome));
2865
3135
  if (outcome === "captcha") {
2866
3136
  console.warn(JSON.stringify({
@@ -2962,6 +3232,10 @@ async function harvest(rawOptions) {
2962
3232
  return finalResult;
2963
3233
  } catch (err) {
2964
3234
  const outcome = classifyAttemptError(err);
3235
+ if ((outcome === "captcha" || outcome === "proxy_tunnel_failed") && effectiveProxyMode === "location") {
3236
+ proxyFailures++;
3237
+ if (proxyFailures >= 2) effectiveProxyMode = "none";
3238
+ }
2965
3239
  const willRetry = i < maxAttempts - 1 && (outcome === "captcha" || proxyMode === "location" && retryableLocationProxyError(outcome));
2966
3240
  if (outcome === "captcha") {
2967
3241
  console.warn(JSON.stringify({