mcp-scraper 0.3.11 → 0.3.12

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 (50) hide show
  1. package/README.md +2 -2
  2. package/dist/bin/api-server.cjs +86 -22
  3. package/dist/bin/api-server.cjs.map +1 -1
  4. package/dist/bin/api-server.js +2 -2
  5. package/dist/bin/browser-agent-stdio-server.cjs +198 -27
  6. package/dist/bin/browser-agent-stdio-server.cjs.map +1 -1
  7. package/dist/bin/browser-agent-stdio-server.js +3 -2
  8. package/dist/bin/browser-agent-stdio-server.js.map +1 -1
  9. package/dist/bin/mcp-scraper-cli.cjs +1 -1
  10. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  11. package/dist/bin/mcp-scraper-cli.js +1 -1
  12. package/dist/bin/mcp-scraper-combined-stdio-server.cjs +254 -56
  13. package/dist/bin/mcp-scraper-combined-stdio-server.cjs.map +1 -1
  14. package/dist/bin/mcp-scraper-combined-stdio-server.js +4 -3
  15. package/dist/bin/mcp-scraper-combined-stdio-server.js.map +1 -1
  16. package/dist/bin/mcp-scraper-install.cjs +1 -1
  17. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  18. package/dist/bin/mcp-scraper-install.js +1 -1
  19. package/dist/bin/mcp-stdio-server.cjs +38 -11
  20. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  21. package/dist/bin/mcp-stdio-server.js +2 -2
  22. package/dist/bin/paa-harvest.cjs +21 -1
  23. package/dist/bin/paa-harvest.cjs.map +1 -1
  24. package/dist/bin/paa-harvest.js +1 -1
  25. package/dist/chunk-3YGKXXUG.js +133 -0
  26. package/dist/chunk-3YGKXXUG.js.map +1 -0
  27. package/dist/{chunk-UWSG3C5J.js → chunk-4OPKIDON.js} +22 -2
  28. package/dist/chunk-4OPKIDON.js.map +1 -0
  29. package/dist/{chunk-VMH7SRWY.js → chunk-7R7VBQRV.js} +39 -12
  30. package/dist/chunk-7R7VBQRV.js.map +1 -0
  31. package/dist/{chunk-L27GJQV7.js → chunk-BW3DGFNQ.js} +28 -5
  32. package/dist/chunk-BW3DGFNQ.js.map +1 -0
  33. package/dist/chunk-FB5ZJGFN.js +7 -0
  34. package/dist/chunk-FB5ZJGFN.js.map +1 -0
  35. package/dist/index.cjs +21 -1
  36. package/dist/index.cjs.map +1 -1
  37. package/dist/index.js +1 -1
  38. package/dist/{server-3SA5Q4OF.js → server-CUPDJLWM.js} +42 -151
  39. package/dist/server-CUPDJLWM.js.map +1 -0
  40. package/dist/{worker-56IXWOQU.js → worker-FG7ZWEGA.js} +2 -2
  41. package/docs/mcp-tool-craft-lint.generated.md +1 -1
  42. package/docs/mcp-tool-manifest.generated.json +1 -1
  43. package/package.json +1 -1
  44. package/dist/chunk-L27GJQV7.js.map +0 -1
  45. package/dist/chunk-RRE7WVHQ.js +0 -7
  46. package/dist/chunk-RRE7WVHQ.js.map +0 -1
  47. package/dist/chunk-UWSG3C5J.js.map +0 -1
  48. package/dist/chunk-VMH7SRWY.js.map +0 -1
  49. package/dist/server-3SA5Q4OF.js.map +0 -1
  50. /package/dist/{worker-56IXWOQU.js.map → worker-FG7ZWEGA.js.map} +0 -0
@@ -10,7 +10,7 @@ import {
10
10
  import "../chunk-LFATOGDF.js";
11
11
  import {
12
12
  PACKAGE_VERSION
13
- } from "../chunk-RRE7WVHQ.js";
13
+ } from "../chunk-FB5ZJGFN.js";
14
14
 
15
15
  // src/cli/human-cli.ts
16
16
  import { Command } from "commander";
@@ -1,10 +1,32 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
3
25
 
4
26
  // bin/mcp-scraper-combined-stdio-server.ts
5
- var import_node_fs4 = require("fs");
6
- var import_node_os4 = require("os");
7
- var import_node_path5 = require("path");
27
+ var import_node_fs5 = require("fs");
28
+ var import_node_os5 = require("os");
29
+ var import_node_path6 = require("path");
8
30
  var import_mcp3 = require("@modelcontextprotocol/sdk/server/mcp.js");
9
31
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
10
32
 
@@ -44,6 +66,29 @@ function youtubeVideoIdFromUrl(url) {
44
66
  }
45
67
  return null;
46
68
  }
