mcp-scraper 0.3.23 → 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 (66) hide show
  1. package/dist/bin/api-server.cjs +8174 -5965
  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-7KKH7LA5.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-4CPJKKY5.js → chunk-WYCER2HW.js} +43 -20
  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-2IA3HUEC.js → server-RRCKVBHG.js} +1579 -271
  46. package/dist/server-RRCKVBHG.js.map +1 -0
  47. package/dist/{worker-5K7TPBPR.js → worker-UC6D2756.js} +7 -6
  48. package/dist/{worker-5K7TPBPR.js.map → worker-UC6D2756.js.map} +1 -1
  49. package/docs/kernel-proxy-future-enhancements.md +80 -0
  50. package/docs/mcp-tool-manifest.generated.json +1 -1
  51. package/docs/spec-browser-routing.md +108 -0
  52. package/docs/spec-mcp-tools.md +146 -0
  53. package/docs/specs/audit-visual-demo.md +309 -0
  54. package/package.json +5 -2
  55. package/dist/chunk-3KYRG7O7.js.map +0 -1
  56. package/dist/chunk-4CPJKKY5.js.map +0 -1
  57. package/dist/chunk-7KKH7LA5.js.map +0 -1
  58. package/dist/chunk-DE7WP4UU.js.map +0 -1
  59. package/dist/chunk-H2R232HK.js.map +0 -1
  60. package/dist/chunk-IGOEAKC7.js.map +0 -1
  61. package/dist/chunk-LFATOGDF.js.map +0 -1
  62. package/dist/chunk-YSSWPZ2B.js +0 -7
  63. package/dist/chunk-YSSWPZ2B.js.map +0 -1
  64. package/dist/chunk-ZAP7MYVO.js.map +0 -1
  65. package/dist/server-2IA3HUEC.js.map +0 -1
  66. /package/dist/{db-EG5ETPTY.js.map → db-FP2ABUU4.js.map} +0 -0
package/dist/index.cjs CHANGED
@@ -38,7 +38,7 @@ module.exports = __toCommonJS(src_exports);
38
38
 
39
39
  // src/schemas.ts
40
40
  var import_zod = require("zod");
41
- var DEFAULT_PROXY_MODE = "configured";
41
+ var DEFAULT_PROXY_MODE = "none";
42
42
  var DEFAULT_MAPS_PROXY_MODE = "location";