69
+ async function readResponseData(res) {
70
+ const text = await res.text();
71
+ if (!text.trim()) return null;
72
+ try {
73
+ return JSON.parse(text);
74
+ } catch {
75
+ return text;
76
+ }
77
+ }
78
+ function httpErrorPayload(path, res, data) {
79
+ const objectData = data && typeof data === "object" && !Array.isArray(data) ? data : null;
80
+ const bodyMessage = objectData ? objectData.message ?? objectData.error ?? objectData.error_code : data;
81
+ return {
82
+ ...objectData ?? { body: data },
83
+ error: objectData?.error ?? objectData?.error_code ?? "mcp_http_error",
84
+ error_type: "http",
85
+ retryable: res.status === 429 || res.status >= 500,
86
+ status: res.status,
87
+ statusText: res.statusText,
88
+ path,
89
+ message: typeof bodyMessage === "string" && bodyMessage.trim() ? bodyMessage : `MCP Scraper HTTP ${res.status}${res.statusText ? ` ${res.statusText}` : ""} for ${path}`
90
+ };
91
+ }
47
92
  var HttpMcpToolExecutor = class {
48
93
  baseUrl;
49
94
  apiKey;
@@ -71,9 +116,9 @@ var HttpMcpToolExecutor = class {
71
116
  body: JSON.stringify(body),
72
117
  signal: AbortSignal.timeout(timeoutMs)
73
118
  });
74
- const data = await res.json();
119
+ const data = await readResponseData(res);
75
120
  if (!res.ok) {
76
- return { content: [{ type: "text", text: JSON.stringify(data) }], isError: true };
121
+ return { content: [{ type: "text", text: JSON.stringify(httpErrorPayload(path, res, data)) }], isError: true };
77
122
  }
78
123
  return { content: [{ type: "text", text: JSON.stringify(data) }] };
79
124
  } catch (err) {
@@ -106,9 +151,9 @@ var HttpMcpToolExecutor = class {
106
151
  },
107
152
  signal: AbortSignal.timeout(timeoutMs)
108
153
  });
109
- const data = await res.json();
154
+ const data = await readResponseData(res);
110
155
  if (!res.ok) {
111
- return { content: [{ type: "text", text: JSON.stringify(data) }], isError: true };
156
+ return { content: [{ type: "text", text: JSON.stringify(httpErrorPayload(path, res, data)) }], isError: true };
112
157
  }
113
158
  return { content: [{ type: "text", text: JSON.stringify(data) }] };
114
159
  } catch (err) {
@@ -252,9 +297,9 @@ var HttpMcpToolExecutor = class {
252
297
 
253
298
  // src/mcp/browser-agent-mcp-server.ts
254
299
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
255
- var import_node_fs = require("fs");
256
- var import_node_os2 = require("os");
257
- var import_node_path2 = require("path");
300
+ var import_node_fs2 = require("fs");
301
+ var import_node_os3 = require("os");
302
+ var import_node_path3 = require("path");
258
303
 
259
304
  // src/lib/browser-service-env.ts
260
305
  function browserServiceProfileName() {
@@ -269,8 +314,137 @@ function browserServiceProfileSaveChanges() {
269
314
  return void 0;
270
315
  }
271
316
 
317
+ // src/services/fanout/export.ts
318
+ var import_node_fs = require("fs");
319
+ var import_node_os = require("os");
320
+ var import_node_path = require("path");
321
+ var import_papaparse = __toESM(require("papaparse"), 1);
322
+ function outputBaseDir() {
323
+ return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path.join)((0, import_node_os.homedir)(), "Downloads", "mcp-scraper");
324
+ }
325
+ function safe(value) {
326
+ return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "capture";
327
+ }
328
+ function writeTable(path, rows, delimiter) {
329
+ (0, import_node_fs.writeFileSync)(path, import_papaparse.default.unparse(rows.length ? rows : [{}], { delimiter }));
330
+ }
331
+ function exportFanout(enriched) {
332
+ const stamp = safe(enriched.capturedAt.replace(/[:.]/g, "-"));
333
+ const outputDir = outputBaseDir();
334
+ const relativeDir = (0, import_node_path.join)("fanout", `${stamp}-${safe(enriched.platform)}`);
335
+ const dir = (0, import_node_path.join)(outputDir, relativeDir);
336
+ (0, import_node_fs.mkdirSync)(dir, { recursive: true });
337
+ const queryRows = enriched.queries.map((q, i) => ({ index: i + 1, query: q }));
338
+ const citationRows = enriched.aggregates.citationOrder.map((c) => {
339
+ const src = enriched.citedUrls.find((s) => s.url === c.url);
340
+ return { rank: c.rank, domain: c.domain, url: c.url, timesCited: c.timesCited, siteType: src?.siteType ?? "", title: src?.title ?? "" };
341
+ });
342
+ const sourceRows = enriched.browsedUrls.map((s) => ({ domain: s.domain, url: s.url, cited: s.cited, timesCited: s.timesCited, siteType: s.siteType, title: s.title, round: s.round ?? "" }));
343
+ const browsedOnlyRows = enriched.browsedOnly.map((s) => ({ domain: s.domain, url: s.url, cited: s.cited, timesCited: s.timesCited, siteType: s.siteType, title: s.title, round: s.round ?? "" }));
344
+ const snippetRows = enriched.snippets.map((s) => ({ domain: s.domain, url: s.url, title: s.title, text: s.text }));
345
+ const domainRows = enriched.aggregates.topSites.map((d) => ({ domain: d.domain, count: d.count, cited: d.cited, timesCited: d.timesCited, siteType: d.siteType }));
346
+ const relativePaths = {
347
+ relativeTo: "MCP_SCRAPER_OUTPUT_DIR or ~/Downloads/mcp-scraper",
348
+ dir: relativeDir,
349
+ json: (0, import_node_path.join)(relativeDir, "fanout.json"),
350
+ queriesCsv: (0, import_node_path.join)(relativeDir, "queries.csv"),
351
+ queriesTsv: (0, import_node_path.join)(relativeDir, "queries.tsv"),
352
+ citationsCsv: (0, import_node_path.join)(relativeDir, "citations.csv"),
353
+ sourcesCsv: (0, import_node_path.join)(relativeDir, "sources.csv"),
354
+ browsedOnlyCsv: (0, import_node_path.join)(relativeDir, "browsed-only.csv"),
355
+ snippetsCsv: (0, import_node_path.join)(relativeDir, "snippets.csv"),
356
+ domainsCsv: (0, import_node_path.join)(relativeDir, "domains.csv"),
357
+ report: (0, import_node_path.join)(relativeDir, "report.html")
358
+ };
359
+ (0, import_node_fs.writeFileSync)((0, import_node_path.join)(outputDir, relativePaths.json), JSON.stringify(enriched, null, 2));
360
+ writeTable((0, import_node_path.join)(outputDir, relativePaths.queriesCsv), queryRows, ",");
361
+ writeTable((0, import_node_path.join)(outputDir, relativePaths.queriesTsv), queryRows, " ");
362
+ writeTable((0, import_node_path.join)(outputDir, relativePaths.citationsCsv), citationRows, ",");
363
+ writeTable((0, import_node_path.join)(outputDir, relativePaths.sourcesCsv), sourceRows, ",");
364
+ writeTable((0, import_node_path.join)(outputDir, relativePaths.browsedOnlyCsv), browsedOnlyRows, ",");
365
+ writeTable((0, import_node_path.join)(outputDir, relativePaths.snippetsCsv), snippetRows, ",");
366
+ writeTable((0, import_node_path.join)(outputDir, relativePaths.domainsCsv), domainRows, ",");
367
+ (0, import_node_fs.writeFileSync)((0, import_node_path.join)(outputDir, relativePaths.report), renderReportHtml(enriched));
368
+ return relativePaths;
369
+ }
370
+ function esc(s) {
371
+ return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
372
+ }
373
+ function renderReportHtml(enriched) {
374
+ const data = JSON.stringify(enriched).replace(/</g, "\\u003c");
375
+ return `<!doctype html>
376
+ <html lang="en">
377
+ <head>
378
+ <meta charset="utf-8" />
379
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
380
+ <title>AI Search Fan-Out \u2014 ${esc(enriched.platform)}</title>
381
+ <style>
382
+ :root{--bg:#0b0d12;--panel:#151922;--line:#252b38;--ink:#e8ecf3;--mut:#8b95a7;--acc:#5b9dff;--cite:#39d98a;--browse:#5a6677}
383
+ *{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.5 -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif}
384
+ .wrap{max-width:980px;margin:0 auto;padding:28px 20px 80px}
385
+ h1{font-size:20px;margin:0 0 2px}.sub{color:var(--mut);margin:0 0 20px;font-size:13px}
386
+ .stat{font-size:30px;font-weight:700}.stat small{font-size:13px;font-weight:400;color:var(--mut)}
387
+ .cards{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin:18px 0}
388
+ .card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:12px 14px}
389
+ .card b{font-size:22px;display:block}.card span{color:var(--mut);font-size:12px}
390
+ .bars{margin:8px 0 22px}.bar{display:flex;align-items:center;gap:8px;margin:4px 0}
391
+ .bar .lab{width:150px;color:var(--mut);font-size:12px;text-align:right}.bar .track{flex:1;background:#10141c;border-radius:5px;overflow:hidden}
392
+ .bar .fill{background:var(--acc);height:16px}.bar .n{width:34px;font-size:12px;color:var(--mut)}
393
+ .tabs{display:flex;gap:8px;margin:22px 0 12px}.tab{background:var(--panel);border:1px solid var(--line);color:var(--ink);padding:7px 14px;border-radius:8px;cursor:pointer}
394
+ .tab.on{background:var(--acc);border-color:var(--acc);color:#04101f;font-weight:600}
395
+ .chips{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:12px}
396
+ .chip{background:var(--panel);border:1px solid var(--line);color:var(--mut);padding:4px 10px;border-radius:20px;cursor:pointer;font-size:12px}
397
+ .chip.on{border-color:var(--acc);color:var(--ink)}.chip b{color:var(--ink)}
398
+ .row{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:10px 12px;margin:6px 0;display:flex;gap:10px;align-items:flex-start}
399
+ .row .idx{color:var(--mut);min-width:22px}.row .main{flex:1}
400
+ .tag{font-size:11px;padding:2px 7px;border-radius:6px;border:1px solid var(--line);color:var(--mut);margin-right:5px}
401
+ .dom{font-weight:600}.url{color:var(--mut);font-size:12px;word-break:break-all}
402
+ .pill{font-size:11px;padding:2px 7px;border-radius:6px}.pill.cite{background:rgba(57,217,138,.15);color:var(--cite)}.pill.brow{background:rgba(90,102,119,.2);color:#aeb8c8}
403
+ .copy{background:none;border:1px solid var(--line);color:var(--mut);border-radius:6px;cursor:pointer;font-size:11px;padding:2px 8px}
404
+ .hide{display:none}
405
+ </style>
406
+ </head>
407
+ <body><div class="wrap">
408
+ <h1>AI Search Fan-Out</h1>
409
+ <p class="sub" id="sub"></p>
410
+ <div class="stat" id="stat"></div>
411
+ <div class="cards" id="cards"></div>
412
+ <div id="charts"></div>
413
+ <div class="tabs"><button class="tab on" data-tab="q">Queries</button><button class="tab" data-tab="u">URLs</button></div>
414
+ <div class="chips" id="chips"></div>
415
+ <div id="list"></div>
416
+ </div>
417
+ <script>
418
+ const D=${data};
419
+ const el=(t,c,h)=>{const e=document.createElement(t);if(c)e.className=c;if(h!=null)e.innerHTML=h;return e};
420
+ document.getElementById('sub').textContent=D.platform+' \xB7 '+(D.meta.model||'model n/a')+' \xB7 '+D.meta.rounds+' search round(s) \xB7 prompt: '+(D.prompt||'');
421
+ document.getElementById('stat').innerHTML='Total Fan-Out: '+D.counts.subQueries+' <small>sub-queries \xB7 '+D.counts.browsed+' URLs researched \xB7 '+D.counts.cited+' cited</small>';
422
+ const cards=[['Sub-queries',D.counts.subQueries],['Researched',D.counts.browsed],['Cited',D.counts.cited],['Browsed-only',D.counts.browsedOnly]];
423
+ const cc=document.getElementById('cards');cards.forEach(([k,v])=>{const c=el('div','card');c.appendChild(el('b',null,v));c.appendChild(el('span',null,k));cc.appendChild(c)});
424
+ function barChart(title,obj){const max=Math.max(1,...Object.values(obj));const box=el('div','bars');box.appendChild(el('div',null,'<b>'+title+'</b>'));Object.entries(obj).forEach(([k,v])=>{const r=el('div','bar');r.appendChild(el('div','lab',k));const t=el('div','track');const f=el('div','fill');f.style.width=(v/max*100)+'%';t.appendChild(f);r.appendChild(t);r.appendChild(el('div','n',v));box.appendChild(r)});return box}
425
+ const charts=document.getElementById('charts');
426
+ charts.appendChild(barChart('URL Categories',D.aggregates.byCategory));
427
+ let tab='q',filter=null;
428
+ const chips=document.getElementById('chips'),list=document.getElementById('list');
429
+ function render(){
430
+ chips.innerHTML='';list.innerHTML='';
431
+ if(tab==='q'){
432
+ D.queries.forEach((q,i)=>{const r=el('div','row');r.appendChild(el('div','idx',i+1));const m=el('div','main');m.appendChild(el('div',null,esc(q)));r.appendChild(m);const b=el('button','copy','copy');b.onclick=()=>navigator.clipboard.writeText(q);r.appendChild(b);list.appendChild(r)});
433
+ } else {
434
+ const counts={};D.browsedUrls.forEach(s=>{counts[s.siteType]=(counts[s.siteType]||0)+1});
435
+ Object.entries(counts).forEach(([k,v])=>{const c=el('button','chip'+(filter===k?' on':''),k+' <b>'+v+'</b>');c.onclick=()=>{filter=filter===k?null:k;render()};chips.appendChild(c)});
436
+ D.browsedUrls.forEach(s=>{if(filter&&s.siteType!==filter)return;const r=el('div','row');const m=el('div','main');m.appendChild(el('div',null,'<span class="dom">'+esc(s.domain)+'</span> <span class="pill '+(s.cited?'cite':'brow')+'">'+(s.cited?'cited '+s.timesCited+'\xD7':'browsed')+'</span> <span class="tag">'+s.siteType+'</span>'));m.appendChild(el('div','url',esc(s.url)));r.appendChild(m);list.appendChild(r)});
437
+ }
438
+ }
439
+ function esc(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')}
440
+ document.querySelectorAll('.tab').forEach(t=>t.onclick=()=>{document.querySelectorAll('.tab').forEach(x=>x.classList.remove('on'));t.classList.add('on');tab=t.dataset.tab;filter=null;render()});
441
+ render();
442
+ </script>
443
+ </body></html>`;
444
+ }
445
+
272
446
  // src/version.ts
273
- var PACKAGE_VERSION = "0.3.11";
447
+ var PACKAGE_VERSION = "0.3.12";
274
448
 
275
449
  // src/mcp/browser-agent-tool-schemas.ts
276
450
  var import_zod = require("zod");
@@ -612,8 +786,8 @@ var BrowserListSessionsOutputSchema = {
612
786
  // src/mcp/replay-annotator.ts
613
787
  var import_node_child_process = require("child_process");
614
788
  var import_promises = require("fs/promises");
615
- var import_node_os = require("os");
616
- var import_node_path = require("path");
789
+ var import_node_os2 = require("os");
790
+ var import_node_path2 = require("path");
617
791
  var import_node_util = require("util");
618
792
  var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
619
793
  function finiteNumber(value) {
@@ -623,11 +797,11 @@ function clamp(value, min, max) {
623
797
  return Math.min(max, Math.max(min, value));
624
798
  }
625
799
  function formatAssTime(seconds) {
626
- const safe = Math.max(0, seconds);
627
- const hours = Math.floor(safe / 3600);
628
- const minutes = Math.floor(safe % 3600 / 60);
629
- const wholeSeconds = Math.floor(safe % 60);
630
- const centiseconds = Math.floor((safe - Math.floor(safe)) * 100);
800
+ const safe2 = Math.max(0, seconds);
801
+ const hours = Math.floor(safe2 / 3600);
802
+ const minutes = Math.floor(safe2 % 3600 / 60);
803
+ const wholeSeconds = Math.floor(safe2 % 60);
804
+ const centiseconds = Math.floor((safe2 - Math.floor(safe2)) * 100);
631
805
  return `${hours}:${String(minutes).padStart(2, "0")}:${String(wholeSeconds).padStart(2, "0")}.${String(centiseconds).padStart(2, "0")}`;
632
806
  }
633
807
  function cssHexToAssColor(color) {
@@ -836,8 +1010,8 @@ function ffmpegFilterPath(path) {
836
1010
  async function annotateReplayVideo(inputFilePath, outputFilePath, options) {
837
1011
  if (!options.annotations.length) throw new Error("annotations must include at least one item");
838
1012
  const size = await videoSize(inputFilePath);
839
- const tmp = await (0, import_promises.mkdtemp)((0, import_node_path.join)((0, import_node_os.tmpdir)(), "mcp-scraper-ass-"));
840
- const assPath = (0, import_node_path.join)(tmp, "annotations.ass");
1013
+ const tmp = await (0, import_promises.mkdtemp)((0, import_node_path2.join)((0, import_node_os2.tmpdir)(), "mcp-scraper-ass-"));
1014
+ const assPath = (0, import_node_path2.join)(tmp, "annotations.ass");
841
1015
  try {
842
1016
  await (0, import_promises.writeFile)(assPath, buildAssSubtitle(options, size), "utf8");
843
1017
  await execFileAsync("ffmpeg", [
@@ -905,8 +1079,8 @@ function actionResult(tool, sessionId, ok, data, nextRecommendedTool = "browser_
905
1079
  nextRecommendedTool
906
1080
  });
907
1081
  }
908
- function outputBaseDir() {
909
- return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path2.join)((0, import_node_os2.homedir)(), "Downloads", "mcp-scraper");
1082
+ function outputBaseDir2() {
1083
+ return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path3.join)((0, import_node_os3.homedir)(), "Downloads", "mcp-scraper");
910
1084
  }
911
1085
  function safeFilePart(value) {
912
1086
  return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 120) || "replay";
@@ -915,7 +1089,7 @@ function replayFilePath(sessionId, replayId, filename) {
915
1089
  const requested = filename?.trim();
916
1090
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
917
1091
  const name = requested ? safeFilePart(requested).replace(/\.mp4$/i, "") : `${stamp}-${safeFilePart(sessionId)}-${safeFilePart(replayId)}`;
918
- return (0, import_node_path2.join)(outputBaseDir(), "browser-replays", `${name}.mp4`);
1092
+ return (0, import_node_path3.join)(outputBaseDir2(), "browser-replays", `${name}.mp4`);
919
1093
  }
920
1094
  function slugPart(value) {
921
1095
  const trimmed = value?.trim().toLowerCase();
@@ -928,6 +1102,11 @@ function savedProfileNameFromEmail(email, domain) {
928
1102
  const domainPart = slugPart(domain);
929
1103
  return `${value}${domainPart ? `-${domainPart}` : ""}`.slice(0, 80) || null;
930
1104
  }
1105
+ function isEnrichedFanout(value) {
1106
+ if (!value || typeof value !== "object") return false;
1107
+ const candidate = value;
1108
+ return typeof candidate.platform === "string" && typeof candidate.capturedAt === "string" && typeof candidate.prompt === "string" && Array.isArray(candidate.queries) && Array.isArray(candidate.browsedUrls) && Array.isArray(candidate.citedUrls) && Array.isArray(candidate.browsedOnly) && Array.isArray(candidate.snippets) && !!candidate.counts && !!candidate.aggregates;
1109
+ }
931
1110
  function annotatedReplayFilePath(sessionId, replayId, filename) {
932
1111
  const requested = filename?.trim();
933
1112
  if (requested) return replayFilePath(sessionId, replayId, requested);
@@ -985,8 +1164,8 @@ function registerBrowserAgentMcpTools(server2, opts) {
985
1164
  }
986
1165
  const bytes = Buffer.from(await res.arrayBuffer());
987
1166
  const filePath = replayFilePath(sessionId, replayId, filename);
988
- (0, import_node_fs.mkdirSync)((0, import_node_path2.join)(outputBaseDir(), "browser-replays"), { recursive: true });
989
- (0, import_node_fs.writeFileSync)(filePath, bytes);
1167
+ (0, import_node_fs2.mkdirSync)((0, import_node_path3.join)(outputBaseDir2(), "browser-replays"), { recursive: true });
1168
+ (0, import_node_fs2.writeFileSync)(filePath, bytes);
990
1169
  return {
991
1170
  ok: true,
992
1171
  data: {
@@ -1445,7 +1624,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
1445
1624
  try {
1446
1625
  const sourcePath = String(downloaded.data.file_path);
1447
1626
  const outputPath = annotatedReplayFilePath(input.session_id, input.replay_id, input.filename);
1448
- (0, import_node_fs.mkdirSync)((0, import_node_path2.join)(outputBaseDir(), "browser-replays"), { recursive: true });
1627
+ (0, import_node_fs2.mkdirSync)((0, import_node_path3.join)(outputBaseDir2(), "browser-replays"), { recursive: true });
1449
1628
  const result = await annotateReplayVideo(sourcePath, outputPath, {
1450
1629
  annotations: input.annotations,
1451
1630
  sourceWidth: input.source_width,
@@ -1524,7 +1703,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
1524
1703
  annotations: annotations("Capture AI Search Fan-Out")
1525
1704
  },
1526
1705
  async (input) => {
1527
- const emit = (result, exports2) => structuredResult({
1706
+ const emit = (result, exports3) => structuredResult({
1528
1707
  ok: true,
1529
1708
  tool: "browser_capture_fanout",
1530
1709
  session_id: input.session_id,
@@ -1540,7 +1719,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
1540
1719
  counts: result.counts,
1541
1720
  aggregates: result.aggregates,
1542
1721
  first_party_domain: result.firstPartyDomain,
1543
- exports: exports2 ?? null,
1722
+ exports: exports3 ?? null,
1544
1723
  ...result.debug ? { debug: result.debug } : {}
1545
1724
  });
1546
1725
  const res = await req("POST", `/agent/sessions/${input.session_id}/capture-fanout`, {
@@ -1552,20 +1731,35 @@ function registerBrowserAgentMcpTools(server2, opts) {
1552
1731
  });
1553
1732
  if (!res.ok) return errorResult("browser_capture_fanout", res.data, input.session_id);
1554
1733
  const hosted = res.data?.result ?? res.data;
1555
- return emit(hosted, res.data?.exports ?? null);
1734
+ let exports2 = res.data?.exports ?? null;
1735
+ if (input.export && !exports2 && isEnrichedFanout(hosted)) {
1736
+ try {
1737
+ exports2 = exportFanout(hosted);
1738
+ } catch (err) {
1739
+ return errorResult(
1740
+ "browser_capture_fanout",
1741
+ {
1742
+ error: `Fan-out captured but local export failed: ${err instanceof Error ? err.message : String(err)}`,
1743
+ result: hosted
1744
+ },
1745
+ input.session_id
1746
+ );
1747
+ }
1748
+ }
1749
+ return emit(hosted, exports2);
1556
1750
  }
1557
1751
  );
1558
1752
  }
1559
1753
 
1560
1754
  // src/mcp/paa-mcp-server.ts
1561
1755
  var import_mcp2 = require("@modelcontextprotocol/sdk/server/mcp.js");
1562
- var import_node_fs3 = require("fs");
1563
- var import_node_path4 = require("path");
1756
+ var import_node_fs4 = require("fs");
1757
+ var import_node_path5 = require("path");
1564
1758
 
1565
1759
  // src/mcp/mcp-response-formatter.ts
1566
- var import_node_fs2 = require("fs");
1567
- var import_node_os3 = require("os");
1568
- var import_node_path3 = require("path");
1760
+ var import_node_fs3 = require("fs");
1761
+ var import_node_os4 = require("os");
1762
+ var import_node_path4 = require("path");
1569
1763
 
1570
1764
  // src/errors.ts
1571
1765
  function sanitizeVendorName(message) {
@@ -1701,17 +1895,17 @@ function reportTitle(full) {
1701
1895
  const title = full.split("\n").find((line) => line.startsWith("# "));
1702
1896
  return title?.replace(/^#\s+/, "").trim() || "MCP Scraper Report";
1703
1897
  }
1704
- function outputBaseDir2() {
1705
- return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path3.join)((0, import_node_os3.homedir)(), "Downloads", "mcp-scraper");
1898
+ function outputBaseDir3() {
1899
+ return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path4.join)((0, import_node_os4.homedir)(), "Downloads", "mcp-scraper");
1706
1900
  }
1707
1901
  function saveFullReport(full) {
1708
1902
  if (!reportSavingEnabled || process.env.MCP_SCRAPER_SAVE_REPORTS === "false") return null;
1709
- const outDir = outputBaseDir2();
1903
+ const outDir = outputBaseDir3();
1710
1904
  try {
1711
- (0, import_node_fs2.mkdirSync)(outDir, { recursive: true });
1905
+ (0, import_node_fs3.mkdirSync)(outDir, { recursive: true });
1712
1906
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1713
- const file = (0, import_node_path3.join)(outDir, `${stamp}-${slugifyReportName(reportTitle(full))}.md`);
1714
- (0, import_node_fs2.writeFileSync)(file, full, "utf8");
1907
+ const file = (0, import_node_path4.join)(outDir, `${stamp}-${slugifyReportName(reportTitle(full))}.md`);
1908
+ (0, import_node_fs3.writeFileSync)(file, full, "utf8");
1715
1909
  return file;
1716
1910
  } catch {
1717
1911
  return null;
@@ -1720,12 +1914,12 @@ function saveFullReport(full) {
1720
1914
  function persistScreenshotLocally(base64, url) {
1721
1915
  if (!reportSavingEnabled || process.env.MCP_SCRAPER_SAVE_REPORTS === "false") return null;
1722
1916
  try {
1723
- const dir = (0, import_node_path3.join)(outputBaseDir2(), "screenshots");
1724
- (0, import_node_fs2.mkdirSync)(dir, { recursive: true });
1917
+ const dir = (0, import_node_path4.join)(outputBaseDir3(), "screenshots");
1918
+ (0, import_node_fs3.mkdirSync)(dir, { recursive: true });
1725
1919
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1726
1920
  const slug = url.replace(/^https?:\/\//, "").replace(/[^a-z0-9]+/gi, "-").replace(/^-+|-+$/g, "").slice(0, 60);
1727
- const filePath = (0, import_node_path3.join)(dir, `${stamp}-${slug}.png`);
1728
- (0, import_node_fs2.writeFileSync)(filePath, Buffer.from(base64, "base64"));
1921
+ const filePath = (0, import_node_path4.join)(dir, `${stamp}-${slug}.png`);
1922
+ (0, import_node_fs3.writeFileSync)(filePath, Buffer.from(base64, "base64"));
1729
1923
  return filePath;
1730
1924
  } catch {
1731
1925
  return null;
@@ -3066,7 +3260,7 @@ function formatInstagramProfileContent(raw, input) {
3066
3260
  const itemRows = items.slice(0, 100).map(
3067
3261
  (item, i) => `| ${i + 1} | ${item.type} | \`${item.shortcode}\` | ${item.url} | ${cell(item.firstSeenStage ?? "")} |`
3068
3262
  ).join("\n");
3069
- const browserLabel = "hosted browser";
3263
+ const browserLabel = browser.profileName ? `hosted browser profile ${browser.profileName}` : "hosted browser";
3070
3264
  const full = [
3071
3265
  `# Instagram Profile Content: ${d.handle ?? input.handle ?? input.url ?? "profile"}`,
3072
3266
  `**Collected:** ${items.length} items \xB7 posts ${typeCounts?.post ?? 0} \xB7 reels ${typeCounts?.reel ?? 0} \xB7 tv ${typeCounts?.tv ?? 0}`,
@@ -3141,7 +3335,7 @@ function formatInstagramMediaDownload(raw, input) {
3141
3335
  const transcriptText = transcript?.text ?? "";
3142
3336
  const chunks = transcript?.chunks ?? [];
3143
3337
  const browser = structuredInstagramBrowser(d.browser);
3144
- const browserLabel = "hosted browser";
3338
+ const browserLabel = browser.profileName ? `hosted browser profile ${browser.profileName}` : "hosted browser";
3145
3339
  const downloadRows = downloads.map((download, i) => {
3146
3340
  const status = download.error ? `error: ${cell(download.error)}` : `${download.sizeBytes ?? 0} bytes`;
3147
3341
  return `| ${i + 1} | ${download.kind} | ${download.savedPath ? `\`${download.savedPath}\`` : "\u2014"} | ${status} |`;
@@ -3386,13 +3580,17 @@ var FacebookVideoTranscribeInputSchema = {
3386
3580
  var InstagramProfileContentInputSchema = {
3387
3581
  handle: import_zod3.z.string().min(1).optional().describe("Instagram handle, with or without @. Provide handle or url."),
3388
3582
  url: import_zod3.z.string().url().optional().describe("Instagram profile URL, e.g. https://www.instagram.com/nasaartemis/. Provide handle or url."),
3389
- maxItems: import_zod3.z.number().int().min(1).max(500).default(50).describe("Maximum profile grid post/reel/tv URLs to collect. Default 50, maximum 500. Use higher values only when the user asks for a fuller archive."),
3390
- maxScrolls: import_zod3.z.number().int().min(0).max(100).default(10).describe("Maximum pagination scroll attempts. Default 10, maximum 100. Increase for long profiles when Instagram continues loading more grid links."),
3583
+ profile: import_zod3.z.string().min(1).optional().describe("Optional saved hosted browser profile name to load authenticated Instagram access. If omitted, the server uses its configured default profile when present."),
3584
+ saveProfileChanges: import_zod3.z.boolean().optional().describe("Whether to save browser changes back to the hosted profile. Leave unset unless intentionally updating the saved login session."),
3585
+ maxItems: import_zod3.z.number().int().min(1).max(2e3).default(50).describe("Maximum profile grid post/reel/tv URLs to collect. Default 50, maximum 2000. Use higher values only when the user asks for a fuller archive."),
3586
+ maxScrolls: import_zod3.z.number().int().min(0).max(250).default(10).describe("Maximum pagination scroll attempts. Default 10, maximum 250. Increase for long profiles when Instagram continues loading more grid links."),
3391
3587
  scrollDelayMs: import_zod3.z.number().int().min(250).max(5e3).default(1200).describe("Delay after each pagination scroll before collecting newly loaded links. Default 1200ms. Increase to 2000-3000ms when Instagram loads slowly."),
3392
3588
  stableScrollLimit: import_zod3.z.number().int().min(1).max(10).default(4).describe("Stop after this many consecutive scrolls with no new links or scroll progress. Default 4.")
3393
3589
  };
3394
3590
  var InstagramMediaDownloadInputSchema = {
3395
3591
  url: import_zod3.z.string().url().describe("Instagram post, reel, or tv URL, e.g. https://www.instagram.com/reel/SHORTCODE/. The tool renders the page, extracts text, image metadata, and Instagram CDN media tracks."),
3592
+ profile: import_zod3.z.string().min(1).optional().describe("Optional saved hosted browser profile name to load authenticated Instagram access. If omitted, the server uses its configured default profile when present."),
3593
+ saveProfileChanges: import_zod3.z.boolean().optional().describe("Whether to save browser changes back to the hosted profile. Leave unset unless intentionally updating the saved login session."),
3396
3594
  mediaTypes: import_zod3.z.array(import_zod3.z.enum(["image", "video", "audio"])).default(["image", "video", "audio"]).describe("Which media types to download when downloadMedia is true. Reels commonly expose separate video-only and audio-only MP4 tracks."),
3397
3595
  downloadMedia: import_zod3.z.boolean().default(true).describe("Download extracted text/media files to the MCP Scraper output directory when the API server can write files. Always returns extracted media URLs even when false."),
3398
3596
  downloadAllTracks: import_zod3.z.boolean().default(false).describe("Download every captured Instagram MP4 track instead of only the selected best video and audio tracks. Use false by default to avoid duplicate bitrates."),
@@ -3804,8 +4002,8 @@ var InstagramBrowserOutput = import_zod3.z.object({
3804
4002
  executablePathConfigured: import_zod3.z.boolean()
3805
4003
  });
3806
4004
  var InstagramPaginationOutput = import_zod3.z.object({
3807
- maxItems: import_zod3.z.number().int().min(1).max(500),
3808
- maxScrolls: import_zod3.z.number().int().min(0).max(100),
4005
+ maxItems: import_zod3.z.number().int().min(1).max(2e3),
4006
+ maxScrolls: import_zod3.z.number().int().min(0).max(250),
3809
4007
  attemptedScrolls: import_zod3.z.number().int().min(0),
3810
4008
  stableScrolls: import_zod3.z.number().int().min(0),
3811
4009
  stableScrollLimit: import_zod3.z.number().int().min(1).max(10),
@@ -4420,8 +4618,8 @@ function localPlanningToolAnnotations(title) {
4420
4618
  }
4421
4619
  function listSavedReports() {
4422
4620
  try {
4423
- const dir = outputBaseDir2();
4424
- return (0, import_node_fs3.readdirSync)(dir).filter((f) => f.endsWith(".md")).map((f) => ({ filename: f, mtimeMs: (0, import_node_fs3.statSync)((0, import_node_path4.join)(dir, f)).mtimeMs })).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, 100);
4621
+ const dir = outputBaseDir3();
4622
+ return (0, import_node_fs4.readdirSync)(dir).filter((f) => f.endsWith(".md")).map((f) => ({ filename: f, mtimeMs: (0, import_node_fs4.statSync)((0, import_node_path5.join)(dir, f)).mtimeMs })).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, 100);
4425
4623
  } catch {
4426
4624
  return [];
4427
4625
  }
@@ -4445,9 +4643,9 @@ function registerSavedReportResources(server2) {
4445
4643
  },
4446
4644
  async (uri, variables) => {
4447
4645
  const requested = Array.isArray(variables.filename) ? variables.filename[0] : variables.filename;
4448
- const filename = (0, import_node_path4.basename)(decodeURIComponent(String(requested ?? "")));
4646
+ const filename = (0, import_node_path5.basename)(decodeURIComponent(String(requested ?? "")));
4449
4647
  if (!filename.endsWith(".md")) throw new Error("Only saved .md reports can be read");
4450
- const text = (0, import_node_fs3.readFileSync)((0, import_node_path4.join)(outputBaseDir2(), filename), "utf8");
4648
+ const text = (0, import_node_fs4.readFileSync)((0, import_node_path5.join)(outputBaseDir3(), filename), "utf8");
4451
4649
  return { contents: [{ uri: uri.href, mimeType: "text/markdown", text }] };
4452
4650
  }
4453
4651
  );
@@ -4747,10 +4945,10 @@ if (!forceStdio && (interactiveTerminal || wantsHelp)) {
4747
4945
  }
4748
4946
  function readApiKeyFile() {
4749
4947
  const explicitPath = process.env.MCP_SCRAPER_KEY_PATH?.trim();
4750
- const paths = [explicitPath, (0, import_node_path5.join)((0, import_node_os4.homedir)(), ".mcp-scraper-key")].filter(Boolean);
4948
+ const paths = [explicitPath, (0, import_node_path6.join)((0, import_node_os5.homedir)(), ".mcp-scraper-key")].filter(Boolean);
4751
4949
  for (const path of paths) {
4752
4950
  try {
4753
- const value = (0, import_node_fs4.readFileSync)(path, "utf8").trim();
4951
+ const value = (0, import_node_fs5.readFileSync)(path, "utf8").trim();
4754
4952
  if (value) return value;
4755
4953
  } catch {
4756
4954
  }