43
43
  var HarvestOptionsSchema = import_zod.z.object({
44
44
  query: import_zod.z.string().min(1),
@@ -60,7 +60,8 @@ var HarvestOptionsSchema = import_zod.z.object({
60
60
  outputDir: import_zod.z.string().default("./paa-output"),
61
61
  format: import_zod.z.enum(["json", "csv", "both"]).default("both"),
62
62
  serpOnly: import_zod.z.boolean().default(false),
63
- pages: import_zod.z.number().int().min(1).max(2).default(1)
63
+ pages: import_zod.z.number().int().min(1).max(2).default(1),
64
+ softDeadlineMs: import_zod.z.number().optional()
64
65
  });
65
66
  var MapsPlaceOptionsSchema = import_zod.z.object({
66
67
  businessName: import_zod.z.string().min(1),
@@ -140,8 +141,7 @@ function browserServiceApiKey() {
140
141
  return value || void 0;
141
142
  }
142
143
  function browserServiceProxyId() {
143
- const value = (process.env.BROWSER_SERVICE_PROXY_ID ?? process.env.KERNEL_PROXY_ID)?.trim();
144
- return value || void 0;
144
+ return void 0;
145
145
  }
146
146
 
147
147
  // src/driver/BrowserDriver.ts
@@ -267,6 +267,192 @@ var LocationMismatchError = class extends Error {
267
267
  }
268
268
  };
269
269
 
270
+ // src/api/cost-telemetry.ts
271
+ var import_node_crypto2 = require("crypto");
272
+
273
+ // src/api/db.ts
274
+ var import_http = require("@libsql/client/http");
275
+ var import_node_crypto = require("crypto");
276
+ var import_zod2 = require("zod");
277
+ var DB_URL = process.env.TURSO_DATABASE_URL ?? "file:./paa-api.db";
278
+ var DB_TOKEN = process.env.TURSO_AUTH_TOKEN;
279
+ var _db = null;
280
+ function getDb() {
281
+ if (!_db) _db = (0, import_http.createClient)({ url: DB_URL, authToken: DB_TOKEN });
282
+ return _db;
283
+ }
284
+ var SiteAuditJobRowSchema = import_zod2.z.object({
285
+ id: import_zod2.z.string(),
286
+ user_id: import_zod2.z.number(),
287
+ status: import_zod2.z.string().default("pending"),
288
+ client_domain: import_zod2.z.string(),
289
+ session_path: import_zod2.z.string(),
290
+ request: import_zod2.z.string(),
291
+ result: import_zod2.z.string().nullable(),
292
+ error: import_zod2.z.string().nullable(),
293
+ created_at: import_zod2.z.string(),
294
+ updated_at: import_zod2.z.string()
295
+ });
296
+ var SiteAuditPhaseLogRowSchema = import_zod2.z.object({
297
+ id: import_zod2.z.number(),
298
+ job_id: import_zod2.z.string(),
299
+ phase: import_zod2.z.string(),
300
+ completed_at: import_zod2.z.string(),
301
+ output_summary: import_zod2.z.string()
302
+ });
303
+
304
+ // src/api/cost-context.ts
305
+ var import_node_async_hooks = require("async_hooks");
306
+ var storage = new import_node_async_hooks.AsyncLocalStorage();
307
+ function runWithCostContext(ctx, fn) {
308
+ return storage.run(ctx, fn);
309
+ }
310
+ function currentCostContext() {
311
+ return storage.getStore();
312
+ }
313
+
314
+ // src/api/cost-rates.ts
315
+ var KERNEL_HEADLESS_USD_PER_SEC = 166667e-10;
316
+ var KERNEL_HEADFUL_USD_PER_SEC = 1333336e-10;
317
+ var HEADLESS_OPS = /* @__PURE__ */ new Set(["harvest", "maps_search", "yt_channel", "fb_search", "fb_ad", "instagram"]);
318
+ function kernelCostUsd(ms, headful) {
319
+ const sec = Math.max(0, ms) / 1e3;
320
+ return sec * (headful ? KERNEL_HEADFUL_USD_PER_SEC : KERNEL_HEADLESS_USD_PER_SEC);
321
+ }
322
+
323
+ // src/api/cost-telemetry.ts
324
+ var _ready = false;
325
+ async function migrateCostTelemetry() {
326
+ if (_ready) return;
327
+ const db = getDb();
328
+ await db.execute(`
329
+ CREATE TABLE IF NOT EXISTS kernel_session_log (
330
+ id TEXT PRIMARY KEY,
331
+ kernel_session_id TEXT,
332
+ op TEXT,
333
+ source TEXT NOT NULL,
334
+ probe_run_id TEXT,
335
+ user_id INTEGER,
336
+ stealth INTEGER,
337
+ headless_sent INTEGER,
338
+ proxy_used INTEGER,
339
+ fallback INTEGER NOT NULL DEFAULT 0,
340
+ opened_at TEXT,
341
+ closed_at TEXT,
342
+ duration_ms INTEGER NOT NULL DEFAULT 0,
343
+ est_cost_usd_headless REAL NOT NULL DEFAULT 0,
344
+ est_cost_usd_headful REAL NOT NULL DEFAULT 0,
345
+ error TEXT,
346
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
347
+ )
348
+ `);
349
+ await db.execute(`CREATE INDEX IF NOT EXISTS kernel_session_log_op ON kernel_session_log(op)`);
350
+ await db.execute(`CREATE INDEX IF NOT EXISTS kernel_session_log_probe ON kernel_session_log(probe_run_id)`);
351
+ await db.execute(`CREATE INDEX IF NOT EXISTS kernel_session_log_created ON kernel_session_log(created_at)`);
352
+ try {
353
+ await db.execute(`ALTER TABLE kernel_session_log ADD COLUMN proxy_source TEXT`);
354
+ } catch {
355
+ }
356
+ try {
357
+ await db.execute(`ALTER TABLE kernel_session_log ADD COLUMN proxy_type TEXT`);
358
+ } catch {
359
+ }
360
+ await db.execute(`
361
+ CREATE TABLE IF NOT EXISTS vendor_usage_log (
362
+ id TEXT PRIMARY KEY,
363
+ op TEXT,
364
+ probe_run_id TEXT,
365
+ user_id INTEGER,
366
+ vendor TEXT NOT NULL,
367
+ model TEXT,
368
+ units REAL NOT NULL DEFAULT 0,
369
+ unit_type TEXT,
370
+ est_cost_usd REAL NOT NULL DEFAULT 0,
371
+ error TEXT,
372
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
373
+ )
374
+ `);
375
+ await db.execute(`CREATE INDEX IF NOT EXISTS vendor_usage_log_op ON vendor_usage_log(op)`);
376
+ await db.execute(`CREATE INDEX IF NOT EXISTS vendor_usage_log_probe ON vendor_usage_log(probe_run_id)`);
377
+ await db.execute(`CREATE INDEX IF NOT EXISTS vendor_usage_log_created ON vendor_usage_log(created_at)`);
378
+ await db.execute(`
379
+ CREATE TABLE IF NOT EXISTS cost_probe_runs (
380
+ id TEXT PRIMARY KEY,
381
+ tool TEXT NOT NULL,
382
+ target TEXT,
383
+ started_at TEXT NOT NULL DEFAULT (datetime('now')),
384
+ wall_ms INTEGER NOT NULL DEFAULT 0,
385
+ success INTEGER NOT NULL DEFAULT 0,
386
+ error TEXT,
387
+ http_only INTEGER NOT NULL DEFAULT 0,
388
+ kernel_used INTEGER NOT NULL DEFAULT 0,
389
+ kernel_sessions INTEGER NOT NULL DEFAULT 0,
390
+ kernel_seconds_total REAL NOT NULL DEFAULT 0,
391
+ kernel_tier_observed TEXT,
392
+ est_kernel_cost_usd_headless REAL NOT NULL DEFAULT 0,
393
+ est_kernel_cost_usd_headful REAL NOT NULL DEFAULT 0,
394
+ vendor_cost_usd REAL NOT NULL DEFAULT 0,
395
+ charged_credits REAL NOT NULL DEFAULT 0,
396
+ margin_usd_headless_starter REAL NOT NULL DEFAULT 0,
397
+ margin_usd_headful_starter REAL NOT NULL DEFAULT 0,
398
+ notes TEXT
399
+ )
400
+ `);
401
+ await db.execute(`CREATE INDEX IF NOT EXISTS cost_probe_runs_tool ON cost_probe_runs(tool)`);
402
+ try {
403
+ await db.execute(`ALTER TABLE cost_probe_runs ADD COLUMN units REAL`);
404
+ } catch {
405
+ }
406
+ try {
407
+ await db.execute(`ALTER TABLE cost_probe_runs ADD COLUMN unit_type TEXT`);
408
+ } catch {
409
+ }
410
+ try {
411
+ await db.execute(`ALTER TABLE cost_probe_runs ADD COLUMN mode TEXT`);
412
+ } catch {
413
+ }
414
+ _ready = true;
415
+ }
416
+ async function recordKernelSession(r) {
417
+ try {
418
+ await migrateCostTelemetry();
419
+ const ctx = currentCostContext();
420
+ const durationMs = Math.max(0, r.closedAtMs - r.openedAtMs);
421
+ const db = getDb();
422
+ await db.execute({
423
+ sql: `INSERT INTO kernel_session_log
424
+ (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)
425
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
426
+ args: [
427
+ (0, import_node_crypto2.randomUUID)(),
428
+ r.kernelSessionId ?? null,
429
+ ctx?.op ?? null,
430
+ r.source,
431
+ ctx?.probeRunId ?? null,
432
+ ctx?.userId ?? null,
433
+ boolToInt(r.stealth),
434
+ boolToInt(r.headlessSent),
435
+ boolToInt(r.proxyUsed),
436
+ r.proxySource ?? null,
437
+ r.proxyType ?? null,
438
+ r.fallback ? 1 : 0,
439
+ new Date(r.openedAtMs).toISOString(),
440
+ new Date(r.closedAtMs).toISOString(),
441
+ durationMs,
442
+ kernelCostUsd(durationMs, false),
443
+ kernelCostUsd(durationMs, true),
444
+ r.error ?? null
445
+ ]
446
+ });
447
+ } catch (err) {
448
+ console.warn("[cost-telemetry] recordKernelSession failed:", err instanceof Error ? err.message : String(err));
449
+ }
450
+ }
451
+ function boolToInt(v) {
452
+ if (v === null || v === void 0) return null;
453
+ return v ? 1 : 0;
454
+ }
455
+
270
456
  // src/driver/BrowserDriver.ts
271
457
  import_playwright_extra.chromium.use((0, import_puppeteer_extra_plugin_stealth.default)());
272
458
  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";
@@ -354,6 +540,12 @@ var BrowserDriver = class {
354
540
  page = null;
355
541
  kernelClient = null;
356
542
  kernelSessionId = null;
543
+ kernelOpenedAtMs = null;
544
+ kernelStealth = null;
545
+ kernelProxyUsed = null;
546
+ kernelProxySource = null;
547
+ kernelProxyType = null;
548
+ kernelHeadlessSent = null;
357
549
  debugEnabled = false;
358
550
  debugSnapshot = {
359
551
  kernel: null,
@@ -385,9 +577,16 @@ var BrowserDriver = class {
385
577
  if (config.kernelProfileName && config.kernelProfileSaveChanges === true) {
386
578
  await ensureKernelProfile(this.kernelClient, config.kernelProfileName);
387
579
  }
580
+ const costCtx = currentCostContext();
581
+ let useHeadless;
582
+ if (config.headlessMode === "headful" || costCtx?.forceHeadful === true) useHeadless = false;
583
+ else if (config.headlessMode === "headless") useHeadless = true;
584
+ else useHeadless = costCtx?.forceHeadless === true || process.env.KERNEL_HEADLESS_DISABLED !== "true" && !!costCtx?.op && HEADLESS_OPS.has(costCtx.op);
585
+ this.kernelHeadlessSent = useHeadless ? true : null;
388
586
  const kernelBrowser = await this.kernelClient.browsers.create({
389
587
  stealth: true,
390
588
  timeout_seconds: timeoutSeconds,
589
+ ...useHeadless ? { headless: true } : {},
391
590
  ...config.kernelProxyId ? { proxy_id: config.kernelProxyId } : {},
392
591
  ...config.kernelProfileName ? {
393
592
  profile: {
@@ -397,9 +596,14 @@ var BrowserDriver = class {
397
596
  } : {}
398
597
  });
399
598
  this.kernelSessionId = kernelBrowser.session_id;
599
+ this.kernelOpenedAtMs = Date.now();
600
+ this.kernelStealth = typeof kernelBrowser.stealth === "boolean" ? kernelBrowser.stealth : null;
601
+ this.kernelProxyUsed = typeof kernelBrowser.proxy_id === "string" ? Boolean(kernelBrowser.proxy_id) : Boolean(config.kernelProxyId);
602
+ this.kernelProxySource = config.kernelProxyResolution?.source ?? null;
603
+ this.kernelProxyType = config.kernelProxyResolution?.proxyType ?? null;
400
604
  let defaultProxyDisabled = null;
401
605
  let defaultProxyDisableError = null;
402
- if (proxyMode === "none") {
606
+ if (!config.kernelProxyId) {
403
607
  try {
404
608
  await withTimeout(
405
609
  this.kernelClient.browsers.update(this.kernelSessionId, { disable_default_proxy: true }),
@@ -750,11 +954,24 @@ var BrowserDriver = class {
750
954
  const b = this.browser;
751
955
  const sessionId = this.kernelSessionId;
752
956
  const client = this.kernelClient;
957
+ const openedAtMs = this.kernelOpenedAtMs;
958
+ const stealth = this.kernelStealth;
959
+ const proxyUsed = this.kernelProxyUsed;
960
+ const proxySource = this.kernelProxySource;
961
+ const proxyType = this.kernelProxyType;
962
+ const headlessSent = this.kernelHeadlessSent;
753
963
  this.browser = null;
754
964
  this.context = null;
755
965
  this.page = null;
756
966
  this.kernelSessionId = null;
757
967
  this.kernelClient = null;
968
+ this.kernelOpenedAtMs = null;
969
+ this.kernelHeadlessSent = null;
970
+ this.kernelProxySource = null;
971
+ this.kernelProxyType = null;
972
+ if (client && sessionId && openedAtMs != null) {
973
+ void recordKernelSession({ kernelSessionId: sessionId, source: "browser_driver", stealth, headlessSent, proxyUsed, proxySource, proxyType, openedAtMs, closedAtMs: Date.now() });
974
+ }
758
975
  if (client && sessionId) {
759
976
  console.info(JSON.stringify({
760
977
  event: "kernel_browser_delete_started",
@@ -1469,6 +1686,7 @@ var PAAExtractor = class {
1469
1686
  );
1470
1687
  let round = 0;
1471
1688
  while (seenQs.size < options.maxQuestions) {
1689
+ if (options.softDeadlineMs && Date.now() >= options.softDeadlineMs) break;
1472
1690
  this.throwIfAborted(signal);
1473
1691
  await this.throwIfCaptcha(page, "Google PAA expansion");
1474
1692
  const beforeQs = await readAllQs();
@@ -1478,6 +1696,7 @@ var PAAExtractor = class {
1478
1696
  if (unexpandedCount === 0) break;
1479
1697
  this.reporter.onDepth(++round);
1480
1698
  for (let ci = 0; ci < unexpandedCount; ci++) {
1699
+ if (options.softDeadlineMs && Date.now() >= options.softDeadlineMs) break;
1481
1700
  this.throwIfAborted(signal);
1482
1701
  try {
1483
1702
  const btn = page.locator(unexpandedSel).first();
@@ -2141,6 +2360,28 @@ var PAAExtractor = class {
2141
2360
  var import_node_fs = require("fs");
2142
2361
  var import_node_path = __toESM(require("path"), 1);
2143
2362
  var import_papaparse = __toESM(require("papaparse"), 1);
2363
+
2364
+ // src/output/memory-library-sink.ts
2365
+ async function postToMemoryLibrary(args) {
2366
+ const url = process.env.MCP_MEMORY_INGEST_URL;
2367
+ const key = process.env.MCP_MEMORY_KEY;
2368
+ if (!url || !key) return;
2369
+ try {
2370
+ const res = await fetch(url, {
2371
+ method: "POST",
2372
+ headers: { "content-type": "application/json" },
2373
+ body: JSON.stringify({ data: { apiKey: key, title: args.title, content: args.content, source: args.source } })
2374
+ });
2375
+ const body = await res.json().catch(() => null);
2376
+ if (!res.ok || body?.result?.error) {
2377
+ console.warn("[memory-library-sink] ingest not accepted:", res.status, body?.result?.message ?? "");
2378
+ }
2379
+ } catch (err) {
2380
+ console.warn("[memory-library-sink] ingest failed:", err?.message);
2381
+ }
2382
+ }
2383
+
2384
+ // src/output/OutputSerializer.ts
2144
2385
  var OutputSerializer = class {
2145
2386
  async writeJSON(result, outputDir) {
2146
2387
  await import_node_fs.promises.mkdir(outputDir, { recursive: true });
@@ -2148,6 +2389,7 @@ var OutputSerializer = class {
2148
2389
  const filename = `${slug}-${Date.now()}.json`;
2149
2390
  const fullPath = import_node_path.default.join(outputDir, filename);
2150
2391
  await import_node_fs.promises.writeFile(fullPath, JSON.stringify(result, null, 2), "utf8");
2392
+ await postToMemoryLibrary({ title: `${result.seed} scrape`, content: JSON.stringify(result), source: `mcp-scraper:${fullPath}` });
2151
2393
  return fullPath;
2152
2394
  }
2153
2395
  async writeCSV(rows, outputDir) {
@@ -2345,11 +2587,13 @@ function proxyIdSuffix2(proxyId) {
2345
2587
  return proxyId ? proxyId.slice(-6) : null;
2346
2588
  }
2347
2589
  function resolution(source, proxyMode, proxyId, target, error, disposable = false) {
2590
+ const proxyType = source === "disabled" ? "none" : source === "configured_fallback" ? "configured" : source === "isp_created" ? "isp" : source === "location_created" || source === "location_reused" ? "residential" : "unknown";
2348
2591
  return {
2349
2592
  kernelProxyId: proxyId,
2350
2593
  ...disposable && proxyId ? { disposableProxyId: proxyId } : {},
2351
2594
  resolution: {
2352
2595
  source,
2596
+ proxyType,
2353
2597
  proxyMode,
2354
2598
  proxyIdPresent: Boolean(proxyId),
2355
2599
  proxyIdSuffix: proxyIdSuffix2(proxyId),
@@ -2544,6 +2788,22 @@ async function createFreshLocationProxy(kernel, options, target) {
2544
2788
  createErrors.push(`${candidate.proxyName}: ${errorText2(err)}`);
2545
2789
  }
2546
2790
  }
2791
+ if (process.env.KERNEL_PROXY_TYPE !== "residential") {
2792
+ const ispTarget = stateTarget(target);
2793
+ try {
2794
+ const created = await kernel.proxies.create({
2795
+ type: "isp",
2796
+ name: freshProxyName(`mcp-serp-isp-${ispTarget.country.toLowerCase()}-${(ispTarget.state ?? "").toLowerCase()}`, options.attemptIndex),
2797
+ config: { country: ispTarget.country, ...ispTarget.state ? { state: ispTarget.state } : {} }
2798
+ });
2799
+ if (created.id) {
2800
+ return resolution("isp_created", options.proxyMode, created.id, ispTarget, null, true);
2801
+ }
2802
+ createErrors.push("isp: Kernel did not return a proxy id");
2803
+ } catch (err) {
2804
+ createErrors.push(`isp: ${errorText2(err)}`);
2805
+ }
2806
+ }
2547
2807
  return resolution("configured_fallback", options.proxyMode, options.configuredKernelProxyId, target, createErrors.join(" | "));
2548
2808
  }
2549
2809
  async function deleteKernelProxyId(kernelApiKey, proxyId) {
@@ -2827,9 +3087,11 @@ async function harvest(rawOptions) {
2827
3087
  };
2828
3088
  const requestedDebug = typeof raw.debug === "boolean" ? raw.debug : false;
2829
3089
  const needsLocationEvidence = proxyMode === "location" && Boolean(proxyOpts.location);
2830
- const maxAttempts = maxAttemptsForProxyMode(proxyMode);
3090
+ const maxAttempts = maxAttemptsForProxyMode(proxyMode) + 1;
2831
3091
  const serializer = new OutputSerializer();
2832
3092
  let lastError = null;
3093
+ let effectiveProxyMode = proxyMode;
3094
+ let proxyFailures = 0;
2833
3095
  for (let i = 0; i < maxAttempts; i++) {
2834
3096
  const attemptNumber = i + 1;
2835
3097
  const startedAtMs = Date.now();
@@ -2837,18 +3099,19 @@ async function harvest(rawOptions) {
2837
3099
  if (signal?.aborted) throw abortReason(signal);
2838
3100
  const resolution2 = await resolveKernelProxyId({
2839
3101
  ...proxyOpts,
3102
+ proxyMode: effectiveProxyMode,
2840
3103
  attemptIndex: i,
2841
- fresh: proxyMode === "location"
3104
+ fresh: effectiveProxyMode === "location"
2842
3105
  });
2843
3106
  const mergedAttempt = {
2844
3107
  ...raw,
2845
3108
  kernelApiKey,
2846
3109
  kernelProxyId: resolution2.kernelProxyId,
2847
3110
  kernelProxyResolution: resolution2.resolution,
2848
- proxyMode,
3111
+ proxyMode: effectiveProxyMode,
2849
3112
  debug: requestedDebug || needsLocationEvidence
2850
3113
  };
2851
- if (proxyMode === "none") mergedAttempt.kernelProxyId = void 0;
3114
+ if (effectiveProxyMode === "none") mergedAttempt.kernelProxyId = void 0;
2852
3115
  const attemptOptions = HarvestOptionsSchema.parse(mergedAttempt);
2853
3116
  await emitAttemptEvent(onAttemptEvent, {
2854
3117
  type: "started",
@@ -2867,10 +3130,17 @@ async function harvest(rawOptions) {
2867
3130
  location: attemptOptions.location ?? null,
2868
3131
  max_questions: attemptOptions.maxQuestions
2869
3132
  }));
2870
- const attempt = await extractOnce(attemptOptions, signal);
3133
+ const isHeadfulEscalation = i === maxAttempts - 1;
3134
+ const needHeadful = isHeadfulEscalation || !attemptOptions.serpOnly;
3135
+ const baseCtx = currentCostContext();
3136
+ const attempt = needHeadful && baseCtx ? await runWithCostContext({ ...baseCtx, forceHeadful: true }, () => extractOnce(attemptOptions, signal)) : await extractOnce(attemptOptions, signal);
2871
3137
  if (attempt.error) {
2872
3138
  const err = attempt.error;
2873
3139
  const outcome = classifyAttemptError(err);
3140
+ if ((outcome === "captcha" || outcome === "proxy_tunnel_failed") && effectiveProxyMode === "location") {
3141
+ proxyFailures++;
3142
+ if (proxyFailures >= 2) effectiveProxyMode = "none";
3143
+ }
2874
3144
  const willRetry = i < maxAttempts - 1 && (outcome === "captcha" || proxyMode === "location" && retryableLocationProxyError(outcome));
2875
3145
  if (outcome === "captcha") {
2876
3146
  console.warn(JSON.stringify({
@@ -2972,6 +3242,10 @@ async function harvest(rawOptions) {
2972
3242
  return finalResult;
2973
3243
  } catch (err) {
2974
3244
  const outcome = classifyAttemptError(err);
3245
+ if ((outcome === "captcha" || outcome === "proxy_tunnel_failed") && effectiveProxyMode === "location") {
3246
+ proxyFailures++;
3247
+ if (proxyFailures >= 2) effectiveProxyMode = "none";
3248
+ }
2975
3249
  const willRetry = i < maxAttempts - 1 && (outcome === "captcha" || proxyMode === "location" && retryableLocationProxyError(outcome));
2976
3250
  if (outcome === "captcha") {
2977
3251
  console.warn(JSON.stringify({