mcp-scraper 0.3.16 → 0.3.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -58
- package/dist/bin/api-server.cjs +418 -53
- package/dist/bin/api-server.cjs.map +1 -1
- package/dist/bin/api-server.js +2 -2
- package/dist/bin/mcp-scraper-cli.cjs +4 -4
- package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
- package/dist/bin/mcp-scraper-cli.js +5 -5
- package/dist/bin/mcp-scraper-cli.js.map +1 -1
- package/dist/bin/mcp-scraper-install.cjs +12 -12
- package/dist/bin/mcp-scraper-install.cjs.map +1 -1
- package/dist/bin/mcp-scraper-install.js +3 -3
- package/dist/bin/mcp-scraper-install.js.map +1 -1
- package/dist/bin/mcp-stdio-server.cjs +2554 -781
- package/dist/bin/mcp-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-stdio-server.js +1377 -9
- package/dist/bin/mcp-stdio-server.js.map +1 -1
- package/dist/bin/paa-harvest.js +2 -3
- package/dist/bin/paa-harvest.js.map +1 -1
- package/dist/{chunk-7XARJHS6.js → chunk-2XCNU22L.js} +332 -13
- package/dist/chunk-2XCNU22L.js.map +1 -0
- package/dist/chunk-C7ZNIXOA.js +7 -0
- package/dist/chunk-C7ZNIXOA.js.map +1 -0
- package/dist/{chunk-ACIUCZ27.js → chunk-DE7WP4UU.js} +11 -11
- package/dist/chunk-DE7WP4UU.js.map +1 -0
- package/dist/{chunk-ZAUMSBV3.js → chunk-HDPWLYK7.js} +6 -6
- package/dist/{chunk-4OPKIDON.js → chunk-IGOEAKC7.js} +4 -6
- package/dist/{chunk-4OPKIDON.js.map → chunk-IGOEAKC7.js.map} +1 -1
- package/dist/{chunk-M2S27J6Z.js → chunk-ZAP7MYVO.js} +26 -1
- package/dist/chunk-ZAP7MYVO.js.map +1 -0
- package/dist/index.js +2 -3
- package/dist/index.js.map +1 -1
- package/dist/{server-LPVBSE2E.js → server-3M3XCVOT.js} +328 -157
- package/dist/server-3M3XCVOT.js.map +1 -0
- package/dist/{worker-2WVKKCC7.js → worker-J6I5XP2F.js} +9 -10
- package/dist/{worker-2WVKKCC7.js.map → worker-J6I5XP2F.js.map} +1 -1
- package/docs/mcp-tool-manifest.generated.json +28 -27
- package/package.json +3 -3
- package/dist/bin/browser-agent-stdio-server.cjs +0 -1523
- package/dist/bin/browser-agent-stdio-server.cjs.map +0 -1
- package/dist/bin/browser-agent-stdio-server.d.cts +0 -1
- package/dist/bin/browser-agent-stdio-server.d.ts +0 -1
- package/dist/bin/browser-agent-stdio-server.js +0 -43
- package/dist/bin/browser-agent-stdio-server.js.map +0 -1
- package/dist/bin/mcp-scraper-combined-stdio-server.cjs +0 -5601
- package/dist/bin/mcp-scraper-combined-stdio-server.cjs.map +0 -1
- package/dist/bin/mcp-scraper-combined-stdio-server.d.cts +0 -1
- package/dist/bin/mcp-scraper-combined-stdio-server.d.ts +0 -1
- package/dist/bin/mcp-scraper-combined-stdio-server.js +0 -70
- package/dist/bin/mcp-scraper-combined-stdio-server.js.map +0 -1
- package/dist/chunk-3YGKXXUG.js +0 -133
- package/dist/chunk-3YGKXXUG.js.map +0 -1
- package/dist/chunk-7XARJHS6.js.map +0 -1
- package/dist/chunk-ACIUCZ27.js.map +0 -1
- package/dist/chunk-ISZWGIWL.js +0 -1332
- package/dist/chunk-ISZWGIWL.js.map +0 -1
- package/dist/chunk-M2S27J6Z.js.map +0 -1
- package/dist/chunk-SOMBZK3V.js +0 -7
- package/dist/chunk-SOMBZK3V.js.map +0 -1
- package/dist/chunk-WN7PBKMV.js +0 -28
- package/dist/chunk-WN7PBKMV.js.map +0 -1
- package/dist/server-LPVBSE2E.js.map +0 -1
- /package/dist/{chunk-ZAUMSBV3.js.map → chunk-HDPWLYK7.js.map} +0 -0
|
@@ -1,10 +1,33 @@
|
|
|
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-stdio-server.ts
|
|
5
|
-
var
|
|
6
|
-
var
|
|
7
|
-
var
|
|
27
|
+
var import_node_fs5 = require("fs");
|
|
28
|
+
var import_node_os5 = require("os");
|
|
29
|
+
var import_node_path6 = require("path");
|
|
30
|
+
var import_mcp3 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
8
31
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
9
32
|
|
|
10
33
|
// src/harvest-timeout.ts
|
|
@@ -297,24 +320,1545 @@ var HttpMcpToolExecutor = class {
|
|
|
297
320
|
}
|
|
298
321
|
};
|
|
299
322
|
|
|
300
|
-
// src/mcp/
|
|
323
|
+
// src/mcp/browser-agent-mcp-server.ts
|
|
301
324
|
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
302
325
|
var import_node_fs2 = require("fs");
|
|
303
|
-
var
|
|
304
|
-
|
|
305
|
-
// src/version.ts
|
|
306
|
-
var PACKAGE_VERSION = "0.3.16";
|
|
326
|
+
var import_node_os3 = require("os");
|
|
327
|
+
var import_node_path3 = require("path");
|
|
307
328
|
|
|
308
|
-
// src/
|
|
329
|
+
// src/services/fanout/export.ts
|
|
309
330
|
var import_node_fs = require("fs");
|
|
310
331
|
var import_node_os = require("os");
|
|
311
332
|
var import_node_path = require("path");
|
|
333
|
+
var import_papaparse = __toESM(require("papaparse"), 1);
|
|
334
|
+
function outputBaseDir() {
|
|
335
|
+
return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path.join)((0, import_node_os.homedir)(), "Downloads", "mcp-scraper");
|
|
336
|
+
}
|
|
337
|
+
function safe(value) {
|
|
338
|
+
return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "capture";
|
|
339
|
+
}
|
|
340
|
+
function writeTable(path, rows, delimiter) {
|
|
341
|
+
(0, import_node_fs.writeFileSync)(path, import_papaparse.default.unparse(rows.length ? rows : [{}], { delimiter }));
|
|
342
|
+
}
|
|
343
|
+
function exportFanout(enriched) {
|
|
344
|
+
const stamp = safe(enriched.capturedAt.replace(/[:.]/g, "-"));
|
|
345
|
+
const outputDir = outputBaseDir();
|
|
346
|
+
const relativeDir = (0, import_node_path.join)("fanout", `${stamp}-${safe(enriched.platform)}`);
|
|
347
|
+
const dir = (0, import_node_path.join)(outputDir, relativeDir);
|
|
348
|
+
(0, import_node_fs.mkdirSync)(dir, { recursive: true });
|
|
349
|
+
const queryRows = enriched.queries.map((q, i) => ({ index: i + 1, query: q }));
|
|
350
|
+
const citationRows = enriched.aggregates.citationOrder.map((c) => {
|
|
351
|
+
const src = enriched.citedUrls.find((s) => s.url === c.url);
|
|
352
|
+
return { rank: c.rank, domain: c.domain, url: c.url, timesCited: c.timesCited, siteType: src?.siteType ?? "", title: src?.title ?? "" };
|
|
353
|
+
});
|
|
354
|
+
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 ?? "" }));
|
|
355
|
+
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 ?? "" }));
|
|
356
|
+
const snippetRows = enriched.snippets.map((s) => ({ domain: s.domain, url: s.url, title: s.title, text: s.text }));
|
|
357
|
+
const domainRows = enriched.aggregates.topSites.map((d) => ({ domain: d.domain, count: d.count, cited: d.cited, timesCited: d.timesCited, siteType: d.siteType }));
|
|
358
|
+
const relativePaths = {
|
|
359
|
+
relativeTo: "MCP_SCRAPER_OUTPUT_DIR or ~/Downloads/mcp-scraper",
|
|
360
|
+
dir: relativeDir,
|
|
361
|
+
json: (0, import_node_path.join)(relativeDir, "fanout.json"),
|
|
362
|
+
queriesCsv: (0, import_node_path.join)(relativeDir, "queries.csv"),
|
|
363
|
+
queriesTsv: (0, import_node_path.join)(relativeDir, "queries.tsv"),
|
|
364
|
+
citationsCsv: (0, import_node_path.join)(relativeDir, "citations.csv"),
|
|
365
|
+
sourcesCsv: (0, import_node_path.join)(relativeDir, "sources.csv"),
|
|
366
|
+
browsedOnlyCsv: (0, import_node_path.join)(relativeDir, "browsed-only.csv"),
|
|
367
|
+
snippetsCsv: (0, import_node_path.join)(relativeDir, "snippets.csv"),
|
|
368
|
+
domainsCsv: (0, import_node_path.join)(relativeDir, "domains.csv"),
|
|
369
|
+
report: (0, import_node_path.join)(relativeDir, "report.html")
|
|
370
|
+
};
|
|
371
|
+
(0, import_node_fs.writeFileSync)((0, import_node_path.join)(outputDir, relativePaths.json), JSON.stringify(enriched, null, 2));
|
|
372
|
+
writeTable((0, import_node_path.join)(outputDir, relativePaths.queriesCsv), queryRows, ",");
|
|
373
|
+
writeTable((0, import_node_path.join)(outputDir, relativePaths.queriesTsv), queryRows, " ");
|
|
374
|
+
writeTable((0, import_node_path.join)(outputDir, relativePaths.citationsCsv), citationRows, ",");
|
|
375
|
+
writeTable((0, import_node_path.join)(outputDir, relativePaths.sourcesCsv), sourceRows, ",");
|
|
376
|
+
writeTable((0, import_node_path.join)(outputDir, relativePaths.browsedOnlyCsv), browsedOnlyRows, ",");
|
|
377
|
+
writeTable((0, import_node_path.join)(outputDir, relativePaths.snippetsCsv), snippetRows, ",");
|
|
378
|
+
writeTable((0, import_node_path.join)(outputDir, relativePaths.domainsCsv), domainRows, ",");
|
|
379
|
+
(0, import_node_fs.writeFileSync)((0, import_node_path.join)(outputDir, relativePaths.report), renderReportHtml(enriched));
|
|
380
|
+
return relativePaths;
|
|
381
|
+
}
|
|
382
|
+
function esc(s) {
|
|
383
|
+
return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
384
|
+
}
|
|
385
|
+
function renderReportHtml(enriched) {
|
|
386
|
+
const data = JSON.stringify(enriched).replace(/</g, "\\u003c");
|
|
387
|
+
return `<!doctype html>
|
|
388
|
+
<html lang="en">
|
|
389
|
+
<head>
|
|
390
|
+
<meta charset="utf-8" />
|
|
391
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
392
|
+
<title>AI Search Fan-Out \u2014 ${esc(enriched.platform)}</title>
|
|
393
|
+
<style>
|
|
394
|
+
:root{--bg:#0b0d12;--panel:#151922;--line:#252b38;--ink:#e8ecf3;--mut:#8b95a7;--acc:#5b9dff;--cite:#39d98a;--browse:#5a6677}
|
|
395
|
+
*{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}
|
|
396
|
+
.wrap{max-width:980px;margin:0 auto;padding:28px 20px 80px}
|
|
397
|
+
h1{font-size:20px;margin:0 0 2px}.sub{color:var(--mut);margin:0 0 20px;font-size:13px}
|
|
398
|
+
.stat{font-size:30px;font-weight:700}.stat small{font-size:13px;font-weight:400;color:var(--mut)}
|
|
399
|
+
.cards{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin:18px 0}
|
|
400
|
+
.card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:12px 14px}
|
|
401
|
+
.card b{font-size:22px;display:block}.card span{color:var(--mut);font-size:12px}
|
|
402
|
+
.bars{margin:8px 0 22px}.bar{display:flex;align-items:center;gap:8px;margin:4px 0}
|
|
403
|
+
.bar .lab{width:150px;color:var(--mut);font-size:12px;text-align:right}.bar .track{flex:1;background:#10141c;border-radius:5px;overflow:hidden}
|
|
404
|
+
.bar .fill{background:var(--acc);height:16px}.bar .n{width:34px;font-size:12px;color:var(--mut)}
|
|
405
|
+
.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}
|
|
406
|
+
.tab.on{background:var(--acc);border-color:var(--acc);color:#04101f;font-weight:600}
|
|
407
|
+
.chips{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:12px}
|
|
408
|
+
.chip{background:var(--panel);border:1px solid var(--line);color:var(--mut);padding:4px 10px;border-radius:20px;cursor:pointer;font-size:12px}
|
|
409
|
+
.chip.on{border-color:var(--acc);color:var(--ink)}.chip b{color:var(--ink)}
|
|
410
|
+
.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}
|
|
411
|
+
.row .idx{color:var(--mut);min-width:22px}.row .main{flex:1}
|
|
412
|
+
.tag{font-size:11px;padding:2px 7px;border-radius:6px;border:1px solid var(--line);color:var(--mut);margin-right:5px}
|
|
413
|
+
.dom{font-weight:600}.url{color:var(--mut);font-size:12px;word-break:break-all}
|
|
414
|
+
.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}
|
|
415
|
+
.copy{background:none;border:1px solid var(--line);color:var(--mut);border-radius:6px;cursor:pointer;font-size:11px;padding:2px 8px}
|
|
416
|
+
.hide{display:none}
|
|
417
|
+
</style>
|
|
418
|
+
</head>
|
|
419
|
+
<body><div class="wrap">
|
|
420
|
+
<h1>AI Search Fan-Out</h1>
|
|
421
|
+
<p class="sub" id="sub"></p>
|
|
422
|
+
<div class="stat" id="stat"></div>
|
|
423
|
+
<div class="cards" id="cards"></div>
|
|
424
|
+
<div id="charts"></div>
|
|
425
|
+
<div class="tabs"><button class="tab on" data-tab="q">Queries</button><button class="tab" data-tab="u">URLs</button></div>
|
|
426
|
+
<div class="chips" id="chips"></div>
|
|
427
|
+
<div id="list"></div>
|
|
428
|
+
</div>
|
|
429
|
+
<script>
|
|
430
|
+
const D=${data};
|
|
431
|
+
const el=(t,c,h)=>{const e=document.createElement(t);if(c)e.className=c;if(h!=null)e.innerHTML=h;return e};
|
|
432
|
+
document.getElementById('sub').textContent=D.platform+' \xB7 '+(D.meta.model||'model n/a')+' \xB7 '+D.meta.rounds+' search round(s) \xB7 prompt: '+(D.prompt||'');
|
|
433
|
+
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>';
|
|
434
|
+
const cards=[['Sub-queries',D.counts.subQueries],['Researched',D.counts.browsed],['Cited',D.counts.cited],['Browsed-only',D.counts.browsedOnly]];
|
|
435
|
+
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)});
|
|
436
|
+
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}
|
|
437
|
+
const charts=document.getElementById('charts');
|
|
438
|
+
charts.appendChild(barChart('URL Categories',D.aggregates.byCategory));
|
|
439
|
+
let tab='q',filter=null;
|
|
440
|
+
const chips=document.getElementById('chips'),list=document.getElementById('list');
|
|
441
|
+
function render(){
|
|
442
|
+
chips.innerHTML='';list.innerHTML='';
|
|
443
|
+
if(tab==='q'){
|
|
444
|
+
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)});
|
|
445
|
+
} else {
|
|
446
|
+
const counts={};D.browsedUrls.forEach(s=>{counts[s.siteType]=(counts[s.siteType]||0)+1});
|
|
447
|
+
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)});
|
|
448
|
+
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)});
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
function esc(s){return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')}
|
|
452
|
+
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()});
|
|
453
|
+
render();
|
|
454
|
+
</script>
|
|
455
|
+
</body></html>`;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// src/version.ts
|
|
459
|
+
var PACKAGE_VERSION = "0.3.18";
|
|
460
|
+
|
|
461
|
+
// src/mcp/browser-agent-tool-schemas.ts
|
|
462
|
+
var import_zod = require("zod");
|
|
463
|
+
var NullableString = import_zod.z.string().nullable();
|
|
464
|
+
var BrowserRawObject = import_zod.z.record(import_zod.z.unknown());
|
|
465
|
+
var BrowserElementOutput = import_zod.z.record(import_zod.z.unknown());
|
|
466
|
+
var BrowserBaseOutput = {
|
|
467
|
+
ok: import_zod.z.boolean().describe("Whether the browser-agent action succeeded."),
|
|
468
|
+
tool: import_zod.z.string().describe("Browser Agent MCP tool that produced this response."),
|
|
469
|
+
session_id: NullableString.describe("Browser session id when the response is scoped to a session.")
|
|
470
|
+
};
|
|
471
|
+
var BrowserReplayBaseOutput = {
|
|
472
|
+
...BrowserBaseOutput,
|
|
473
|
+
replay_id: NullableString.describe("Replay id when the response is scoped to a replay.")
|
|
474
|
+
};
|
|
475
|
+
var BrowserOpenInputSchema = {
|
|
476
|
+
label: import_zod.z.string().optional().describe("Optional human label for this session, shown in the watch console."),
|
|
477
|
+
url: import_zod.z.string().url().optional().describe("Optional URL to navigate to immediately after opening."),
|
|
478
|
+
profile: import_zod.z.string().optional().describe("Optional saved hosted profile name to load a logged-in session for a site."),
|
|
479
|
+
save_profile_changes: import_zod.z.boolean().optional().describe("Persist cookies and browser storage back to the named profile when the session is closed. Use this for profile setup or intentional auth refreshes; avoid parallel sessions writing to the same profile."),
|
|
480
|
+
timeout_seconds: import_zod.z.number().int().min(60).max(259200).optional().describe("How long the direct no-proxy browser session may live before auto-termination. Defaults to 600. The browser idles into a zero-cost standby between actions, so a longer timeout is cheap.")
|
|
481
|
+
};
|
|
482
|
+
var BrowserProfileConnectInputSchema = {
|
|
483
|
+
email: import_zod.z.string().optional().describe("Account email for the login being saved. Used to derive a stable, reusable profile name AND recorded as a note of who is connected. Does NOT import existing cookies \u2014 the user signs in fresh via the returned watch_url."),
|
|
484
|
+
profile: import_zod.z.string().optional().describe("Profile to add this login to. Omit to derive one from email. A single profile holds MANY site logins \u2014 pass the same profile name again with a different domain to stack another account (e.g. add claude.ai to a profile that already has chatgpt.com)."),
|
|
485
|
+
domain: import_zod.z.string().optional().describe("Site to log into, e.g. chatgpt.com, claude.ai, reddit.com. Defaults to chatgpt.com. Each domain is a separate login inside the profile."),
|
|
486
|
+
login_url: import_zod.z.string().url().optional().describe("Login page for the domain. Defaults to https://<domain>/ (https://chatgpt.com/ when domain is omitted)."),
|
|
487
|
+
url: import_zod.z.string().url().optional().describe("Deprecated alias for login_url. Use login_url."),
|
|
488
|
+
note: import_zod.z.string().optional().describe('Free-text note describing this login, e.g. "ChatGPT Plus, work account". Surfaced by browser_profile_list so you can see what the profile holds.'),
|
|
489
|
+
label: import_zod.z.string().optional().describe("Optional human label for this sign-in setup session."),
|
|
490
|
+
timeout_seconds: import_zod.z.number().int().min(60).max(259200).optional().describe("How long the sign-in setup session may live before auto-termination. Defaults to 600.")
|
|
491
|
+
};
|
|
492
|
+
var BrowserProfileListInputSchema = {
|
|
493
|
+
profile: import_zod.z.string().optional().describe("Profile whose saved logins to list. Omit to derive from email."),
|
|
494
|
+
email: import_zod.z.string().optional().describe("Account email used to derive the profile name when profile is not given."),
|
|
495
|
+
domain: import_zod.z.string().optional().describe("Restrict to one site login, e.g. chatgpt.com. Use this to poll a single login until its status reads AUTHENTICATED."),
|
|
496
|
+
connection_id: import_zod.z.string().optional().describe("A specific login connection id returned by browser_profile_connect, to poll just that one.")
|
|
497
|
+
};
|
|
498
|
+
var BrowserSessionInputSchema = {
|
|
499
|
+
session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself.")
|
|
500
|
+
};
|
|
501
|
+
var BrowserLocateTargetSchema = import_zod.z.object({
|
|
502
|
+
name: import_zod.z.string().optional().describe("Optional label for this target, echoed in the result."),
|
|
503
|
+
selector: import_zod.z.string().optional().describe('CSS selector for the exact DOM element to locate, for example h1, input[name="q"], or [data-testid="result"].'),
|
|
504
|
+
text: import_zod.z.string().optional().describe("Visible text to locate when a selector is not known. The tool returns the text range bounds when possible."),
|
|
505
|
+
match: import_zod.z.enum(["contains", "exact"]).default("contains").describe("How to match text targets. Defaults to contains."),
|
|
506
|
+
index: import_zod.z.number().int().min(0).default(0).describe("Zero-based match index when multiple elements match.")
|
|
507
|
+
}).refine((value) => Boolean(value.selector || value.text), {
|
|
508
|
+
message: "target requires selector or text"
|
|
509
|
+
});
|
|
510
|
+
var BrowserLocateInputSchema = {
|
|
511
|
+
session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
512
|
+
targets: import_zod.z.array(BrowserLocateTargetSchema).min(1).max(20).describe("DOM targets to locate in the current viewport. Use selectors for exact elements, or text for visible text ranges.")
|
|
513
|
+
};
|
|
514
|
+
var BrowserGotoInputSchema = {
|
|
515
|
+
session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
516
|
+
url: import_zod.z.string().url().describe("URL to navigate the browser to.")
|
|
517
|
+
};
|
|
518
|
+
var BrowserClickInputSchema = {
|
|
519
|
+
session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
520
|
+
x: import_zod.z.number().describe("X coordinate to click, in screenshot pixels. Use only coordinates from the latest browser_screenshot, browser_read, or browser_locate result; do not guess."),
|
|
521
|
+
y: import_zod.z.number().describe("Y coordinate to click, in screenshot pixels. Use only coordinates from the latest browser_screenshot, browser_read, or browser_locate result; do not guess."),
|
|
522
|
+
button: import_zod.z.enum(["left", "right", "middle"]).default("left").describe("Mouse button."),
|
|
523
|
+
num_clicks: import_zod.z.number().int().min(1).max(3).optional().describe("Number of clicks, e.g. 2 for double-click.")
|
|
524
|
+
};
|
|
525
|
+
var BrowserTypeInputSchema = {
|
|
526
|
+
session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
527
|
+
text: import_zod.z.string().describe("Text to type at the current focus. Click a field first to focus it."),
|
|
528
|
+
delay: import_zod.z.number().int().min(0).max(500).optional().describe("Optional per-keystroke delay in ms for human-like typing.")
|
|
529
|
+
};
|
|
530
|
+
var BrowserScrollInputSchema = {
|
|
531
|
+
session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
532
|
+
delta_y: import_zod.z.number().default(5).describe("Vertical scroll in wheel units. Positive scrolls down, negative up."),
|
|
533
|
+
delta_x: import_zod.z.number().default(0).describe("Horizontal scroll in wheel units."),
|
|
534
|
+
x: import_zod.z.number().optional().describe("X position to scroll at. Defaults to screen center."),
|
|
535
|
+
y: import_zod.z.number().optional().describe("Y position to scroll at. Defaults to screen center.")
|
|
536
|
+
};
|
|
537
|
+
var BrowserPressInputSchema = {
|
|
538
|
+
session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
539
|
+
keys: import_zod.z.array(import_zod.z.string()).min(1).describe('Keys or combinations to press, e.g. ["Return"], ["Ctrl+a"], ["Ctrl+Shift+Tab"].')
|
|
540
|
+
};
|
|
541
|
+
var BrowserReplayStopInputSchema = {
|
|
542
|
+
session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
543
|
+
replay_id: import_zod.z.string().describe("The replay id returned by browser_replay_start or browser_list_replays. Use only a returned replay_id; do not construct one yourself.")
|
|
544
|
+
};
|
|
545
|
+
var BrowserReplayDownloadInputSchema = {
|
|
546
|
+
session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
547
|
+
replay_id: import_zod.z.string().describe("The replay id returned by browser_replay_start or browser_list_replays. Use only a returned replay_id; do not construct one yourself."),
|
|
548
|
+
filename: import_zod.z.string().optional().describe("Optional local MP4 filename. Defaults to a timestamped replay filename.")
|
|
549
|
+
};
|
|
550
|
+
var BrowserReplayAnnotationSchema = import_zod.z.object({
|
|
551
|
+
type: import_zod.z.enum(["box", "circle", "underline", "arrow", "label"]).default("box").describe("Annotation style to draw over the replay."),
|
|
552
|
+
start_seconds: import_zod.z.number().min(0).default(0).describe("When the annotation should appear in the replay."),
|
|
553
|
+
end_seconds: import_zod.z.number().min(0).optional().describe("When the annotation should disappear. Defaults to two seconds after start_seconds."),
|
|
554
|
+
left: import_zod.z.number().optional().describe("Target left edge in browser screenshot pixels. Use element.left from browser_screenshot/browser_read."),
|
|
555
|
+
top: import_zod.z.number().optional().describe("Target top edge in browser screenshot pixels. Use element.top from browser_screenshot/browser_read."),
|
|
556
|
+
width: import_zod.z.number().positive().optional().describe("Target width in browser screenshot pixels. Use element.width from browser_screenshot/browser_read."),
|
|
557
|
+
height: import_zod.z.number().positive().optional().describe("Target height in browser screenshot pixels. Use element.height from browser_screenshot/browser_read."),
|
|
558
|
+
x: import_zod.z.number().optional().describe("Point target x coordinate in browser screenshot pixels when no box is available."),
|
|
559
|
+
y: import_zod.z.number().optional().describe("Point target y coordinate in browser screenshot pixels when no box is available."),
|
|
560
|
+
from_x: import_zod.z.number().optional().describe("Arrow start x coordinate in browser screenshot pixels. Defaults near the target."),
|
|
561
|
+
from_y: import_zod.z.number().optional().describe("Arrow start y coordinate in browser screenshot pixels. Defaults near the target."),
|
|
562
|
+
to_x: import_zod.z.number().optional().describe("Arrow target x coordinate in browser screenshot pixels. Defaults to the target box center."),
|
|
563
|
+
to_y: import_zod.z.number().optional().describe("Arrow target y coordinate in browser screenshot pixels. Defaults to the target box center."),
|
|
564
|
+
label: import_zod.z.string().max(120).optional().describe("Optional text callout to render near the annotation."),
|
|
565
|
+
color: import_zod.z.string().regex(/^#?[0-9a-fA-F]{6}$/).optional().describe("Annotation color as hex, for example #ff3b30."),
|
|
566
|
+
thickness: import_zod.z.number().min(1).max(24).optional().describe("Stroke thickness in pixels. Defaults to 5.")
|
|
567
|
+
});
|
|
568
|
+
var BrowserReplayMarkInputSchema = {
|
|
569
|
+
session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself. A replay must already be recording."),
|
|
570
|
+
target: BrowserLocateTargetSchema.describe("The exact DOM element or text range to mark in the current viewport."),
|
|
571
|
+
type: import_zod.z.enum(["box", "circle", "underline", "arrow"]).default("box").describe("Annotation style to generate. Labels are included on the returned annotation when label is provided."),
|
|
572
|
+
label: import_zod.z.string().max(120).optional().describe("Optional callout text to render near the target."),
|
|
573
|
+
color: import_zod.z.string().regex(/^#?[0-9a-fA-F]{6}$/).optional().describe("Annotation color as hex, for example #ff3b30."),
|
|
574
|
+
thickness: import_zod.z.number().min(1).max(24).optional().describe("Stroke thickness in pixels. Defaults to 5."),
|
|
575
|
+
padding: import_zod.z.number().min(0).max(80).default(8).describe("Pixels to expand the DOM bounds so the highlight does not touch the text edge."),
|
|
576
|
+
start_offset_seconds: import_zod.z.number().min(-5).max(10).default(-0.25).describe("Offset from the current replay time. Negative values make the callout appear just before the mark action is captured."),
|
|
577
|
+
duration_seconds: import_zod.z.number().min(0.5).max(30).default(4).describe("How long the generated annotation should remain visible.")
|
|
578
|
+
};
|
|
579
|
+
var BrowserReplayAnnotateInputSchema = {
|
|
580
|
+
session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
581
|
+
replay_id: import_zod.z.string().describe("The replay id returned by browser_replay_start or browser_list_replays. Use only a returned replay_id; do not construct one yourself."),
|
|
582
|
+
annotations: import_zod.z.array(BrowserReplayAnnotationSchema).min(1).max(50).describe("Timed overlay annotations to render on the replay. Prefer annotations returned by browser_replay_mark; otherwise use exact DOM bounds from browser_locate/browser_screenshot/browser_read."),
|
|
583
|
+
filename: import_zod.z.string().optional().describe("Optional output MP4 filename. Defaults to a timestamped annotated replay filename."),
|
|
584
|
+
source_width: import_zod.z.number().positive().optional().describe("Width of the screenshot coordinate space used for annotations. Defaults to the replay video width."),
|
|
585
|
+
source_height: import_zod.z.number().positive().optional().describe("Height of the coordinate space used for annotations. When this is a page viewport height smaller than the replay video height, the annotator infers the browser chrome top offset."),
|
|
586
|
+
source_left_offset: import_zod.z.number().min(0).optional().describe("Optional explicit X offset from annotation coordinates to replay video coordinates. Usually omitted."),
|
|
587
|
+
source_top_offset: import_zod.z.number().min(0).optional().describe("Optional explicit Y offset from annotation coordinates to replay video coordinates. Usually omitted because browser chrome offset is inferred when source_height is a page viewport height.")
|
|
588
|
+
};
|
|
589
|
+
var BrowserListInputSchema = {
|
|
590
|
+
include_closed: import_zod.z.boolean().default(false).describe("Include closed sessions in the list.")
|
|
591
|
+
};
|
|
592
|
+
var BrowserCaptureFanoutInputSchema = {
|
|
593
|
+
session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. The session must be on chatgpt.com or claude.ai and logged in through a saved hosted profile. Use only a returned session_id; do not construct one yourself."),
|
|
594
|
+
prompt: import_zod.z.string().optional().describe("Optional prompt to type into the chat composer and submit before capturing. Omit to passively capture the fan-out of a prompt the user just ran in the visible browser. The prompt must trigger web search for there to be a fan-out."),
|
|
595
|
+
wait_ms: import_zod.z.number().int().min(0).max(18e4).optional().describe("How long to wait for the answer stream to finish before parsing. Defaults to 90000 when a prompt is sent, 8000 for passive capture. Capture ends as soon as the answer stream finishes."),
|
|
596
|
+
first_party_domain: import_zod.z.string().optional().describe("The brand/site being researched, for example example.com. Sources on this domain are tagged First-party/vendor. Set this when the prompt is about a specific business."),
|
|
597
|
+
reset: import_zod.z.boolean().default(false).describe("Clear any previously buffered stream for this page before capturing. Use when re-running to avoid mixing turns."),
|
|
598
|
+
export: import_zod.z.boolean().default(false).describe("When true, write JSON, CSV, TSV exports (queries, citations, all sources, browsed-only sources, snippets, domains) and a self-contained HTML report to MCP_SCRAPER_OUTPUT_DIR/fanout, returning paths relative to MCP_SCRAPER_OUTPUT_DIR.")
|
|
599
|
+
};
|
|
600
|
+
var FanoutSourceOutput = import_zod.z.object({
|
|
601
|
+
url: import_zod.z.string(),
|
|
602
|
+
domain: import_zod.z.string(),
|
|
603
|
+
title: import_zod.z.string(),
|
|
604
|
+
cited: import_zod.z.boolean(),
|
|
605
|
+
timesCited: import_zod.z.number().int().min(0),
|
|
606
|
+
snippet: import_zod.z.string(),
|
|
607
|
+
round: import_zod.z.number().int().nullable(),
|
|
608
|
+
siteType: import_zod.z.string().describe("URL category: First-party/vendor, News/media, Reddit, Social/video, Encyclopedia, Review site, Docs, or Blog.")
|
|
609
|
+
});
|
|
610
|
+
var BrowserCaptureFanoutOutputSchema = {
|
|
611
|
+
...BrowserBaseOutput,
|
|
612
|
+
tool: import_zod.z.literal("query_fanout_workflow"),
|
|
613
|
+
platform: import_zod.z.enum(["ChatGPT", "Claude"]).describe("Which AI-search surface the fan-out was captured from."),
|
|
614
|
+
captured_at: import_zod.z.string(),
|
|
615
|
+
prompt: import_zod.z.string().describe("The user prompt that triggered the captured fan-out, when recoverable."),
|
|
616
|
+
meta: import_zod.z.object({
|
|
617
|
+
model: import_zod.z.string(),
|
|
618
|
+
finishType: import_zod.z.string(),
|
|
619
|
+
title: import_zod.z.string(),
|
|
620
|
+
rounds: import_zod.z.number().int().min(0)
|
|
621
|
+
}),
|
|
622
|
+
queries: import_zod.z.array(import_zod.z.string()).describe("Every web-search sub-query the model issued, in capture order. Classify these by funnel stage and type yourself from the text."),
|
|
623
|
+
browsed_urls: import_zod.z.array(FanoutSourceOutput).describe("Every researched URL (cited and browsed-only), cited first."),
|
|
624
|
+
cited_urls: import_zod.z.array(FanoutSourceOutput).describe("The subset of researched URLs actually cited in the final answer."),
|
|
625
|
+
browsed_only: import_zod.z.array(FanoutSourceOutput).describe("Researched URLs the model pulled but did not cite."),
|
|
626
|
+
snippets: import_zod.z.array(import_zod.z.object({ url: import_zod.z.string(), domain: import_zod.z.string(), title: import_zod.z.string(), text: import_zod.z.string() })),
|
|
627
|
+
counts: import_zod.z.object({
|
|
628
|
+
subQueries: import_zod.z.number().int().min(0),
|
|
629
|
+
browsed: import_zod.z.number().int().min(0),
|
|
630
|
+
cited: import_zod.z.number().int().min(0),
|
|
631
|
+
browsedOnly: import_zod.z.number().int().min(0)
|
|
632
|
+
}),
|
|
633
|
+
aggregates: import_zod.z.object({
|
|
634
|
+
topSites: import_zod.z.array(import_zod.z.object({ domain: import_zod.z.string(), count: import_zod.z.number().int(), cited: import_zod.z.boolean(), timesCited: import_zod.z.number().int(), siteType: import_zod.z.string() })),
|
|
635
|
+
citationOrder: import_zod.z.array(import_zod.z.object({ rank: import_zod.z.number().int(), domain: import_zod.z.string(), url: import_zod.z.string(), timesCited: import_zod.z.number().int() })),
|
|
636
|
+
byCategory: import_zod.z.record(import_zod.z.number().int())
|
|
637
|
+
}).describe("Objective aggregates: top sourced sites by frequency, citation order, and URL-category counts."),
|
|
638
|
+
first_party_domain: import_zod.z.string().nullable(),
|
|
639
|
+
exports: import_zod.z.object({
|
|
640
|
+
relativeTo: import_zod.z.string(),
|
|
641
|
+
dir: import_zod.z.string(),
|
|
642
|
+
json: import_zod.z.string(),
|
|
643
|
+
queriesCsv: import_zod.z.string(),
|
|
644
|
+
queriesTsv: import_zod.z.string(),
|
|
645
|
+
citationsCsv: import_zod.z.string(),
|
|
646
|
+
sourcesCsv: import_zod.z.string(),
|
|
647
|
+
browsedOnlyCsv: import_zod.z.string(),
|
|
648
|
+
snippetsCsv: import_zod.z.string(),
|
|
649
|
+
domainsCsv: import_zod.z.string(),
|
|
650
|
+
report: import_zod.z.string()
|
|
651
|
+
}).nullable().describe("Relative export paths when export=true, otherwise null. Paths are relative to MCP_SCRAPER_OUTPUT_DIR, or ~/Downloads/mcp-scraper when that env var is not set."),
|
|
652
|
+
debug: import_zod.z.object({
|
|
653
|
+
interceptorReady: import_zod.z.boolean(),
|
|
654
|
+
rawBytes: import_zod.z.number().int().min(0),
|
|
655
|
+
unmappedKeys: import_zod.z.array(import_zod.z.string()),
|
|
656
|
+
note: import_zod.z.string()
|
|
657
|
+
}).optional()
|
|
658
|
+
};
|
|
659
|
+
var BrowserOpenOutputSchema = {
|
|
660
|
+
ok: import_zod.z.boolean(),
|
|
661
|
+
tool: import_zod.z.literal("browser_open"),
|
|
662
|
+
session_id: import_zod.z.string().describe("Session id returned by browser_open. Use only this exact value in later browser_* calls; do not construct one yourself."),
|
|
663
|
+
watch_url: import_zod.z.string().describe("Human watch/takeover URL for this browser session on mcpscraper.dev."),
|
|
664
|
+
live_view_url: NullableString.describe("Deprecated; always null. Open watch_url to view the live session."),
|
|
665
|
+
url: NullableString.describe("Initial URL requested by the caller, when provided."),
|
|
666
|
+
hint: import_zod.z.string(),
|
|
667
|
+
raw: BrowserRawObject.optional()
|
|
668
|
+
};
|
|
669
|
+
var BrowserProfileLogin = import_zod.z.object({
|
|
670
|
+
connection_id: NullableString.describe("Auth connection id for this login."),
|
|
671
|
+
domain: import_zod.z.string().describe("Site this login is for, e.g. chatgpt.com."),
|
|
672
|
+
status: import_zod.z.string().describe("Auth status, e.g. NEEDS_AUTH or AUTHENTICATED."),
|
|
673
|
+
account_email: NullableString.describe("Account email recorded for this login, when known."),
|
|
674
|
+
note: NullableString.describe("Free-text note describing this login."),
|
|
675
|
+
watch_url: NullableString.describe("mcpscraper.dev sign-in link when this login still needs the user to authenticate."),
|
|
676
|
+
last_connected_at: NullableString.describe("When this login was last saved or refreshed.")
|
|
677
|
+
});
|
|
678
|
+
var BrowserProfileConnectOutputSchema = {
|
|
679
|
+
ok: import_zod.z.boolean(),
|
|
680
|
+
tool: import_zod.z.literal("browser_profile_connect"),
|
|
681
|
+
session_id: NullableString,
|
|
682
|
+
auth_connection_id: import_zod.z.string(),
|
|
683
|
+
watch_url: import_zod.z.string().describe("mcpscraper.dev sign-in link to give the user so they can complete this login."),
|
|
684
|
+
live_view_url: NullableString.describe("Deprecated; always null. Open watch_url to view the sign-in."),
|
|
685
|
+
profile: import_zod.z.string().describe("Profile this login was added to. Reuse it to stack more logins or to open a session."),
|
|
686
|
+
domain: import_zod.z.string(),
|
|
687
|
+
setup_url: import_zod.z.string(),
|
|
688
|
+
account_email: NullableString,
|
|
689
|
+
note: NullableString,
|
|
690
|
+
status: import_zod.z.string(),
|
|
691
|
+
flow_status: NullableString,
|
|
692
|
+
flow_step: NullableString,
|
|
693
|
+
flow_expires_at: NullableString,
|
|
694
|
+
post_login_url: NullableString,
|
|
695
|
+
connected_logins: import_zod.z.array(BrowserProfileLogin).describe("Every login currently saved in this profile, so you can see what else is connected."),
|
|
696
|
+
next_steps: import_zod.z.array(import_zod.z.string()),
|
|
697
|
+
raw: BrowserRawObject.optional()
|
|
698
|
+
};
|
|
699
|
+
var BrowserProfileListOutputSchema = {
|
|
700
|
+
ok: import_zod.z.boolean(),
|
|
701
|
+
tool: import_zod.z.literal("browser_profile_list"),
|
|
702
|
+
session_id: import_zod.z.null(),
|
|
703
|
+
profile: import_zod.z.string().describe("Profile these logins belong to."),
|
|
704
|
+
connections: import_zod.z.array(BrowserProfileLogin).describe("All site logins saved in this profile, each with its current auth status and note."),
|
|
705
|
+
count: import_zod.z.number().int().min(0)
|
|
706
|
+
};
|
|
707
|
+
var BrowserScreenshotOutputSchema = {
|
|
708
|
+
...BrowserBaseOutput,
|
|
709
|
+
tool: import_zod.z.literal("browser_screenshot"),
|
|
710
|
+
url: NullableString,
|
|
711
|
+
title: NullableString,
|
|
712
|
+
text: import_zod.z.string(),
|
|
713
|
+
elements: import_zod.z.array(BrowserElementOutput),
|
|
714
|
+
screenshot: import_zod.z.object({
|
|
715
|
+
mime_type: import_zod.z.string(),
|
|
716
|
+
inline: import_zod.z.boolean()
|
|
717
|
+
}).nullable()
|
|
718
|
+
};
|
|
719
|
+
var BrowserReadOutputSchema = {
|
|
720
|
+
...BrowserBaseOutput,
|
|
721
|
+
tool: import_zod.z.literal("browser_read"),
|
|
722
|
+
url: NullableString,
|
|
723
|
+
title: NullableString,
|
|
724
|
+
text: import_zod.z.string(),
|
|
725
|
+
elements: import_zod.z.array(BrowserElementOutput),
|
|
726
|
+
raw: BrowserRawObject.optional()
|
|
727
|
+
};
|
|
728
|
+
var BrowserLocateOutputSchema = {
|
|
729
|
+
...BrowserBaseOutput,
|
|
730
|
+
tool: import_zod.z.literal("browser_locate"),
|
|
731
|
+
url: NullableString,
|
|
732
|
+
title: NullableString,
|
|
733
|
+
viewport: BrowserRawObject.nullable(),
|
|
734
|
+
replay: BrowserRawObject.nullable(),
|
|
735
|
+
targets: import_zod.z.array(BrowserRawObject),
|
|
736
|
+
raw: BrowserRawObject.optional()
|
|
737
|
+
};
|
|
738
|
+
var BrowserActionOutputSchema = {
|
|
739
|
+
...BrowserBaseOutput,
|
|
740
|
+
result: BrowserRawObject.describe("Provider action result. Check ok and follow with browser_screenshot/browser_read when page state matters."),
|
|
741
|
+
nextRecommendedTool: import_zod.z.string().nullable()
|
|
742
|
+
};
|
|
743
|
+
var BrowserReplayStartOutputSchema = {
|
|
744
|
+
...BrowserReplayBaseOutput,
|
|
745
|
+
tool: import_zod.z.literal("browser_replay_start"),
|
|
746
|
+
view_url: NullableString,
|
|
747
|
+
download_url: NullableString,
|
|
748
|
+
raw: BrowserRawObject.optional()
|
|
749
|
+
};
|
|
750
|
+
var BrowserReplayStopOutputSchema = {
|
|
751
|
+
...BrowserReplayBaseOutput,
|
|
752
|
+
tool: import_zod.z.literal("browser_replay_stop"),
|
|
753
|
+
view_url: NullableString,
|
|
754
|
+
download_url: NullableString,
|
|
755
|
+
raw: BrowserRawObject.optional()
|
|
756
|
+
};
|
|
757
|
+
var BrowserListReplaysOutputSchema = {
|
|
758
|
+
...BrowserBaseOutput,
|
|
759
|
+
tool: import_zod.z.literal("browser_list_replays"),
|
|
760
|
+
replays: import_zod.z.array(BrowserRawObject),
|
|
761
|
+
count: import_zod.z.number().int().min(0)
|
|
762
|
+
};
|
|
763
|
+
var BrowserReplayDownloadOutputSchema = {
|
|
764
|
+
...BrowserReplayBaseOutput,
|
|
765
|
+
tool: import_zod.z.literal("browser_replay_download"),
|
|
766
|
+
file_path: NullableString,
|
|
767
|
+
bytes: import_zod.z.number().int().min(0).nullable(),
|
|
768
|
+
mime_type: NullableString,
|
|
769
|
+
download_url: NullableString
|
|
770
|
+
};
|
|
771
|
+
var BrowserReplayMarkOutputSchema = {
|
|
772
|
+
...BrowserReplayBaseOutput,
|
|
773
|
+
tool: import_zod.z.literal("browser_replay_mark"),
|
|
774
|
+
annotation: BrowserRawObject,
|
|
775
|
+
source_width: import_zod.z.number().nullable(),
|
|
776
|
+
source_height: import_zod.z.number().nullable(),
|
|
777
|
+
target: BrowserRawObject.nullable(),
|
|
778
|
+
hint: import_zod.z.string()
|
|
779
|
+
};
|
|
780
|
+
var BrowserReplayAnnotateOutputSchema = {
|
|
781
|
+
...BrowserReplayBaseOutput,
|
|
782
|
+
tool: import_zod.z.literal("browser_replay_annotate"),
|
|
783
|
+
source_file_path: NullableString,
|
|
784
|
+
annotated_file_path: NullableString,
|
|
785
|
+
bytes: import_zod.z.number().int().min(0).nullable(),
|
|
786
|
+
width: import_zod.z.number().int().min(0).nullable(),
|
|
787
|
+
height: import_zod.z.number().int().min(0).nullable(),
|
|
788
|
+
annotation_count: import_zod.z.number().int().min(0).nullable(),
|
|
789
|
+
mime_type: NullableString
|
|
790
|
+
};
|
|
791
|
+
var BrowserCloseOutputSchema = {
|
|
792
|
+
...BrowserBaseOutput,
|
|
793
|
+
tool: import_zod.z.literal("browser_close"),
|
|
794
|
+
closed: import_zod.z.boolean(),
|
|
795
|
+
raw: BrowserRawObject.optional()
|
|
796
|
+
};
|
|
797
|
+
var BrowserListSessionsOutputSchema = {
|
|
798
|
+
ok: import_zod.z.boolean(),
|
|
799
|
+
tool: import_zod.z.literal("browser_list_sessions"),
|
|
800
|
+
session_id: import_zod.z.null(),
|
|
801
|
+
sessions: import_zod.z.array(BrowserRawObject),
|
|
802
|
+
count: import_zod.z.number().int().min(0)
|
|
803
|
+
};
|
|
804
|
+
|
|
805
|
+
// src/mcp/replay-annotator.ts
|
|
806
|
+
var import_node_child_process = require("child_process");
|
|
807
|
+
var import_promises = require("fs/promises");
|
|
808
|
+
var import_node_os2 = require("os");
|
|
809
|
+
var import_node_path2 = require("path");
|
|
810
|
+
var import_node_util = require("util");
|
|
811
|
+
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
812
|
+
function finiteNumber(value) {
|
|
813
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
814
|
+
}
|
|
815
|
+
function clamp(value, min, max) {
|
|
816
|
+
return Math.min(max, Math.max(min, value));
|
|
817
|
+
}
|
|
818
|
+
function formatAssTime(seconds) {
|
|
819
|
+
const safe2 = Math.max(0, seconds);
|
|
820
|
+
const hours = Math.floor(safe2 / 3600);
|
|
821
|
+
const minutes = Math.floor(safe2 % 3600 / 60);
|
|
822
|
+
const wholeSeconds = Math.floor(safe2 % 60);
|
|
823
|
+
const centiseconds = Math.floor((safe2 - Math.floor(safe2)) * 100);
|
|
824
|
+
return `${hours}:${String(minutes).padStart(2, "0")}:${String(wholeSeconds).padStart(2, "0")}.${String(centiseconds).padStart(2, "0")}`;
|
|
825
|
+
}
|
|
826
|
+
function cssHexToAssColor(color) {
|
|
827
|
+
const normalized = color?.trim().match(/^#?([0-9a-fA-F]{6})$/)?.[1] ?? "ff3b30";
|
|
828
|
+
const red = normalized.slice(0, 2);
|
|
829
|
+
const green = normalized.slice(2, 4);
|
|
830
|
+
const blue = normalized.slice(4, 6);
|
|
831
|
+
return `&H${blue}${green}${red}&`;
|
|
832
|
+
}
|
|
833
|
+
function escapeAssText(value) {
|
|
834
|
+
return value.replace(/\\/g, "\\\\").replace(/\{/g, "\\{").replace(/\}/g, "\\}").replace(/\r?\n/g, "\\N");
|
|
835
|
+
}
|
|
836
|
+
function resolveCoordinateTransform(video, source, options) {
|
|
837
|
+
const explicitOffsetX = finiteNumber(options.sourceLeftOffset) ? options.sourceLeftOffset : null;
|
|
838
|
+
const explicitOffsetY = finiteNumber(options.sourceTopOffset) ? options.sourceTopOffset : null;
|
|
839
|
+
const inferredTopOffset = explicitOffsetY == null && Math.round(source.width) === Math.round(video.width) && source.height < video.height && video.height - source.height <= 220 ? video.height - source.height : 0;
|
|
840
|
+
const offsetX = explicitOffsetX ?? 0;
|
|
841
|
+
const offsetY = explicitOffsetY ?? inferredTopOffset;
|
|
842
|
+
return {
|
|
843
|
+
scaleX: (video.width - offsetX) / source.width,
|
|
844
|
+
scaleY: (video.height - offsetY) / source.height,
|
|
845
|
+
offsetX,
|
|
846
|
+
offsetY
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
function transformPoint(x, y, transform) {
|
|
850
|
+
return {
|
|
851
|
+
x: x * transform.scaleX + transform.offsetX,
|
|
852
|
+
y: y * transform.scaleY + transform.offsetY
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
function scaleRect(annotation, transform) {
|
|
856
|
+
if (finiteNumber(annotation.left) && finiteNumber(annotation.top) && finiteNumber(annotation.width) && finiteNumber(annotation.height)) {
|
|
857
|
+
const origin = transformPoint(annotation.left, annotation.top, transform);
|
|
858
|
+
return {
|
|
859
|
+
left: origin.x,
|
|
860
|
+
top: origin.y,
|
|
861
|
+
width: annotation.width * transform.scaleX,
|
|
862
|
+
height: annotation.height * transform.scaleY
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
if (finiteNumber(annotation.x) && finiteNumber(annotation.y)) {
|
|
866
|
+
const radius = finiteNumber(annotation.width) ? annotation.width / 2 : 28;
|
|
867
|
+
const point = transformPoint(annotation.x, annotation.y, transform);
|
|
868
|
+
return {
|
|
869
|
+
left: point.x - radius * transform.scaleX,
|
|
870
|
+
top: point.y - radius * transform.scaleY,
|
|
871
|
+
width: radius * 2 * transform.scaleX,
|
|
872
|
+
height: radius * 2 * transform.scaleY
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
throw new Error("annotation needs either left/top/width/height or x/y");
|
|
876
|
+
}
|
|
877
|
+
function rectPath(rect) {
|
|
878
|
+
const x1 = Math.round(rect.left);
|
|
879
|
+
const y1 = Math.round(rect.top);
|
|
880
|
+
const x2 = Math.round(rect.left + rect.width);
|
|
881
|
+
const y2 = Math.round(rect.top + rect.height);
|
|
882
|
+
return `m ${x1} ${y1} l ${x2} ${y1} l ${x2} ${y2} l ${x1} ${y2} l ${x1} ${y1}`;
|
|
883
|
+
}
|
|
884
|
+
function ellipsePath(rect) {
|
|
885
|
+
const cx = rect.left + rect.width / 2;
|
|
886
|
+
const cy = rect.top + rect.height / 2;
|
|
887
|
+
const rx = Math.max(4, rect.width / 2);
|
|
888
|
+
const ry = Math.max(4, rect.height / 2);
|
|
889
|
+
const points = [];
|
|
890
|
+
for (let i = 0; i <= 32; i += 1) {
|
|
891
|
+
const t = Math.PI * 2 * i / 32;
|
|
892
|
+
const x = Math.round(cx + Math.cos(t) * rx);
|
|
893
|
+
const y = Math.round(cy + Math.sin(t) * ry);
|
|
894
|
+
points.push(`${i === 0 ? "m" : "l"} ${x} ${y}`);
|
|
895
|
+
}
|
|
896
|
+
return points.join(" ");
|
|
897
|
+
}
|
|
898
|
+
function filledRectPath(left, top, width, height) {
|
|
899
|
+
return rectPath({ left, top, width, height });
|
|
900
|
+
}
|
|
901
|
+
function arrowPath(fromX, fromY, toX, toY, thickness) {
|
|
902
|
+
const dx = toX - fromX;
|
|
903
|
+
const dy = toY - fromY;
|
|
904
|
+
const length = Math.max(1, Math.hypot(dx, dy));
|
|
905
|
+
const ux = dx / length;
|
|
906
|
+
const uy = dy / length;
|
|
907
|
+
const px = -uy;
|
|
908
|
+
const py = ux;
|
|
909
|
+
const half = thickness / 2;
|
|
910
|
+
const head = Math.max(14, thickness * 4);
|
|
911
|
+
const baseX = toX - ux * head;
|
|
912
|
+
const baseY = toY - uy * head;
|
|
913
|
+
const p1x = Math.round(fromX + px * half);
|
|
914
|
+
const p1y = Math.round(fromY + py * half);
|
|
915
|
+
const p2x = Math.round(baseX + px * half);
|
|
916
|
+
const p2y = Math.round(baseY + py * half);
|
|
917
|
+
const p3x = Math.round(baseX + px * head * 0.55);
|
|
918
|
+
const p3y = Math.round(baseY + py * head * 0.55);
|
|
919
|
+
const p4x = Math.round(toX);
|
|
920
|
+
const p4y = Math.round(toY);
|
|
921
|
+
const p5x = Math.round(baseX - px * head * 0.55);
|
|
922
|
+
const p5y = Math.round(baseY - py * head * 0.55);
|
|
923
|
+
const p6x = Math.round(baseX - px * half);
|
|
924
|
+
const p6y = Math.round(baseY - py * half);
|
|
925
|
+
const p7x = Math.round(fromX - px * half);
|
|
926
|
+
const p7y = Math.round(fromY - py * half);
|
|
927
|
+
return `m ${p1x} ${p1y} l ${p2x} ${p2y} l ${p3x} ${p3y} l ${p4x} ${p4y} l ${p5x} ${p5y} l ${p6x} ${p6y} l ${p7x} ${p7y} l ${p1x} ${p1y}`;
|
|
928
|
+
}
|
|
929
|
+
function eventLine(start, end, body) {
|
|
930
|
+
return `Dialogue: 0,${formatAssTime(start)},${formatAssTime(end)},Default,,0,0,0,,${body}`;
|
|
931
|
+
}
|
|
932
|
+
function labelLine(start, end, x, y, label) {
|
|
933
|
+
const safeX = Math.round(x);
|
|
934
|
+
const safeY = Math.round(y);
|
|
935
|
+
return eventLine(
|
|
936
|
+
start,
|
|
937
|
+
end,
|
|
938
|
+
`{\\an7\\pos(${safeX},${safeY})\\fs30\\bord4\\shad0\\c&HFFFFFF&\\3c&H000000&}${escapeAssText(label)}`
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
function shapeLine(start, end, path, color, thickness, filled = false) {
|
|
942
|
+
const fillAlpha = filled ? "&H00&" : "&HFF&";
|
|
943
|
+
const border = filled ? 0 : thickness;
|
|
944
|
+
return eventLine(
|
|
945
|
+
start,
|
|
946
|
+
end,
|
|
947
|
+
`{\\an7\\pos(0,0)\\p1\\bord${border}\\shad0\\1a${fillAlpha}\\1c${color}\\3c${color}}${path}`
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
function buildAssSubtitle(options, video) {
|
|
951
|
+
const source = {
|
|
952
|
+
width: options.sourceWidth && options.sourceWidth > 0 ? options.sourceWidth : video.width,
|
|
953
|
+
height: options.sourceHeight && options.sourceHeight > 0 ? options.sourceHeight : video.height
|
|
954
|
+
};
|
|
955
|
+
const transform = resolveCoordinateTransform(video, source, options);
|
|
956
|
+
const lines = [];
|
|
957
|
+
for (const annotation of options.annotations) {
|
|
958
|
+
const start = Math.max(0, annotation.start_seconds ?? 0);
|
|
959
|
+
const end = annotation.end_seconds && annotation.end_seconds > start ? annotation.end_seconds : start + 2;
|
|
960
|
+
const type = annotation.type ?? "box";
|
|
961
|
+
const color = cssHexToAssColor(annotation.color);
|
|
962
|
+
const thickness = Math.round(clamp(annotation.thickness ?? 5, 2, 24));
|
|
963
|
+
if (type === "label") {
|
|
964
|
+
if (!annotation.label) throw new Error("label annotation needs label");
|
|
965
|
+
const rect2 = scaleRect(annotation, transform);
|
|
966
|
+
lines.push(labelLine(start, end, rect2.left, Math.max(8, rect2.top), annotation.label));
|
|
967
|
+
continue;
|
|
968
|
+
}
|
|
969
|
+
if (type === "arrow") {
|
|
970
|
+
const rect2 = finiteNumber(annotation.to_x) && finiteNumber(annotation.to_y) ? null : scaleRect(annotation, transform);
|
|
971
|
+
const toPoint = finiteNumber(annotation.to_x) && finiteNumber(annotation.to_y) ? transformPoint(annotation.to_x, annotation.to_y, transform) : null;
|
|
972
|
+
const toX = toPoint ? toPoint.x : rect2.left + rect2.width / 2;
|
|
973
|
+
const toY = toPoint ? toPoint.y : rect2.top + rect2.height / 2;
|
|
974
|
+
const fromPoint = finiteNumber(annotation.from_x) && finiteNumber(annotation.from_y) ? transformPoint(annotation.from_x, annotation.from_y, transform) : null;
|
|
975
|
+
const fromX = fromPoint ? fromPoint.x : clamp(toX - 120, 12, video.width - 12);
|
|
976
|
+
const fromY = fromPoint ? fromPoint.y : clamp(toY - 90, 12, video.height - 12);
|
|
977
|
+
lines.push(shapeLine(start, end, arrowPath(fromX, fromY, toX, toY, thickness), color, thickness, true));
|
|
978
|
+
if (annotation.label) lines.push(labelLine(start, end, fromX + 8, Math.max(96, fromY - 36), annotation.label));
|
|
979
|
+
continue;
|
|
980
|
+
}
|
|
981
|
+
const rect = scaleRect(annotation, transform);
|
|
982
|
+
if (type === "underline") {
|
|
983
|
+
const underlineTop = rect.top + rect.height + thickness;
|
|
984
|
+
lines.push(shapeLine(start, end, filledRectPath(rect.left, underlineTop, rect.width, thickness), color, 0, true));
|
|
985
|
+
} else if (type === "circle") {
|
|
986
|
+
lines.push(shapeLine(start, end, ellipsePath(rect), color, thickness));
|
|
987
|
+
} else {
|
|
988
|
+
lines.push(shapeLine(start, end, rectPath(rect), color, thickness));
|
|
989
|
+
}
|
|
990
|
+
if (annotation.label) lines.push(labelLine(start, end, rect.left, Math.max(8, rect.top - 38), annotation.label));
|
|
991
|
+
}
|
|
992
|
+
return [
|
|
993
|
+
"[Script Info]",
|
|
994
|
+
"ScriptType: v4.00+",
|
|
995
|
+
`PlayResX: ${Math.round(video.width)}`,
|
|
996
|
+
`PlayResY: ${Math.round(video.height)}`,
|
|
997
|
+
"ScaledBorderAndShadow: yes",
|
|
998
|
+
"",
|
|
999
|
+
"[V4+ Styles]",
|
|
1000
|
+
"Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding",
|
|
1001
|
+
"Style: Default,Arial,30,&H00FFFFFF,&H000000FF,&H00000000,&H90000000,0,0,0,0,100,100,0,0,1,2,0,7,0,0,0,1",
|
|
1002
|
+
"",
|
|
1003
|
+
"[Events]",
|
|
1004
|
+
"Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text",
|
|
1005
|
+
...lines,
|
|
1006
|
+
""
|
|
1007
|
+
].join("\n");
|
|
1008
|
+
}
|
|
1009
|
+
async function videoSize(inputFilePath) {
|
|
1010
|
+
const { stdout } = await execFileAsync("ffprobe", [
|
|
1011
|
+
"-v",
|
|
1012
|
+
"error",
|
|
1013
|
+
"-select_streams",
|
|
1014
|
+
"v:0",
|
|
1015
|
+
"-show_entries",
|
|
1016
|
+
"stream=width,height",
|
|
1017
|
+
"-of",
|
|
1018
|
+
"json",
|
|
1019
|
+
inputFilePath
|
|
1020
|
+
], { maxBuffer: 1024 * 1024 });
|
|
1021
|
+
const parsed = JSON.parse(stdout);
|
|
1022
|
+
const stream = parsed.streams?.[0];
|
|
1023
|
+
if (!stream?.width || !stream.height) throw new Error("could not read replay video dimensions");
|
|
1024
|
+
return { width: stream.width, height: stream.height };
|
|
1025
|
+
}
|
|
1026
|
+
function ffmpegFilterPath(path) {
|
|
1027
|
+
return path.replace(/\\/g, "\\\\").replace(/:/g, "\\:");
|
|
1028
|
+
}
|
|
1029
|
+
async function annotateReplayVideo(inputFilePath, outputFilePath, options) {
|
|
1030
|
+
if (!options.annotations.length) throw new Error("annotations must include at least one item");
|
|
1031
|
+
const size = await videoSize(inputFilePath);
|
|
1032
|
+
const tmp = await (0, import_promises.mkdtemp)((0, import_node_path2.join)((0, import_node_os2.tmpdir)(), "mcp-scraper-ass-"));
|
|
1033
|
+
const assPath = (0, import_node_path2.join)(tmp, "annotations.ass");
|
|
1034
|
+
try {
|
|
1035
|
+
await (0, import_promises.writeFile)(assPath, buildAssSubtitle(options, size), "utf8");
|
|
1036
|
+
await execFileAsync("ffmpeg", [
|
|
1037
|
+
"-y",
|
|
1038
|
+
"-i",
|
|
1039
|
+
inputFilePath,
|
|
1040
|
+
"-vf",
|
|
1041
|
+
`ass=${ffmpegFilterPath(assPath)}`,
|
|
1042
|
+
"-c:v",
|
|
1043
|
+
"libx264",
|
|
1044
|
+
"-pix_fmt",
|
|
1045
|
+
"yuv420p",
|
|
1046
|
+
"-movflags",
|
|
1047
|
+
"+faststart",
|
|
1048
|
+
"-c:a",
|
|
1049
|
+
"copy",
|
|
1050
|
+
outputFilePath
|
|
1051
|
+
], { maxBuffer: 1024 * 1024 * 20 });
|
|
1052
|
+
const out = await (0, import_promises.stat)(outputFilePath);
|
|
1053
|
+
return {
|
|
1054
|
+
filePath: outputFilePath,
|
|
1055
|
+
bytes: out.size,
|
|
1056
|
+
width: size.width,
|
|
1057
|
+
height: size.height,
|
|
1058
|
+
annotationCount: options.annotations.length
|
|
1059
|
+
};
|
|
1060
|
+
} finally {
|
|
1061
|
+
await (0, import_promises.rm)(tmp, { recursive: true, force: true });
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
312
1064
|
|
|
313
1065
|
// src/errors.ts
|
|
314
1066
|
function sanitizeVendorName(message) {
|
|
315
1067
|
return message.replace(/kernel\.sh\s+sessions?/gi, "sessions").replace(/kernel\.sh\s+session/gi, "this session").replace(/kernel\.sh/gi, "the service").replace(/kernel\s+sessions?/gi, "sessions").replace(/kernel\s+session/gi, "this session").replace(/\bkernel\b/gi, "the service").replace(/ +/g, " ").trim();
|
|
316
1068
|
}
|
|
317
1069
|
|
|
1070
|
+
// src/api/outbound-sanitize.ts
|
|
1071
|
+
var KEY_RENAMES = {
|
|
1072
|
+
kernel: "browserRuntime",
|
|
1073
|
+
kernel_session_id: "browser_session_id",
|
|
1074
|
+
kernel_delete_started: "session_cleanup_started",
|
|
1075
|
+
kernel_delete_succeeded: "session_cleanup_succeeded",
|
|
1076
|
+
kernel_delete_error: "session_cleanup_error",
|
|
1077
|
+
kernelSessionId: "browserSessionId",
|
|
1078
|
+
kernelDeleteStarted: "sessionCleanupStarted",
|
|
1079
|
+
kernelDeleteSucceeded: "sessionCleanupSucceeded",
|
|
1080
|
+
kernelDeleteError: "sessionCleanupError",
|
|
1081
|
+
kernelProxyId: "proxyId"
|
|
1082
|
+
};
|
|
1083
|
+
var SANITIZED_VALUE_KEYS = /error|message/i;
|
|
1084
|
+
var VENDOR_URL_KEYS = /* @__PURE__ */ new Set([
|
|
1085
|
+
"hosted_url",
|
|
1086
|
+
"hostedUrl",
|
|
1087
|
+
"live_view_url",
|
|
1088
|
+
"liveViewUrl",
|
|
1089
|
+
"cdp_ws_url",
|
|
1090
|
+
"cdpWsUrl",
|
|
1091
|
+
"browser_live_view_url"
|
|
1092
|
+
]);
|
|
1093
|
+
var VENDOR_URL_RE = /\b(?:wss?|https?):\/\/[^\s"'<>]*\bonkernel\.com[^\s"'<>]*/gi;
|
|
1094
|
+
function redactVendorUrls(value) {
|
|
1095
|
+
return value.replace(VENDOR_URL_RE, "[browser-service]");
|
|
1096
|
+
}
|
|
1097
|
+
function sanitizeOutboundDiagnostics(value, parentKey = "") {
|
|
1098
|
+
if (typeof value === "string") {
|
|
1099
|
+
let out = redactVendorUrls(value);
|
|
1100
|
+
if (SANITIZED_VALUE_KEYS.test(parentKey) && /kernel/i.test(out)) {
|
|
1101
|
+
out = sanitizeVendorName(out);
|
|
1102
|
+
}
|
|
1103
|
+
return out;
|
|
1104
|
+
}
|
|
1105
|
+
if (Array.isArray(value)) return value.map((v) => sanitizeOutboundDiagnostics(v, parentKey));
|
|
1106
|
+
if (value !== null && typeof value === "object") {
|
|
1107
|
+
const out = {};
|
|
1108
|
+
for (const [key, val] of Object.entries(value)) {
|
|
1109
|
+
const renamed = KEY_RENAMES[key] ?? key;
|
|
1110
|
+
if (VENDOR_URL_KEYS.has(key)) {
|
|
1111
|
+
out[renamed] = null;
|
|
1112
|
+
continue;
|
|
1113
|
+
}
|
|
1114
|
+
out[renamed] = sanitizeOutboundDiagnostics(val, key);
|
|
1115
|
+
}
|
|
1116
|
+
return out;
|
|
1117
|
+
}
|
|
1118
|
+
return value;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// src/mcp/browser-agent-mcp-server.ts
|
|
1122
|
+
function structuredResult(value, isError = false) {
|
|
1123
|
+
const safe2 = sanitizeOutboundDiagnostics(value);
|
|
1124
|
+
return {
|
|
1125
|
+
content: [{ type: "text", text: JSON.stringify(safe2) }],
|
|
1126
|
+
structuredContent: safe2,
|
|
1127
|
+
isError
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
function errorMessage(value) {
|
|
1131
|
+
if (value && typeof value === "object") {
|
|
1132
|
+
const data = value;
|
|
1133
|
+
if (typeof data.error === "string") return data.error;
|
|
1134
|
+
if (typeof data.message === "string") return data.message;
|
|
1135
|
+
}
|
|
1136
|
+
return typeof value === "string" ? value : "Browser Agent request failed";
|
|
1137
|
+
}
|
|
1138
|
+
function errorResult(tool, value, sessionId = null, replayId = null) {
|
|
1139
|
+
return structuredResult({
|
|
1140
|
+
ok: false,
|
|
1141
|
+
tool,
|
|
1142
|
+
session_id: sessionId,
|
|
1143
|
+
...replayId !== null ? { replay_id: replayId } : {},
|
|
1144
|
+
error: errorMessage(value),
|
|
1145
|
+
raw: value && typeof value === "object" ? value : { value }
|
|
1146
|
+
}, true);
|
|
1147
|
+
}
|
|
1148
|
+
function actionResult(tool, sessionId, ok, data, nextRecommendedTool = "browser_screenshot") {
|
|
1149
|
+
if (!ok) return errorResult(tool, data, sessionId);
|
|
1150
|
+
return structuredResult({
|
|
1151
|
+
ok: true,
|
|
1152
|
+
tool,
|
|
1153
|
+
session_id: sessionId,
|
|
1154
|
+
result: data && typeof data === "object" ? data : { value: data },
|
|
1155
|
+
nextRecommendedTool
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
function outputBaseDir2() {
|
|
1159
|
+
return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path3.join)((0, import_node_os3.homedir)(), "Downloads", "mcp-scraper");
|
|
1160
|
+
}
|
|
1161
|
+
function safeFilePart(value) {
|
|
1162
|
+
return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 120) || "replay";
|
|
1163
|
+
}
|
|
1164
|
+
function replayFilePath(sessionId, replayId, filename) {
|
|
1165
|
+
const requested = filename?.trim();
|
|
1166
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
1167
|
+
const name = requested ? safeFilePart(requested).replace(/\.mp4$/i, "") : `${stamp}-${safeFilePart(sessionId)}-${safeFilePart(replayId)}`;
|
|
1168
|
+
return (0, import_node_path3.join)(outputBaseDir2(), "browser-replays", `${name}.mp4`);
|
|
1169
|
+
}
|
|
1170
|
+
function slugPart(value) {
|
|
1171
|
+
const trimmed = value?.trim().toLowerCase();
|
|
1172
|
+
if (!trimmed) return null;
|
|
1173
|
+
return trimmed.replace(/^https?:\/\//, "").replace(/^www\./, "").replace(/\/.*$/, "").replace(/[^a-z0-9._-]+/g, "-").replace(/[._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1174
|
+
}
|
|
1175
|
+
function savedProfileNameFromEmail(email) {
|
|
1176
|
+
const value = slugPart(email);
|
|
1177
|
+
if (!value) return null;
|
|
1178
|
+
return value.slice(0, 80) || null;
|
|
1179
|
+
}
|
|
1180
|
+
function isEnrichedFanout(value) {
|
|
1181
|
+
if (!value || typeof value !== "object") return false;
|
|
1182
|
+
const candidate = value;
|
|
1183
|
+
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;
|
|
1184
|
+
}
|
|
1185
|
+
function annotatedReplayFilePath(sessionId, replayId, filename) {
|
|
1186
|
+
const requested = filename?.trim();
|
|
1187
|
+
if (requested) return replayFilePath(sessionId, replayId, requested);
|
|
1188
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
1189
|
+
return replayFilePath(sessionId, replayId, `${stamp}-${safeFilePart(sessionId)}-${safeFilePart(replayId)}-annotated`);
|
|
1190
|
+
}
|
|
1191
|
+
function finiteNumber2(value) {
|
|
1192
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
1193
|
+
}
|
|
1194
|
+
function expandElementBounds(element, viewport, padding) {
|
|
1195
|
+
const sourceWidth = finiteNumber2(viewport?.width) && viewport.width > 0 ? viewport.width : 1920;
|
|
1196
|
+
const sourceHeight = finiteNumber2(viewport?.height) && viewport.height > 0 ? viewport.height : 1080;
|
|
1197
|
+
const left = Math.max(0, element.left - padding);
|
|
1198
|
+
const top = Math.max(0, element.top - padding);
|
|
1199
|
+
const right = Math.min(sourceWidth, element.left + element.width + padding);
|
|
1200
|
+
const bottom = Math.min(sourceHeight, element.top + element.height + padding);
|
|
1201
|
+
return {
|
|
1202
|
+
left,
|
|
1203
|
+
top,
|
|
1204
|
+
width: Math.max(1, right - left),
|
|
1205
|
+
height: Math.max(1, bottom - top),
|
|
1206
|
+
sourceWidth,
|
|
1207
|
+
sourceHeight
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
function registerBrowserAgentMcpTools(server2, opts) {
|
|
1211
|
+
const baseUrl2 = opts.baseUrl.replace(/\/$/, "");
|
|
1212
|
+
const consoleBase = (opts.consoleBaseUrl ?? opts.baseUrl).replace(/\/$/, "");
|
|
1213
|
+
const timeoutMs = opts.timeoutMs ?? 9e4;
|
|
1214
|
+
async function req(method, path, body, requestTimeoutMs = timeoutMs) {
|
|
1215
|
+
try {
|
|
1216
|
+
const res = await fetch(`${baseUrl2}${path}`, {
|
|
1217
|
+
method,
|
|
1218
|
+
headers: { "Content-Type": "application/json", "x-api-key": opts.apiKey },
|
|
1219
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
1220
|
+
signal: AbortSignal.timeout(requestTimeoutMs)
|
|
1221
|
+
});
|
|
1222
|
+
const data = await res.json().catch(() => ({}));
|
|
1223
|
+
return { ok: res.ok, data };
|
|
1224
|
+
} catch (err) {
|
|
1225
|
+
return { ok: false, data: { error: err instanceof Error ? err.message : String(err) } };
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
async function downloadReplay(sessionId, replayId, filename) {
|
|
1229
|
+
const path = `/agent/sessions/${encodeURIComponent(sessionId)}/replays/${encodeURIComponent(replayId)}/download`;
|
|
1230
|
+
try {
|
|
1231
|
+
const res = await fetch(`${baseUrl2}${path}`, {
|
|
1232
|
+
method: "GET",
|
|
1233
|
+
headers: { "x-api-key": opts.apiKey },
|
|
1234
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
1235
|
+
});
|
|
1236
|
+
if (!res.ok) {
|
|
1237
|
+
const data = await res.json().catch(async () => ({ error: await res.text().catch(() => `HTTP ${res.status}`) }));
|
|
1238
|
+
return { ok: false, data };
|
|
1239
|
+
}
|
|
1240
|
+
const bytes = Buffer.from(await res.arrayBuffer());
|
|
1241
|
+
const filePath = replayFilePath(sessionId, replayId, filename);
|
|
1242
|
+
(0, import_node_fs2.mkdirSync)((0, import_node_path3.join)(outputBaseDir2(), "browser-replays"), { recursive: true });
|
|
1243
|
+
(0, import_node_fs2.writeFileSync)(filePath, bytes);
|
|
1244
|
+
return {
|
|
1245
|
+
ok: true,
|
|
1246
|
+
data: {
|
|
1247
|
+
replay_id: replayId,
|
|
1248
|
+
file_path: filePath,
|
|
1249
|
+
bytes: bytes.length,
|
|
1250
|
+
mime_type: res.headers.get("content-type") ?? "video/mp4",
|
|
1251
|
+
download_url: `${baseUrl2}${path}`
|
|
1252
|
+
}
|
|
1253
|
+
};
|
|
1254
|
+
} catch (err) {
|
|
1255
|
+
return { ok: false, data: { error: err instanceof Error ? err.message : String(err) } };
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
const annotations = (title, readOnly = false) => ({
|
|
1259
|
+
title,
|
|
1260
|
+
readOnlyHint: readOnly,
|
|
1261
|
+
destructiveHint: false,
|
|
1262
|
+
idempotentHint: false,
|
|
1263
|
+
openWorldHint: true
|
|
1264
|
+
});
|
|
1265
|
+
function normalizeLogin(conn) {
|
|
1266
|
+
return {
|
|
1267
|
+
connection_id: conn.connection_id ?? null,
|
|
1268
|
+
domain: conn.domain,
|
|
1269
|
+
status: conn.status,
|
|
1270
|
+
account_email: conn.account_email ?? null,
|
|
1271
|
+
note: conn.note ?? null,
|
|
1272
|
+
watch_url: conn.connection_id && conn.status !== "AUTHENTICATED" ? `${consoleBase}/console/auth/${conn.connection_id}` : null,
|
|
1273
|
+
last_connected_at: conn.last_connected_at ?? null
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
async function fetchConnectedLogins(profile) {
|
|
1277
|
+
const list = await req("POST", "/agent/profiles/list", { profile });
|
|
1278
|
+
return list.ok && Array.isArray(list.data?.connections) ? list.data.connections.map(normalizeLogin) : [];
|
|
1279
|
+
}
|
|
1280
|
+
server2.registerTool(
|
|
1281
|
+
"browser_profile_connect",
|
|
1282
|
+
{
|
|
1283
|
+
title: "Save a Site Login to a Profile",
|
|
1284
|
+
description: `Save a logged-in browser session so this server can reuse a site the user is signed into (ChatGPT, Claude, Reddit, or any account-gated site). Returns an mcpscraper.dev watch_url \u2014 give it to the user, they sign in once, and the cookies are saved to a named profile for later browser_open calls. ONE profile holds MANY logins: call this again with the SAME profile and a DIFFERENT domain to stack another account (e.g. add claude.ai to a profile that already has chatgpt.com). Pass note to record what each login is. Side effects: opens a short-lived sign-in session and persists cookies to the profile; it does NOT import the user's existing browser cookies (they log in fresh). NOT for one-off scraping of public pages (use extract_url) and NOT a way to drive the browser (use browser_open). Sample asks: "save my ChatGPT login", "connect my Reddit account", "add my Claude login to the same profile". After the user signs in, poll browser_profile_list until the login reads AUTHENTICATED, then browser_open with the profile.`,
|
|
1285
|
+
inputSchema: BrowserProfileConnectInputSchema,
|
|
1286
|
+
outputSchema: BrowserProfileConnectOutputSchema,
|
|
1287
|
+
annotations: annotations("Save a Site Login to a Profile")
|
|
1288
|
+
},
|
|
1289
|
+
async (input) => {
|
|
1290
|
+
const domain = slugPart(input.domain) || "chatgpt.com";
|
|
1291
|
+
const setupUrl = input.login_url ?? input.url ?? (domain === "chatgpt.com" ? "https://chatgpt.com/" : `https://${domain}/`);
|
|
1292
|
+
const profile = input.profile?.trim() || savedProfileNameFromEmail(input.email) || browserServiceProfileName();
|
|
1293
|
+
if (!profile) {
|
|
1294
|
+
return errorResult("browser_profile_connect", {
|
|
1295
|
+
error: "profile or email is required when BROWSER_AGENT_PROFILE_NAME is not available"
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
const accountEmail = input.email?.trim() || null;
|
|
1299
|
+
const note = input.note?.trim() || null;
|
|
1300
|
+
const open = await req("POST", "/agent/profiles/onboard", {
|
|
1301
|
+
label: input.label ?? `Login setup: ${domain} \u2192 ${profile}`,
|
|
1302
|
+
profile,
|
|
1303
|
+
domain,
|
|
1304
|
+
login_url: setupUrl,
|
|
1305
|
+
...accountEmail ? { account_email: accountEmail } : {},
|
|
1306
|
+
...note ? { note } : {}
|
|
1307
|
+
});
|
|
1308
|
+
if (!open.ok) return errorResult("browser_profile_connect", open.data);
|
|
1309
|
+
const connectedLogins = await fetchConnectedLogins(profile).catch(() => []);
|
|
1310
|
+
return structuredResult({
|
|
1311
|
+
ok: true,
|
|
1312
|
+
tool: "browser_profile_connect",
|
|
1313
|
+
session_id: null,
|
|
1314
|
+
auth_connection_id: open.data.connection_id,
|
|
1315
|
+
watch_url: `${consoleBase}/console/auth/${open.data.connection_id}`,
|
|
1316
|
+
live_view_url: null,
|
|
1317
|
+
profile,
|
|
1318
|
+
domain,
|
|
1319
|
+
setup_url: setupUrl,
|
|
1320
|
+
account_email: accountEmail,
|
|
1321
|
+
note,
|
|
1322
|
+
status: open.data.status ?? null,
|
|
1323
|
+
flow_status: open.data.flow_status ?? null,
|
|
1324
|
+
flow_step: open.data.flow_step ?? null,
|
|
1325
|
+
flow_expires_at: open.data.flow_expires_at ?? null,
|
|
1326
|
+
post_login_url: open.data.post_login_url ?? null,
|
|
1327
|
+
connected_logins: connectedLogins,
|
|
1328
|
+
next_steps: [
|
|
1329
|
+
`Give the user watch_url so they can sign in to ${domain}.`,
|
|
1330
|
+
"Poll browser_profile_list (with this profile) until the login reads AUTHENTICATED.",
|
|
1331
|
+
"To add another account to this same profile, call browser_profile_connect again with the same profile and a different domain.",
|
|
1332
|
+
"When authenticated, call browser_open with this profile to drive the logged-in session."
|
|
1333
|
+
],
|
|
1334
|
+
raw: open.data
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
);
|
|
1338
|
+
server2.registerTool(
|
|
1339
|
+
"browser_profile_list",
|
|
1340
|
+
{
|
|
1341
|
+
title: "List Saved Logins in a Profile",
|
|
1342
|
+
description: `List every site login saved in a profile, each with its current auth status (NEEDS_AUTH / AUTHENTICATED), account email, and note. Use it to (1) see what a profile is connected to before opening a session, and (2) poll a just-saved login until it flips to AUTHENTICATED. Read-only, no cost. Pass profile (or email to derive it); narrow with domain or connection_id to poll a single login. Sample asks: "what's connected in my profile", "is my ChatGPT login ready yet", "which accounts are saved". Pairs with browser_profile_connect (which adds logins) and browser_open (which uses them).`,
|
|
1343
|
+
inputSchema: BrowserProfileListInputSchema,
|
|
1344
|
+
outputSchema: BrowserProfileListOutputSchema,
|
|
1345
|
+
annotations: annotations("List Saved Logins in a Profile", true)
|
|
1346
|
+
},
|
|
1347
|
+
async (input) => {
|
|
1348
|
+
const profile = input.profile?.trim() || savedProfileNameFromEmail(input.email) || browserServiceProfileName();
|
|
1349
|
+
if (!profile) {
|
|
1350
|
+
return errorResult("browser_profile_list", {
|
|
1351
|
+
error: "profile or email is required when BROWSER_AGENT_PROFILE_NAME is not available"
|
|
1352
|
+
});
|
|
1353
|
+
}
|
|
1354
|
+
const domain = slugPart(input.domain) || void 0;
|
|
1355
|
+
const res = await req("POST", "/agent/profiles/list", {
|
|
1356
|
+
profile,
|
|
1357
|
+
...domain ? { domain } : {},
|
|
1358
|
+
...input.connection_id ? { connection_id: input.connection_id } : {}
|
|
1359
|
+
});
|
|
1360
|
+
if (!res.ok) return errorResult("browser_profile_list", res.data);
|
|
1361
|
+
const connections = (Array.isArray(res.data?.connections) ? res.data.connections : []).map(normalizeLogin);
|
|
1362
|
+
return structuredResult({
|
|
1363
|
+
ok: true,
|
|
1364
|
+
tool: "browser_profile_list",
|
|
1365
|
+
session_id: null,
|
|
1366
|
+
profile,
|
|
1367
|
+
connections,
|
|
1368
|
+
count: connections.length
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
);
|
|
1372
|
+
server2.registerTool(
|
|
1373
|
+
"browser_open",
|
|
1374
|
+
{
|
|
1375
|
+
title: "Open Browser Session",
|
|
1376
|
+
description: "Open a direct no-proxy hosted browser session you can drive. Pass a saved profile name to load a session that is already logged into the sites saved in that profile (one profile can hold several logins). To set up or refresh a login first, use browser_profile_connect, then poll browser_profile_list until it reads AUTHENTICATED. Returns a session_id used by all other browser_* tools.",
|
|
1377
|
+
inputSchema: BrowserOpenInputSchema,
|
|
1378
|
+
outputSchema: BrowserOpenOutputSchema,
|
|
1379
|
+
annotations: annotations("Open Browser Session")
|
|
1380
|
+
},
|
|
1381
|
+
async (input) => {
|
|
1382
|
+
const profile = input.profile ?? browserServiceProfileName();
|
|
1383
|
+
const saveProfileChanges = input.save_profile_changes ?? browserServiceProfileSaveChanges();
|
|
1384
|
+
const open = await req("POST", "/agent/sessions", {
|
|
1385
|
+
label: input.label,
|
|
1386
|
+
...profile ? { profile } : {},
|
|
1387
|
+
...profile && typeof saveProfileChanges === "boolean" ? { save_profile_changes: saveProfileChanges } : {},
|
|
1388
|
+
disable_default_proxy: true,
|
|
1389
|
+
timeout_seconds: input.timeout_seconds,
|
|
1390
|
+
...input.url ? { url: input.url } : {}
|
|
1391
|
+
});
|
|
1392
|
+
if (!open.ok) return errorResult("browser_open", open.data);
|
|
1393
|
+
const session = open.data;
|
|
1394
|
+
return structuredResult({
|
|
1395
|
+
ok: true,
|
|
1396
|
+
tool: "browser_open",
|
|
1397
|
+
session_id: session.session_id,
|
|
1398
|
+
watch_url: `${consoleBase}/console/${session.session_id}`,
|
|
1399
|
+
live_view_url: null,
|
|
1400
|
+
url: input.url ?? null,
|
|
1401
|
+
hint: "Call browser_screenshot to see the page. Click by the x,y of an element from the snapshot.",
|
|
1402
|
+
raw: session
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
);
|
|
1406
|
+
server2.registerTool(
|
|
1407
|
+
"browser_screenshot",
|
|
1408
|
+
{
|
|
1409
|
+
title: "See Page (Screenshot + Elements)",
|
|
1410
|
+
description: "Capture what the browser currently shows. Returns a screenshot image PLUS a text snapshot listing interactive elements with their center x,y coordinates, the page url and title, and visible text. This is your primary way to perceive the page. Click elements by their listed x,y. If a Cloudflare/CAPTCHA challenge is visible, wait and screenshot again rather than clicking it.",
|
|
1411
|
+
inputSchema: BrowserSessionInputSchema,
|
|
1412
|
+
outputSchema: BrowserScreenshotOutputSchema,
|
|
1413
|
+
annotations: annotations("See Page", true)
|
|
1414
|
+
},
|
|
1415
|
+
async (input) => {
|
|
1416
|
+
const res = await req("POST", `/agent/sessions/${input.session_id}/screenshot`);
|
|
1417
|
+
if (!res.ok) return errorResult("browser_screenshot", res.data, input.session_id);
|
|
1418
|
+
const { image_base64, mime_type, url, title, elements, text } = res.data;
|
|
1419
|
+
const content = [];
|
|
1420
|
+
if (image_base64) content.push({ type: "image", data: image_base64, mimeType: mime_type ?? "image/png" });
|
|
1421
|
+
const structured = {
|
|
1422
|
+
ok: true,
|
|
1423
|
+
tool: "browser_screenshot",
|
|
1424
|
+
session_id: input.session_id,
|
|
1425
|
+
url: url ?? null,
|
|
1426
|
+
title: title ?? null,
|
|
1427
|
+
text: typeof text === "string" ? text : "",
|
|
1428
|
+
elements: Array.isArray(elements) ? elements : [],
|
|
1429
|
+
screenshot: image_base64 ? { mime_type: mime_type ?? "image/png", inline: true } : null
|
|
1430
|
+
};
|
|
1431
|
+
content.push({
|
|
1432
|
+
type: "text",
|
|
1433
|
+
text: JSON.stringify(structured)
|
|
1434
|
+
});
|
|
1435
|
+
return { content, structuredContent: structured };
|
|
1436
|
+
}
|
|
1437
|
+
);
|
|
1438
|
+
server2.registerTool(
|
|
1439
|
+
"browser_read",
|
|
1440
|
+
{
|
|
1441
|
+
title: "Read Page Text + Elements",
|
|
1442
|
+
description: "Return the page url, title, visible text, and the list of interactive elements (with x,y) without an image. Cheaper than browser_screenshot when you only need to read content or find a target element to click.",
|
|
1443
|
+
inputSchema: BrowserSessionInputSchema,
|
|
1444
|
+
outputSchema: BrowserReadOutputSchema,
|
|
1445
|
+
annotations: annotations("Read Page", true)
|
|
1446
|
+
},
|
|
1447
|
+
async (input) => {
|
|
1448
|
+
const res = await req("POST", `/agent/sessions/${input.session_id}/read`);
|
|
1449
|
+
if (!res.ok) return errorResult("browser_read", res.data, input.session_id);
|
|
1450
|
+
return structuredResult({
|
|
1451
|
+
ok: true,
|
|
1452
|
+
tool: "browser_read",
|
|
1453
|
+
session_id: input.session_id,
|
|
1454
|
+
url: res.data?.url ?? null,
|
|
1455
|
+
title: res.data?.title ?? null,
|
|
1456
|
+
text: typeof res.data?.text === "string" ? res.data.text : "",
|
|
1457
|
+
elements: Array.isArray(res.data?.elements) ? res.data.elements : [],
|
|
1458
|
+
raw: res.data
|
|
1459
|
+
});
|
|
1460
|
+
}
|
|
1461
|
+
);
|
|
1462
|
+
server2.registerTool(
|
|
1463
|
+
"browser_locate",
|
|
1464
|
+
{
|
|
1465
|
+
title: "Locate DOM Targets",
|
|
1466
|
+
description: "Locate exact visible DOM elements or text ranges in the current browser viewport and return left/top/width/height bounds in screenshot pixels. Use this before drawing annotations or when a callout must literally circle, box, underline, or point to a real element. Prefer CSS selectors for exact UI elements; use text when selector is unknown. When a replay is actively recording, the result includes replay_elapsed_seconds for timing.",
|
|
1467
|
+
inputSchema: BrowserLocateInputSchema,
|
|
1468
|
+
outputSchema: BrowserLocateOutputSchema,
|
|
1469
|
+
annotations: annotations("Locate DOM Targets", true)
|
|
1470
|
+
},
|
|
1471
|
+
async (input) => {
|
|
1472
|
+
const res = await req("POST", `/agent/sessions/${input.session_id}/locate`, { targets: input.targets });
|
|
1473
|
+
if (!res.ok) return errorResult("browser_locate", res.data, input.session_id);
|
|
1474
|
+
return structuredResult({
|
|
1475
|
+
ok: true,
|
|
1476
|
+
tool: "browser_locate",
|
|
1477
|
+
session_id: input.session_id,
|
|
1478
|
+
url: res.data?.url ?? null,
|
|
1479
|
+
title: res.data?.title ?? null,
|
|
1480
|
+
viewport: res.data?.viewport ?? null,
|
|
1481
|
+
replay: res.data?.replay ?? null,
|
|
1482
|
+
targets: Array.isArray(res.data?.targets) ? res.data.targets : [],
|
|
1483
|
+
raw: res.data
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
);
|
|
1487
|
+
server2.registerTool(
|
|
1488
|
+
"browser_goto",
|
|
1489
|
+
{
|
|
1490
|
+
title: "Navigate To URL",
|
|
1491
|
+
description: "Navigate an existing browser session to a URL when the user asks you to go to another page or continue browsing in the same session. Use browser_open first if no session exists. Follow with browser_screenshot to see redirects, login walls, CAPTCHA/Cloudflare state, or the loaded page.",
|
|
1492
|
+
inputSchema: BrowserGotoInputSchema,
|
|
1493
|
+
outputSchema: BrowserActionOutputSchema,
|
|
1494
|
+
annotations: annotations("Navigate To URL")
|
|
1495
|
+
},
|
|
1496
|
+
async (input) => {
|
|
1497
|
+
const res = await req("POST", `/agent/sessions/${input.session_id}/goto`, { url: input.url });
|
|
1498
|
+
return actionResult("browser_goto", input.session_id, res.ok, res.data, "browser_screenshot");
|
|
1499
|
+
}
|
|
1500
|
+
);
|
|
1501
|
+
server2.registerTool(
|
|
1502
|
+
"browser_click",
|
|
1503
|
+
{
|
|
1504
|
+
title: "Click",
|
|
1505
|
+
description: "Click a visible page target using screenshot pixel coordinates. Use this when the user asks you to press a button, open a menu, choose a result, or activate a visible UI element. Use x/y only from the latest browser_screenshot, browser_read, or browser_locate result; do not guess coordinates. Follow with browser_screenshot when the click may change the page.",
|
|
1506
|
+
inputSchema: BrowserClickInputSchema,
|
|
1507
|
+
outputSchema: BrowserActionOutputSchema,
|
|
1508
|
+
annotations: annotations("Click")
|
|
1509
|
+
},
|
|
1510
|
+
async (input) => {
|
|
1511
|
+
const res = await req("POST", `/agent/sessions/${input.session_id}/click`, {
|
|
1512
|
+
x: input.x,
|
|
1513
|
+
y: input.y,
|
|
1514
|
+
button: input.button,
|
|
1515
|
+
num_clicks: input.num_clicks
|
|
1516
|
+
});
|
|
1517
|
+
return actionResult("browser_click", input.session_id, res.ok, res.data, "browser_screenshot");
|
|
1518
|
+
}
|
|
1519
|
+
);
|
|
1520
|
+
server2.registerTool(
|
|
1521
|
+
"browser_type",
|
|
1522
|
+
{
|
|
1523
|
+
title: "Type Text",
|
|
1524
|
+
description: 'Type text into the currently focused browser field. Use this when the user asks you to fill a search box, form field, login field, or editable text area. Click or Tab to the field first if focus is uncertain. Use browser_press with ["Return"] to submit, and browser_screenshot afterward when page state matters.',
|
|
1525
|
+
inputSchema: BrowserTypeInputSchema,
|
|
1526
|
+
outputSchema: BrowserActionOutputSchema,
|
|
1527
|
+
annotations: annotations("Type Text")
|
|
1528
|
+
},
|
|
1529
|
+
async (input) => {
|
|
1530
|
+
const res = await req("POST", `/agent/sessions/${input.session_id}/type`, { text: input.text, delay: input.delay });
|
|
1531
|
+
return actionResult("browser_type", input.session_id, res.ok, res.data, "browser_screenshot");
|
|
1532
|
+
}
|
|
1533
|
+
);
|
|
1534
|
+
server2.registerTool(
|
|
1535
|
+
"browser_scroll",
|
|
1536
|
+
{
|
|
1537
|
+
title: "Scroll",
|
|
1538
|
+
description: "Scroll the page to reveal more content before reading, clicking, or locating elements. Positive delta_y scrolls down; negative delta_y scrolls up. Follow with browser_screenshot or browser_read to inspect newly revealed content.",
|
|
1539
|
+
inputSchema: BrowserScrollInputSchema,
|
|
1540
|
+
outputSchema: BrowserActionOutputSchema,
|
|
1541
|
+
annotations: annotations("Scroll")
|
|
1542
|
+
},
|
|
1543
|
+
async (input) => {
|
|
1544
|
+
const res = await req("POST", `/agent/sessions/${input.session_id}/scroll`, {
|
|
1545
|
+
delta_y: input.delta_y,
|
|
1546
|
+
delta_x: input.delta_x,
|
|
1547
|
+
x: input.x,
|
|
1548
|
+
y: input.y
|
|
1549
|
+
});
|
|
1550
|
+
return actionResult("browser_scroll", input.session_id, res.ok, res.data, "browser_screenshot");
|
|
1551
|
+
}
|
|
1552
|
+
);
|
|
1553
|
+
server2.registerTool(
|
|
1554
|
+
"browser_press",
|
|
1555
|
+
{
|
|
1556
|
+
title: "Press Keys",
|
|
1557
|
+
description: 'Press keyboard keys or combinations in the active browser session. Use this for submit, Escape, Tab navigation, select-all, or keyboard shortcuts. Examples: ["Return"], ["Escape"], ["Tab"], ["Ctrl+a"], ["Ctrl+Shift+Tab"]. Use browser_type for text entry and browser_screenshot after keypresses that may change the page.',
|
|
1558
|
+
inputSchema: BrowserPressInputSchema,
|
|
1559
|
+
outputSchema: BrowserActionOutputSchema,
|
|
1560
|
+
annotations: annotations("Press Keys")
|
|
1561
|
+
},
|
|
1562
|
+
async (input) => {
|
|
1563
|
+
const res = await req("POST", `/agent/sessions/${input.session_id}/press`, { keys: input.keys });
|
|
1564
|
+
return actionResult("browser_press", input.session_id, res.ok, res.data, "browser_screenshot");
|
|
1565
|
+
}
|
|
1566
|
+
);
|
|
1567
|
+
server2.registerTool(
|
|
1568
|
+
"browser_replay_start",
|
|
1569
|
+
{
|
|
1570
|
+
title: "Start Recording",
|
|
1571
|
+
description: "Start recording an MP4 replay of the session. Returns replay_id, view_url when available, and a download_url. Use to capture a task for later review; stop with browser_replay_stop.",
|
|
1572
|
+
inputSchema: BrowserSessionInputSchema,
|
|
1573
|
+
outputSchema: BrowserReplayStartOutputSchema,
|
|
1574
|
+
annotations: annotations("Start Recording")
|
|
1575
|
+
},
|
|
1576
|
+
async (input) => {
|
|
1577
|
+
const res = await req("POST", `/agent/sessions/${input.session_id}/replay/start`);
|
|
1578
|
+
if (!res.ok) return errorResult("browser_replay_start", res.data, input.session_id);
|
|
1579
|
+
return structuredResult({
|
|
1580
|
+
ok: true,
|
|
1581
|
+
tool: "browser_replay_start",
|
|
1582
|
+
session_id: input.session_id,
|
|
1583
|
+
replay_id: res.data?.replay_id ?? res.data?.replayId ?? null,
|
|
1584
|
+
view_url: res.data?.view_url ?? res.data?.viewUrl ?? null,
|
|
1585
|
+
download_url: res.data?.download_url ?? res.data?.downloadUrl ?? null,
|
|
1586
|
+
raw: res.data
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
);
|
|
1590
|
+
server2.registerTool(
|
|
1591
|
+
"browser_replay_stop",
|
|
1592
|
+
{
|
|
1593
|
+
title: "Stop Recording",
|
|
1594
|
+
description: "Stop a replay recording and expose the final view_url and download_url. Use browser_replay_download to save the MP4 to the configured output directory.",
|
|
1595
|
+
inputSchema: BrowserReplayStopInputSchema,
|
|
1596
|
+
outputSchema: BrowserReplayStopOutputSchema,
|
|
1597
|
+
annotations: annotations("Stop Recording")
|
|
1598
|
+
},
|
|
1599
|
+
async (input) => {
|
|
1600
|
+
const res = await req("POST", `/agent/sessions/${input.session_id}/replay/stop`, { replay_id: input.replay_id });
|
|
1601
|
+
if (!res.ok) return errorResult("browser_replay_stop", res.data, input.session_id, input.replay_id);
|
|
1602
|
+
return structuredResult({
|
|
1603
|
+
ok: true,
|
|
1604
|
+
tool: "browser_replay_stop",
|
|
1605
|
+
session_id: input.session_id,
|
|
1606
|
+
replay_id: res.data?.replay_id ?? res.data?.replayId ?? input.replay_id,
|
|
1607
|
+
view_url: res.data?.view_url ?? res.data?.viewUrl ?? null,
|
|
1608
|
+
download_url: res.data?.download_url ?? res.data?.downloadUrl ?? null,
|
|
1609
|
+
raw: res.data
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
);
|
|
1613
|
+
server2.registerTool(
|
|
1614
|
+
"browser_list_replays",
|
|
1615
|
+
{
|
|
1616
|
+
title: "List Replay Videos",
|
|
1617
|
+
description: "List replay recordings for a browser session, including final view_url and authenticated download_url values when available.",
|
|
1618
|
+
inputSchema: BrowserSessionInputSchema,
|
|
1619
|
+
outputSchema: BrowserListReplaysOutputSchema,
|
|
1620
|
+
annotations: annotations("List Replay Videos", true)
|
|
1621
|
+
},
|
|
1622
|
+
async (input) => {
|
|
1623
|
+
const res = await req("GET", `/agent/sessions/${input.session_id}/replays`);
|
|
1624
|
+
if (!res.ok) return errorResult("browser_list_replays", res.data, input.session_id);
|
|
1625
|
+
const replays = Array.isArray(res.data?.replays) ? res.data.replays : [];
|
|
1626
|
+
return structuredResult({
|
|
1627
|
+
ok: true,
|
|
1628
|
+
tool: "browser_list_replays",
|
|
1629
|
+
session_id: input.session_id,
|
|
1630
|
+
replays,
|
|
1631
|
+
count: replays.length
|
|
1632
|
+
});
|
|
1633
|
+
}
|
|
1634
|
+
);
|
|
1635
|
+
server2.registerTool(
|
|
1636
|
+
"browser_replay_download",
|
|
1637
|
+
{
|
|
1638
|
+
title: "Download Replay MP4",
|
|
1639
|
+
description: "Download a replay recording through MCP Scraper and save the MP4 under MCP_SCRAPER_OUTPUT_DIR/browser-replays. Use after browser_replay_stop or browser_list_replays.",
|
|
1640
|
+
inputSchema: BrowserReplayDownloadInputSchema,
|
|
1641
|
+
outputSchema: BrowserReplayDownloadOutputSchema,
|
|
1642
|
+
annotations: annotations("Download Replay MP4")
|
|
1643
|
+
},
|
|
1644
|
+
async (input) => {
|
|
1645
|
+
const res = await downloadReplay(input.session_id, input.replay_id, input.filename);
|
|
1646
|
+
if (!res.ok) return errorResult("browser_replay_download", res.data, input.session_id, input.replay_id);
|
|
1647
|
+
return structuredResult({
|
|
1648
|
+
ok: true,
|
|
1649
|
+
tool: "browser_replay_download",
|
|
1650
|
+
session_id: input.session_id,
|
|
1651
|
+
replay_id: input.replay_id,
|
|
1652
|
+
file_path: res.data?.file_path ?? null,
|
|
1653
|
+
bytes: typeof res.data?.bytes === "number" ? res.data.bytes : null,
|
|
1654
|
+
mime_type: res.data?.mime_type ?? null,
|
|
1655
|
+
download_url: res.data?.download_url ?? null
|
|
1656
|
+
});
|
|
1657
|
+
}
|
|
1658
|
+
);
|
|
1659
|
+
server2.registerTool(
|
|
1660
|
+
"browser_replay_mark",
|
|
1661
|
+
{
|
|
1662
|
+
title: "Mark Replay Annotation",
|
|
1663
|
+
description: "While a replay is actively recording, locate one exact DOM target and return a ready-to-use annotation object with DOM bounds and replay-relative timing. Use this instead of guessing start_seconds or drawing rough rectangles. Workflow: start browser_replay_start, navigate until the target is visible and stable, call browser_replay_mark for each callout, then stop the replay and pass the returned annotations to browser_replay_annotate.",
|
|
1664
|
+
inputSchema: BrowserReplayMarkInputSchema,
|
|
1665
|
+
outputSchema: BrowserReplayMarkOutputSchema,
|
|
1666
|
+
annotations: annotations("Mark Replay Annotation", true)
|
|
1667
|
+
},
|
|
1668
|
+
async (input) => {
|
|
1669
|
+
const res = await req("POST", `/agent/sessions/${input.session_id}/locate`, { targets: [input.target] });
|
|
1670
|
+
if (!res.ok) return errorResult("browser_replay_mark", res.data, input.session_id);
|
|
1671
|
+
const target = res.data?.targets?.[0];
|
|
1672
|
+
const element = target?.element;
|
|
1673
|
+
const elapsed = res.data?.replay?.replay_elapsed_seconds;
|
|
1674
|
+
if (!target?.found || !element) {
|
|
1675
|
+
return errorResult("browser_replay_mark", { error: target?.error ?? "target not found in current viewport", target }, input.session_id);
|
|
1676
|
+
}
|
|
1677
|
+
if (!finiteNumber2(elapsed)) {
|
|
1678
|
+
return errorResult("browser_replay_mark", { error: "no active replay clock found; call browser_replay_start before browser_replay_mark" }, input.session_id);
|
|
1679
|
+
}
|
|
1680
|
+
const padded = expandElementBounds(element, res.data?.viewport, input.padding ?? 8);
|
|
1681
|
+
const start = Math.max(0, elapsed + (input.start_offset_seconds ?? -0.25));
|
|
1682
|
+
const duration = input.duration_seconds ?? 4;
|
|
1683
|
+
const annotation = {
|
|
1684
|
+
type: input.type ?? "box",
|
|
1685
|
+
start_seconds: Number(start.toFixed(3)),
|
|
1686
|
+
end_seconds: Number((start + duration).toFixed(3)),
|
|
1687
|
+
left: Math.round(padded.left),
|
|
1688
|
+
top: Math.round(padded.top),
|
|
1689
|
+
width: Math.round(padded.width),
|
|
1690
|
+
height: Math.round(padded.height),
|
|
1691
|
+
...input.label ? { label: input.label } : {},
|
|
1692
|
+
...input.color ? { color: input.color } : {},
|
|
1693
|
+
...input.thickness ? { thickness: input.thickness } : {}
|
|
1694
|
+
};
|
|
1695
|
+
return structuredResult({
|
|
1696
|
+
ok: true,
|
|
1697
|
+
tool: "browser_replay_mark",
|
|
1698
|
+
session_id: input.session_id,
|
|
1699
|
+
replay_id: res.data?.replay?.replay_id ?? res.data?.replay?.replayId ?? null,
|
|
1700
|
+
annotation,
|
|
1701
|
+
source_width: padded.sourceWidth,
|
|
1702
|
+
source_height: padded.sourceHeight,
|
|
1703
|
+
replay: res.data.replay,
|
|
1704
|
+
target,
|
|
1705
|
+
hint: "Append annotation to your annotations array. Use source_width/source_height with browser_replay_annotate."
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1708
|
+
);
|
|
1709
|
+
server2.registerTool(
|
|
1710
|
+
"browser_replay_annotate",
|
|
1711
|
+
{
|
|
1712
|
+
title: "Annotate Replay MP4",
|
|
1713
|
+
description: "Download a browser replay MP4, render visual annotations over it, and save a new annotated MP4 under MCP_SCRAPER_OUTPUT_DIR/browser-replays. Use this after browser_replay_stop when the user wants proof videos with circles, boxes, arrows, underlines, or labels. For accurate timing and placement, prefer annotations returned by browser_replay_mark while the replay is recording; otherwise use exact left/top/width/height bounds from browser_locate. If the replay video size differs from the screenshot coordinate space, pass source_width and source_height.",
|
|
1714
|
+
inputSchema: BrowserReplayAnnotateInputSchema,
|
|
1715
|
+
outputSchema: BrowserReplayAnnotateOutputSchema,
|
|
1716
|
+
annotations: annotations("Annotate Replay MP4")
|
|
1717
|
+
},
|
|
1718
|
+
async (input) => {
|
|
1719
|
+
const sourceName = input.filename ? `${input.filename}-source` : void 0;
|
|
1720
|
+
const downloaded = await downloadReplay(input.session_id, input.replay_id, sourceName);
|
|
1721
|
+
if (!downloaded.ok) return errorResult("browser_replay_annotate", downloaded.data, input.session_id, input.replay_id);
|
|
1722
|
+
try {
|
|
1723
|
+
const sourcePath = String(downloaded.data.file_path);
|
|
1724
|
+
const outputPath = annotatedReplayFilePath(input.session_id, input.replay_id, input.filename);
|
|
1725
|
+
(0, import_node_fs2.mkdirSync)((0, import_node_path3.join)(outputBaseDir2(), "browser-replays"), { recursive: true });
|
|
1726
|
+
const result = await annotateReplayVideo(sourcePath, outputPath, {
|
|
1727
|
+
annotations: input.annotations,
|
|
1728
|
+
sourceWidth: input.source_width,
|
|
1729
|
+
sourceHeight: input.source_height,
|
|
1730
|
+
sourceLeftOffset: input.source_left_offset,
|
|
1731
|
+
sourceTopOffset: input.source_top_offset
|
|
1732
|
+
});
|
|
1733
|
+
return structuredResult({
|
|
1734
|
+
ok: true,
|
|
1735
|
+
tool: "browser_replay_annotate",
|
|
1736
|
+
session_id: input.session_id,
|
|
1737
|
+
replay_id: input.replay_id,
|
|
1738
|
+
source_file_path: sourcePath,
|
|
1739
|
+
annotated_file_path: result.filePath,
|
|
1740
|
+
bytes: result.bytes,
|
|
1741
|
+
width: result.width,
|
|
1742
|
+
height: result.height,
|
|
1743
|
+
annotation_count: result.annotationCount,
|
|
1744
|
+
mime_type: "video/mp4"
|
|
1745
|
+
});
|
|
1746
|
+
} catch (err) {
|
|
1747
|
+
return errorResult("browser_replay_annotate", { error: err instanceof Error ? err.message : String(err) }, input.session_id, input.replay_id);
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
);
|
|
1751
|
+
server2.registerTool(
|
|
1752
|
+
"browser_close",
|
|
1753
|
+
{
|
|
1754
|
+
title: "Close Browser Session",
|
|
1755
|
+
description: "Close and release a browser session when the task is done, when the user asks to stop the browser, or when active browser billing should end. Use browser_list_sessions first if you need to recover the session_id.",
|
|
1756
|
+
inputSchema: BrowserSessionInputSchema,
|
|
1757
|
+
outputSchema: BrowserCloseOutputSchema,
|
|
1758
|
+
annotations: { title: "Close Browser Session", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
|
|
1759
|
+
},
|
|
1760
|
+
async (input) => {
|
|
1761
|
+
const res = await req("DELETE", `/agent/sessions/${input.session_id}`);
|
|
1762
|
+
if (!res.ok) return errorResult("browser_close", res.data, input.session_id);
|
|
1763
|
+
return structuredResult({
|
|
1764
|
+
ok: true,
|
|
1765
|
+
tool: "browser_close",
|
|
1766
|
+
session_id: input.session_id,
|
|
1767
|
+
closed: true,
|
|
1768
|
+
raw: res.data
|
|
1769
|
+
});
|
|
1770
|
+
}
|
|
1771
|
+
);
|
|
1772
|
+
server2.registerTool(
|
|
1773
|
+
"browser_list_sessions",
|
|
1774
|
+
{
|
|
1775
|
+
title: "List Browser Sessions",
|
|
1776
|
+
description: "List browser sessions and their status, with a watch_url for each. Use this to recover a session_id, find an active browser, or decide which session to close. Use browser_open to create a new session.",
|
|
1777
|
+
inputSchema: BrowserListInputSchema,
|
|
1778
|
+
outputSchema: BrowserListSessionsOutputSchema,
|
|
1779
|
+
annotations: annotations("List Browser Sessions", true)
|
|
1780
|
+
},
|
|
1781
|
+
async (input) => {
|
|
1782
|
+
const res = await req("GET", `/agent/sessions${input.include_closed ? "?all=1" : ""}`);
|
|
1783
|
+
if (!res.ok) return errorResult("browser_list_sessions", res.data);
|
|
1784
|
+
const sessions = (res.data.sessions ?? []).map((s) => ({ ...s, watch_url: `${consoleBase}/console/${s.session_id}` }));
|
|
1785
|
+
return structuredResult({
|
|
1786
|
+
ok: true,
|
|
1787
|
+
tool: "browser_list_sessions",
|
|
1788
|
+
session_id: null,
|
|
1789
|
+
sessions,
|
|
1790
|
+
count: sessions.length
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1793
|
+
);
|
|
1794
|
+
server2.registerTool(
|
|
1795
|
+
"query_fanout_workflow",
|
|
1796
|
+
{
|
|
1797
|
+
title: "Capture AI Search Fan-Out",
|
|
1798
|
+
description: `Capture the query fan-out behind a ChatGPT or Claude web-search answer for Answer Engine Optimization (AEO): the exact sub-queries the model issued, every researched URL split into cited vs browsed-only (with citation frequency and snippet), each source tagged by category (First-party/vendor, News/media, Reddit, Social/video, Encyclopedia, Review site, Docs, Blog), plus top sourced sites and citation order. Set export=true for durable JSON, CSV, TSV, and HTML artifacts; export paths are returned relative to MCP_SCRAPER_OUTPUT_DIR. This returns the raw structured data; YOU analyze it \u2014 classify each sub-query by funnel stage (Problem-aware, Solution-aware, Decision-aware, Retention) and type (BoFu, Branded, Comparison, How-to, Search operator), name the brands the model researched, and give AEO insights. Sample asks: "capture the fan-out for this ChatGPT answer", "what did Claude search and cite for best CRM", "show the sub-queries and sources behind this AI answer". WRITE NOTE: passing prompt submits a real message in the user's logged-in account (a new conversation turn) \u2014 only send when the user wants that; omit prompt to capture a prompt the user just ran. Setup, if not already connected or if the profile disconnected: call browser_profile_connect, give the user the watch_url, let them complete login, then poll browser_profile_list until status is AUTHENTICATED. After that, browser_open the saved direct no-proxy profile and go to chatgpt.com or claude.ai. Fan-out is captured only as it streams, so the session must be open when the prompt runs. NOT for Google AI Overview citations \u2014 use harvest_paa for those; this tool is ChatGPT and Claude only.`,
|
|
1799
|
+
inputSchema: BrowserCaptureFanoutInputSchema,
|
|
1800
|
+
outputSchema: BrowserCaptureFanoutOutputSchema,
|
|
1801
|
+
annotations: annotations("Capture AI Search Fan-Out")
|
|
1802
|
+
},
|
|
1803
|
+
async (input) => {
|
|
1804
|
+
const emit = (result, exports3) => structuredResult({
|
|
1805
|
+
ok: true,
|
|
1806
|
+
tool: "query_fanout_workflow",
|
|
1807
|
+
session_id: input.session_id,
|
|
1808
|
+
platform: result.platform,
|
|
1809
|
+
captured_at: result.capturedAt,
|
|
1810
|
+
prompt: result.prompt,
|
|
1811
|
+
meta: result.meta,
|
|
1812
|
+
queries: result.queries,
|
|
1813
|
+
browsed_urls: result.browsedUrls,
|
|
1814
|
+
cited_urls: result.citedUrls,
|
|
1815
|
+
browsed_only: result.browsedOnly,
|
|
1816
|
+
snippets: result.snippets,
|
|
1817
|
+
counts: result.counts,
|
|
1818
|
+
aggregates: result.aggregates,
|
|
1819
|
+
first_party_domain: result.firstPartyDomain,
|
|
1820
|
+
exports: exports3 ?? null,
|
|
1821
|
+
...result.debug ? { debug: result.debug } : {}
|
|
1822
|
+
});
|
|
1823
|
+
const res = await req("POST", `/agent/sessions/${input.session_id}/capture-fanout`, {
|
|
1824
|
+
prompt: input.prompt,
|
|
1825
|
+
wait_ms: input.wait_ms,
|
|
1826
|
+
first_party_domain: input.first_party_domain,
|
|
1827
|
+
reset: input.reset,
|
|
1828
|
+
export: input.export
|
|
1829
|
+
}, Math.max(timeoutMs, (input.wait_ms ?? (input.prompt ? 9e4 : 8e3)) + 3e4));
|
|
1830
|
+
if (!res.ok) return errorResult("query_fanout_workflow", res.data, input.session_id);
|
|
1831
|
+
const hosted = res.data?.result ?? res.data;
|
|
1832
|
+
let exports2 = res.data?.exports ?? null;
|
|
1833
|
+
if (input.export && !exports2 && isEnrichedFanout(hosted)) {
|
|
1834
|
+
try {
|
|
1835
|
+
exports2 = exportFanout(hosted);
|
|
1836
|
+
} catch (err) {
|
|
1837
|
+
return errorResult(
|
|
1838
|
+
"query_fanout_workflow",
|
|
1839
|
+
{
|
|
1840
|
+
error: `Fan-out captured but local export failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1841
|
+
result: hosted
|
|
1842
|
+
},
|
|
1843
|
+
input.session_id
|
|
1844
|
+
);
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
return emit(hosted, exports2);
|
|
1848
|
+
}
|
|
1849
|
+
);
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
// src/mcp/paa-mcp-server.ts
|
|
1853
|
+
var import_mcp2 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
1854
|
+
var import_node_fs4 = require("fs");
|
|
1855
|
+
var import_node_path5 = require("path");
|
|
1856
|
+
|
|
1857
|
+
// src/mcp/mcp-response-formatter.ts
|
|
1858
|
+
var import_node_fs3 = require("fs");
|
|
1859
|
+
var import_node_os4 = require("os");
|
|
1860
|
+
var import_node_path4 = require("path");
|
|
1861
|
+
|
|
318
1862
|
// src/mcp/workflow-catalog.ts
|
|
319
1863
|
var WORKFLOW_RECIPES = [
|
|
320
1864
|
{
|
|
@@ -507,6 +2051,99 @@ function buildLinkGraph(pages, startUrl) {
|
|
|
507
2051
|
return { edges, metrics };
|
|
508
2052
|
}
|
|
509
2053
|
|
|
2054
|
+
// src/api/seo-link-report.ts
|
|
2055
|
+
function registrableDomain(host) {
|
|
2056
|
+
const h = host.replace(/^www\./, "").toLowerCase();
|
|
2057
|
+
const parts = h.split(".");
|
|
2058
|
+
return parts.length <= 2 ? h : parts.slice(-2).join(".");
|
|
2059
|
+
}
|
|
2060
|
+
function buildLinkReport(edges, metrics, siteUrl) {
|
|
2061
|
+
let siteReg = "";
|
|
2062
|
+
try {
|
|
2063
|
+
siteReg = registrableDomain(new URL(siteUrl).hostname);
|
|
2064
|
+
} catch {
|
|
2065
|
+
siteReg = "";
|
|
2066
|
+
}
|
|
2067
|
+
const internalEdges = edges.filter((e) => e.internal);
|
|
2068
|
+
const pages = metrics.length;
|
|
2069
|
+
const orphans = metrics.filter((m) => m.orphan).length;
|
|
2070
|
+
const brokenInternal = internalEdges.filter((e) => e.targetStatus != null && e.targetStatus >= 400).length;
|
|
2071
|
+
const sumInlinks = metrics.reduce((a, m) => a + m.inlinks, 0);
|
|
2072
|
+
const sumOutlinks = metrics.reduce((a, m) => a + m.outlinksInternal + m.outlinksExternal, 0);
|
|
2073
|
+
const distribution = { zero: 0, oneToTwo: 0, threeToTen: 0, elevenPlus: 0 };
|
|
2074
|
+
for (const m of metrics) {
|
|
2075
|
+
if (m.inlinks === 0) distribution.zero++;
|
|
2076
|
+
else if (m.inlinks <= 2) distribution.oneToTwo++;
|
|
2077
|
+
else if (m.inlinks <= 10) distribution.threeToTen++;
|
|
2078
|
+
else distribution.elevenPlus++;
|
|
2079
|
+
}
|
|
2080
|
+
const topByInlinks = [...metrics].sort((a, b) => b.inlinks - a.inlinks).slice(0, 20).map((m) => ({ url: m.url, inlinks: m.inlinks, outlinksInternal: m.outlinksInternal, outlinksExternal: m.outlinksExternal }));
|
|
2081
|
+
const domMap = /* @__PURE__ */ new Map();
|
|
2082
|
+
let externalTotal = 0;
|
|
2083
|
+
for (const e of edges) {
|
|
2084
|
+
let domain;
|
|
2085
|
+
try {
|
|
2086
|
+
domain = registrableDomain(new URL(e.to).hostname);
|
|
2087
|
+
} catch {
|
|
2088
|
+
continue;
|
|
2089
|
+
}
|
|
2090
|
+
if (!domain || domain === siteReg) continue;
|
|
2091
|
+
externalTotal++;
|
|
2092
|
+
const d = domMap.get(domain) ?? { links: 0, nofollow: 0, pages: /* @__PURE__ */ new Set() };
|
|
2093
|
+
d.links++;
|
|
2094
|
+
if (e.nofollow) d.nofollow++;
|
|
2095
|
+
d.pages.add(e.from);
|
|
2096
|
+
domMap.set(domain, d);
|
|
2097
|
+
}
|
|
2098
|
+
const externalDomains = [...domMap.entries()].map(([domain, d]) => ({ domain, links: d.links, nofollow: d.nofollow, pages: d.pages.size })).sort((a, b) => b.links - a.links);
|
|
2099
|
+
const round = (n) => Math.round(n * 10) / 10;
|
|
2100
|
+
return {
|
|
2101
|
+
summary: {
|
|
2102
|
+
internal: {
|
|
2103
|
+
totalLinks: internalEdges.length,
|
|
2104
|
+
pages,
|
|
2105
|
+
orphans,
|
|
2106
|
+
brokenInternal,
|
|
2107
|
+
avgInlinks: pages ? round(sumInlinks / pages) : 0,
|
|
2108
|
+
avgOutlinks: pages ? round(sumOutlinks / pages) : 0,
|
|
2109
|
+
distribution,
|
|
2110
|
+
topByInlinks
|
|
2111
|
+
},
|
|
2112
|
+
external: {
|
|
2113
|
+
totalLinks: externalTotal,
|
|
2114
|
+
uniqueDomains: externalDomains.length,
|
|
2115
|
+
topDomains: externalDomains.slice(0, 20)
|
|
2116
|
+
}
|
|
2117
|
+
},
|
|
2118
|
+
externalDomains
|
|
2119
|
+
};
|
|
2120
|
+
}
|
|
2121
|
+
function renderLinkReport(r) {
|
|
2122
|
+
const s = r.summary;
|
|
2123
|
+
const lines = [
|
|
2124
|
+
`## Link analysis`,
|
|
2125
|
+
`**Internal:** ${s.internal.totalLinks} links \xB7 ${s.internal.pages} pages \xB7 avg ${s.internal.avgInlinks} inlinks/page \xB7 ${s.internal.orphans} orphans \xB7 ${s.internal.brokenInternal} broken`,
|
|
2126
|
+
`**External:** ${s.external.totalLinks} links to ${s.external.uniqueDomains} domains`,
|
|
2127
|
+
``,
|
|
2128
|
+
`### Inlink distribution`,
|
|
2129
|
+
`- 0 inlinks (orphans): ${s.internal.distribution.zero}`,
|
|
2130
|
+
`- 1\u20132: ${s.internal.distribution.oneToTwo}`,
|
|
2131
|
+
`- 3\u201310: ${s.internal.distribution.threeToTen}`,
|
|
2132
|
+
`- 11+: ${s.internal.distribution.elevenPlus}`,
|
|
2133
|
+
``,
|
|
2134
|
+
`### Top 20 internal pages by inlinks`,
|
|
2135
|
+
`| inlinks | out (int/ext) | URL |`,
|
|
2136
|
+
`|---|---|---|`,
|
|
2137
|
+
...s.internal.topByInlinks.map((p) => `| ${p.inlinks} | ${p.outlinksInternal}/${p.outlinksExternal} | ${p.url} |`),
|
|
2138
|
+
``,
|
|
2139
|
+
`### Top 20 external domains by links`,
|
|
2140
|
+
`| links | nofollow | from pages | domain |`,
|
|
2141
|
+
`|---|---|---|---|`,
|
|
2142
|
+
...s.external.topDomains.map((d) => `| ${d.links} | ${d.nofollow} | ${d.pages} | ${d.domain} |`)
|
|
2143
|
+
];
|
|
2144
|
+
return lines.join("\n");
|
|
2145
|
+
}
|
|
2146
|
+
|
|
510
2147
|
// src/api/seo-issues.ts
|
|
511
2148
|
var THIN_WORDS = 200;
|
|
512
2149
|
var TITLE_MAX = 60;
|
|
@@ -760,17 +2397,17 @@ function reportTitle(full) {
|
|
|
760
2397
|
const title = full.split("\n").find((line) => line.startsWith("# "));
|
|
761
2398
|
return title?.replace(/^#\s+/, "").trim() || "MCP Scraper Report";
|
|
762
2399
|
}
|
|
763
|
-
function
|
|
764
|
-
return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0,
|
|
2400
|
+
function outputBaseDir3() {
|
|
2401
|
+
return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path4.join)((0, import_node_os4.homedir)(), "Downloads", "mcp-scraper");
|
|
765
2402
|
}
|
|
766
2403
|
function saveFullReport(full) {
|
|
767
2404
|
if (!reportSavingEnabled || process.env.MCP_SCRAPER_SAVE_REPORTS === "false") return null;
|
|
768
|
-
const outDir =
|
|
2405
|
+
const outDir = outputBaseDir3();
|
|
769
2406
|
try {
|
|
770
|
-
(0,
|
|
2407
|
+
(0, import_node_fs3.mkdirSync)(outDir, { recursive: true });
|
|
771
2408
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
772
|
-
const file = (0,
|
|
773
|
-
(0,
|
|
2409
|
+
const file = (0, import_node_path4.join)(outDir, `${stamp}-${slugifyReportName(reportTitle(full))}.md`);
|
|
2410
|
+
(0, import_node_fs3.writeFileSync)(file, full, "utf8");
|
|
774
2411
|
return file;
|
|
775
2412
|
} catch {
|
|
776
2413
|
return null;
|
|
@@ -785,9 +2422,9 @@ function saveBulkSite(siteUrl, pages, seo, imageAudit) {
|
|
|
785
2422
|
if (!reportSavingActive()) return null;
|
|
786
2423
|
try {
|
|
787
2424
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
788
|
-
const dir = (0,
|
|
789
|
-
const pagesDir = (0,
|
|
790
|
-
(0,
|
|
2425
|
+
const dir = (0, import_node_path4.join)(outputBaseDir3(), `extract-${slugifyReportName(siteUrl)}-${stamp}`);
|
|
2426
|
+
const pagesDir = (0, import_node_path4.join)(dir, "pages");
|
|
2427
|
+
(0, import_node_fs3.mkdirSync)(pagesDir, { recursive: true });
|
|
791
2428
|
const indexRows = pages.map((p, i) => {
|
|
792
2429
|
const num = String(i + 1).padStart(4, "0");
|
|
793
2430
|
const slug = slugifyReportName(p.url.replace(/^https?:\/\//, "")).slice(0, 60) || "page";
|
|
@@ -801,7 +2438,7 @@ function saveBulkSite(siteUrl, pages, seo, imageAudit) {
|
|
|
801
2438
|
"",
|
|
802
2439
|
body || "_(no content extracted)_"
|
|
803
2440
|
].filter(Boolean).join("\n");
|
|
804
|
-
(0,
|
|
2441
|
+
(0, import_node_fs3.writeFileSync)((0, import_node_path4.join)(pagesDir, fname), content, "utf8");
|
|
805
2442
|
return `| ${i + 1} | ${cell(p.title ?? "Untitled")} | ${p.url} | pages/${fname} |`;
|
|
806
2443
|
});
|
|
807
2444
|
const dataFilesSection = seo ? [
|
|
@@ -812,6 +2449,9 @@ function saveBulkSite(siteUrl, pages, seo, imageAudit) {
|
|
|
812
2449
|
`- \`pages.jsonl\` \u2014 per-page SEO fields, one JSON object per page`,
|
|
813
2450
|
`- \`links.jsonl\` \u2014 internal/external link edges (anchor, rel, target status)`,
|
|
814
2451
|
`- \`link-metrics.jsonl\` \u2014 inlinks, crawl depth, orphan flags per page`,
|
|
2452
|
+
`- \`link-report.md\` \u2014 link analysis: top internal pages by inlinks, top external domains, distribution`,
|
|
2453
|
+
`- \`links-summary.json\` \u2014 link totals + top-20 internal pages + top-20 external domains`,
|
|
2454
|
+
`- \`external-domains.json\` \u2014 every external domain linked to, ordered by link count`,
|
|
815
2455
|
...imageAudit ? [
|
|
816
2456
|
`- \`images.jsonl\` \u2014 every image with byte size, format, over-100KB and legacy-format flags`,
|
|
817
2457
|
`- \`images-summary.json\` \u2014 image totals (count, total weight, over-100KB, legacy)`
|
|
@@ -830,34 +2470,40 @@ function saveBulkSite(siteUrl, pages, seo, imageAudit) {
|
|
|
830
2470
|
|---|-------|-----|------|
|
|
831
2471
|
${indexRows.join("\n")}`
|
|
832
2472
|
].filter(Boolean).join("\n");
|
|
833
|
-
const indexFile = (0,
|
|
834
|
-
(0,
|
|
2473
|
+
const indexFile = (0, import_node_path4.join)(dir, "index.md");
|
|
2474
|
+
(0, import_node_fs3.writeFileSync)(indexFile, index, "utf8");
|
|
835
2475
|
let seoFiles;
|
|
836
2476
|
if (seo) {
|
|
837
2477
|
const toJsonl = (rows) => rows.map((r) => JSON.stringify(r)).join("\n");
|
|
838
|
-
const pagesJsonl = (0,
|
|
839
|
-
const linksJsonl = (0,
|
|
840
|
-
const metricsJsonl = (0,
|
|
841
|
-
const issuesFile = (0,
|
|
842
|
-
const reportFile = (0,
|
|
843
|
-
(0,
|
|
844
|
-
(0,
|
|
845
|
-
(0,
|
|
846
|
-
(0,
|
|
847
|
-
(0,
|
|
2478
|
+
const pagesJsonl = (0, import_node_path4.join)(dir, "pages.jsonl");
|
|
2479
|
+
const linksJsonl = (0, import_node_path4.join)(dir, "links.jsonl");
|
|
2480
|
+
const metricsJsonl = (0, import_node_path4.join)(dir, "link-metrics.jsonl");
|
|
2481
|
+
const issuesFile = (0, import_node_path4.join)(dir, "issues.json");
|
|
2482
|
+
const reportFile = (0, import_node_path4.join)(dir, "report.md");
|
|
2483
|
+
(0, import_node_fs3.writeFileSync)(pagesJsonl, toJsonl(seo.pageRows), "utf8");
|
|
2484
|
+
(0, import_node_fs3.writeFileSync)(linksJsonl, toJsonl(seo.edges), "utf8");
|
|
2485
|
+
(0, import_node_fs3.writeFileSync)(metricsJsonl, toJsonl(seo.metrics), "utf8");
|
|
2486
|
+
(0, import_node_fs3.writeFileSync)(issuesFile, JSON.stringify(seo.issues, null, 2), "utf8");
|
|
2487
|
+
(0, import_node_fs3.writeFileSync)(reportFile, seo.reportMd + (imageAudit ? `
|
|
848
2488
|
|
|
849
2489
|
${renderImageSection(imageAudit)}` : ""), "utf8");
|
|
850
|
-
|
|
2490
|
+
const linkReportFile = (0, import_node_path4.join)(dir, "link-report.md");
|
|
2491
|
+
const linksSummaryFile = (0, import_node_path4.join)(dir, "links-summary.json");
|
|
2492
|
+
const externalDomainsFile = (0, import_node_path4.join)(dir, "external-domains.json");
|
|
2493
|
+
(0, import_node_fs3.writeFileSync)(linkReportFile, renderLinkReport(seo.linkReport), "utf8");
|
|
2494
|
+
(0, import_node_fs3.writeFileSync)(linksSummaryFile, JSON.stringify(seo.linkReport.summary, null, 2), "utf8");
|
|
2495
|
+
(0, import_node_fs3.writeFileSync)(externalDomainsFile, JSON.stringify(seo.linkReport.externalDomains, null, 2), "utf8");
|
|
2496
|
+
seoFiles = [pagesJsonl, linksJsonl, metricsJsonl, issuesFile, reportFile, linkReportFile, linksSummaryFile, externalDomainsFile];
|
|
851
2497
|
if (imageAudit) {
|
|
852
|
-
const imagesJsonl = (0,
|
|
853
|
-
const imagesSummary = (0,
|
|
854
|
-
(0,
|
|
855
|
-
(0,
|
|
2498
|
+
const imagesJsonl = (0, import_node_path4.join)(dir, "images.jsonl");
|
|
2499
|
+
const imagesSummary = (0, import_node_path4.join)(dir, "images-summary.json");
|
|
2500
|
+
(0, import_node_fs3.writeFileSync)(imagesJsonl, imageAudit.rows.map((r) => JSON.stringify(r)).join("\n"), "utf8");
|
|
2501
|
+
(0, import_node_fs3.writeFileSync)(imagesSummary, JSON.stringify(imageAudit.summary, null, 2), "utf8");
|
|
856
2502
|
seoFiles.push(imagesJsonl, imagesSummary);
|
|
857
2503
|
}
|
|
858
2504
|
if (seo.branding) {
|
|
859
|
-
const brandingFile = (0,
|
|
860
|
-
(0,
|
|
2505
|
+
const brandingFile = (0, import_node_path4.join)(dir, "branding.json");
|
|
2506
|
+
(0, import_node_fs3.writeFileSync)(brandingFile, JSON.stringify(seo.branding, null, 2), "utf8");
|
|
861
2507
|
seoFiles.push(brandingFile);
|
|
862
2508
|
}
|
|
863
2509
|
}
|
|
@@ -869,13 +2515,13 @@ ${renderImageSection(imageAudit)}` : ""), "utf8");
|
|
|
869
2515
|
function saveUrlInventory(siteUrl, urls) {
|
|
870
2516
|
if (!reportSavingActive()) return null;
|
|
871
2517
|
try {
|
|
872
|
-
const outDir =
|
|
873
|
-
(0,
|
|
2518
|
+
const outDir = outputBaseDir3();
|
|
2519
|
+
(0, import_node_fs3.mkdirSync)(outDir, { recursive: true });
|
|
874
2520
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
875
|
-
const file = (0,
|
|
2521
|
+
const file = (0, import_node_path4.join)(outDir, `${stamp}-urlmap-${slugifyReportName(siteUrl.replace(/^https?:\/\//, ""))}.csv`);
|
|
876
2522
|
const csv = (v) => /[",\n]/.test(v) ? `"${v.replace(/"/g, '""')}"` : v;
|
|
877
2523
|
const rows = ["url,status", ...urls.map((u) => `${csv(u.url)},${u.status ?? ""}`)];
|
|
878
|
-
(0,
|
|
2524
|
+
(0, import_node_fs3.writeFileSync)(file, rows.join("\n"), "utf8");
|
|
879
2525
|
return file;
|
|
880
2526
|
} catch {
|
|
881
2527
|
return null;
|
|
@@ -884,12 +2530,12 @@ function saveUrlInventory(siteUrl, urls) {
|
|
|
884
2530
|
function persistScreenshotLocally(base64, url) {
|
|
885
2531
|
if (!reportSavingEnabled || process.env.MCP_SCRAPER_SAVE_REPORTS === "false") return null;
|
|
886
2532
|
try {
|
|
887
|
-
const dir = (0,
|
|
888
|
-
(0,
|
|
2533
|
+
const dir = (0, import_node_path4.join)(outputBaseDir3(), "screenshots");
|
|
2534
|
+
(0, import_node_fs3.mkdirSync)(dir, { recursive: true });
|
|
889
2535
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
890
2536
|
const slug = url.replace(/^https?:\/\//, "").replace(/[^a-z0-9]+/gi, "-").replace(/^-+|-+$/g, "").slice(0, 60);
|
|
891
|
-
const filePath = (0,
|
|
892
|
-
(0,
|
|
2537
|
+
const filePath = (0, import_node_path4.join)(dir, `${stamp}-${slug}.png`);
|
|
2538
|
+
(0, import_node_fs3.writeFileSync)(filePath, Buffer.from(base64, "base64"));
|
|
893
2539
|
return filePath;
|
|
894
2540
|
} catch {
|
|
895
2541
|
return null;
|
|
@@ -1299,6 +2945,7 @@ function buildSeoExport(siteUrl, pages, branding) {
|
|
|
1299
2945
|
metrics: [...metrics.values()],
|
|
1300
2946
|
issues,
|
|
1301
2947
|
reportMd: renderIssueReport(siteUrl, pages, issues, metrics),
|
|
2948
|
+
linkReport: buildLinkReport(edges, [...metrics.values()], siteUrl),
|
|
1302
2949
|
branding: branding ?? void 0
|
|
1303
2950
|
};
|
|
1304
2951
|
}
|
|
@@ -1407,6 +3054,9 @@ async function formatAuditSite(raw, input) {
|
|
|
1407
3054
|
const topIssues = Object.entries(seo.issues).sort((a, b) => b[1].count - a[1].count).slice(0, 8).map(([k, v]) => `- \`${k}\` \u2014 ${v.count}`).join("\n");
|
|
1408
3055
|
const img = imageAudit.summary;
|
|
1409
3056
|
const imgLine = `${img.unique} images \xB7 ${img.totalSize} total \xB7 ${img.over100kb} over 100 KB \xB7 ${img.legacyFormat} legacy format`;
|
|
3057
|
+
const lr = seo.linkReport.summary;
|
|
3058
|
+
const topExt = lr.external.topDomains.slice(0, 3).map((dm) => `${dm.domain} (${dm.links})`).join(", ") || "\u2014";
|
|
3059
|
+
const linkLine = `${lr.internal.totalLinks} internal / ${lr.external.totalLinks} external links \xB7 ${lr.internal.orphans} orphans \xB7 ${lr.internal.brokenInternal} broken \xB7 avg ${lr.internal.avgInlinks} inlinks/page \xB7 top external: ${topExt}`;
|
|
1410
3060
|
const durationLine = `**${pages.length} pages** \xB7 ${((d.durationMs ?? 0) / 1e3).toFixed(1)}s`;
|
|
1411
3061
|
const structuredContent = {
|
|
1412
3062
|
url: input.url,
|
|
@@ -1414,7 +3064,8 @@ async function formatAuditSite(raw, input) {
|
|
|
1414
3064
|
durationMs: d.durationMs ?? 0,
|
|
1415
3065
|
bulkFolder: bulk?.dir ?? null,
|
|
1416
3066
|
issues: Object.fromEntries(Object.entries(seo.issues).map(([k, v]) => [k, v.count])),
|
|
1417
|
-
images: { unique: img.unique, totalBytes: img.totalBytes, over100kb: img.over100kb, legacyFormat: img.legacyFormat }
|
|
3067
|
+
images: { unique: img.unique, totalBytes: img.totalBytes, over100kb: img.over100kb, legacyFormat: img.legacyFormat },
|
|
3068
|
+
links: { internal: lr.internal.totalLinks, external: lr.external.totalLinks, orphans: lr.internal.orphans, brokenInternal: lr.internal.brokenInternal, externalDomains: lr.external.uniqueDomains }
|
|
1418
3069
|
};
|
|
1419
3070
|
const location = bulk ? `
|
|
1420
3071
|
## \u{1F4C1} Technical audit saved
|
|
@@ -1433,6 +3084,9 @@ Report saving is disabled in this environment. Enable \`MCP_SCRAPER_SAVE_REPORTS
|
|
|
1433
3084
|
## Top issues
|
|
1434
3085
|
${topIssues || "_none found_"}`,
|
|
1435
3086
|
`
|
|
3087
|
+
## Links
|
|
3088
|
+
${linkLine}`,
|
|
3089
|
+
`
|
|
1436
3090
|
## Images
|
|
1437
3091
|
${imgLine}`
|
|
1438
3092
|
].join("\n");
|
|
@@ -2539,7 +4193,11 @@ ROUTING
|
|
|
2539
4193
|
- Workflows (multi-step): local directory build -> directory_workflow; rank blueprint -> rank_tracker_workflow;
|
|
2540
4194
|
AI-answer citation fan-out (AEO) -> query_fanout_workflow; deep research -> deep_research_workflow.
|
|
2541
4195
|
- Anything without a dedicated tool (e.g. Reddit, arbitrary logged-in sites) -> the browser_* agent
|
|
2542
|
-
(browser_open, then navigate/read)
|
|
4196
|
+
(browser_open, then navigate/read).
|
|
4197
|
+
- Logged-in sites (ChatGPT, Claude, Reddit, any account): save the login first -> browser_profile_connect
|
|
4198
|
+
(returns an mcpscraper.dev sign-in link for the user; one profile holds MANY logins, call it again with
|
|
4199
|
+
the same profile + a new domain to add accounts) -> poll browser_profile_list until AUTHENTICATED ->
|
|
4200
|
+
browser_open with that profile. browser_profile_list also shows what a profile is connected to.
|
|
2543
4201
|
|
|
2544
4202
|
NOTES
|
|
2545
4203
|
- Bulk / full-site crawls: call extract_site with rotateProxies:true for blocked or rate-limited sites.
|
|
@@ -2551,749 +4209,756 @@ NOTES
|
|
|
2551
4209
|
`.trim();
|
|
2552
4210
|
|
|
2553
4211
|
// src/mcp/mcp-tool-schemas.ts
|
|
2554
|
-
var
|
|
4212
|
+
var import_zod3 = require("zod");
|
|
2555
4213
|
|
|
2556
4214
|
// src/schemas.ts
|
|
2557
|
-
var
|
|
4215
|
+
var import_zod2 = require("zod");
|
|
2558
4216
|
var DEFAULT_PROXY_MODE = "configured";
|
|
2559
4217
|
var DEFAULT_MAPS_PROXY_MODE = "location";
|
|
2560
|
-
var HarvestOptionsSchema =
|
|
2561
|
-
query:
|
|
2562
|
-
location:
|
|
2563
|
-
gl:
|
|
2564
|
-
hl:
|
|
2565
|
-
device:
|
|
2566
|
-
proxyMode:
|
|
2567
|
-
proxyZip:
|
|
2568
|
-
debug:
|
|
2569
|
-
depth:
|
|
2570
|
-
maxQuestions:
|
|
2571
|
-
headless:
|
|
2572
|
-
profileDir:
|
|
2573
|
-
proxy:
|
|
2574
|
-
kernelApiKey:
|
|
2575
|
-
kernelProxyId:
|
|
2576
|
-
kernelProxyResolution:
|
|
2577
|
-
outputDir:
|
|
2578
|
-
format:
|
|
2579
|
-
serpOnly:
|
|
2580
|
-
pages:
|
|
4218
|
+
var HarvestOptionsSchema = import_zod2.z.object({
|
|
4219
|
+
query: import_zod2.z.string().min(1),
|
|
4220
|
+
location: import_zod2.z.string().optional(),
|
|
4221
|
+
gl: import_zod2.z.string().length(2).default("us"),
|
|
4222
|
+
hl: import_zod2.z.string().length(2).default("en"),
|
|
4223
|
+
device: import_zod2.z.enum(["desktop", "mobile"]).default("desktop"),
|
|
4224
|
+
proxyMode: import_zod2.z.enum(["location", "configured", "none"]).default(DEFAULT_PROXY_MODE),
|
|
4225
|
+
proxyZip: import_zod2.z.string().regex(/^\d{5}$/).optional(),
|
|
4226
|
+
debug: import_zod2.z.boolean().default(false),
|
|
4227
|
+
depth: import_zod2.z.number().int().min(1).max(30).default(3),
|
|
4228
|
+
maxQuestions: import_zod2.z.number().int().min(1).max(1e3).default(100),
|
|
4229
|
+
headless: import_zod2.z.boolean().default(false),
|
|
4230
|
+
profileDir: import_zod2.z.string().optional(),
|
|
4231
|
+
proxy: import_zod2.z.string().url().optional(),
|
|
4232
|
+
kernelApiKey: import_zod2.z.string().optional(),
|
|
4233
|
+
kernelProxyId: import_zod2.z.string().optional(),
|
|
4234
|
+
kernelProxyResolution: import_zod2.z.unknown().optional(),
|
|
4235
|
+
outputDir: import_zod2.z.string().default("./paa-output"),
|
|
4236
|
+
format: import_zod2.z.enum(["json", "csv", "both"]).default("both"),
|
|
4237
|
+
serpOnly: import_zod2.z.boolean().default(false),
|
|
4238
|
+
pages: import_zod2.z.number().int().min(1).max(2).default(1)
|
|
2581
4239
|
});
|
|
2582
|
-
var MapsPlaceOptionsSchema =
|
|
2583
|
-
businessName:
|
|
2584
|
-
location:
|
|
2585
|
-
gl:
|
|
2586
|
-
hl:
|
|
2587
|
-
includeReviews:
|
|
2588
|
-
maxReviews:
|
|
2589
|
-
kernelApiKey:
|
|
2590
|
-
kernelProxyId:
|
|
2591
|
-
headless:
|
|
4240
|
+
var MapsPlaceOptionsSchema = import_zod2.z.object({
|
|
4241
|
+
businessName: import_zod2.z.string().min(1),
|
|
4242
|
+
location: import_zod2.z.string().min(1),
|
|
4243
|
+
gl: import_zod2.z.string().length(2).default("us"),
|
|
4244
|
+
hl: import_zod2.z.string().length(2).default("en"),
|
|
4245
|
+
includeReviews: import_zod2.z.boolean().default(false),
|
|
4246
|
+
maxReviews: import_zod2.z.number().int().min(1).max(500).default(50),
|
|
4247
|
+
kernelApiKey: import_zod2.z.string().optional(),
|
|
4248
|
+
kernelProxyId: import_zod2.z.string().optional(),
|
|
4249
|
+
headless: import_zod2.z.boolean().default(true)
|
|
2592
4250
|
});
|
|
2593
|
-
var MapsSearchOptionsSchema =
|
|
2594
|
-
query:
|
|
2595
|
-
location:
|
|
2596
|
-
gl:
|
|
2597
|
-
hl:
|
|
2598
|
-
maxResults:
|
|
2599
|
-
proxyMode:
|
|
2600
|
-
proxyZip:
|
|
2601
|
-
debug:
|
|
2602
|
-
kernelApiKey:
|
|
2603
|
-
kernelProxyId:
|
|
2604
|
-
kernelProxyResolution:
|
|
2605
|
-
headless:
|
|
4251
|
+
var MapsSearchOptionsSchema = import_zod2.z.object({
|
|
4252
|
+
query: import_zod2.z.string().min(1),
|
|
4253
|
+
location: import_zod2.z.string().optional(),
|
|
4254
|
+
gl: import_zod2.z.string().length(2).default("us"),
|
|
4255
|
+
hl: import_zod2.z.string().length(2).default("en"),
|
|
4256
|
+
maxResults: import_zod2.z.number().int().min(1).max(50).default(10),
|
|
4257
|
+
proxyMode: import_zod2.z.enum(["location", "configured", "none"]).default(DEFAULT_MAPS_PROXY_MODE),
|
|
4258
|
+
proxyZip: import_zod2.z.string().regex(/^\d{5}$/).optional(),
|
|
4259
|
+
debug: import_zod2.z.boolean().default(false),
|
|
4260
|
+
kernelApiKey: import_zod2.z.string().optional(),
|
|
4261
|
+
kernelProxyId: import_zod2.z.string().optional(),
|
|
4262
|
+
kernelProxyResolution: import_zod2.z.unknown().optional(),
|
|
4263
|
+
headless: import_zod2.z.boolean().default(true)
|
|
2606
4264
|
});
|
|
2607
|
-
var RawPAAItemSchema =
|
|
2608
|
-
question:
|
|
2609
|
-
answer:
|
|
2610
|
-
sourceTitle:
|
|
2611
|
-
sourceSite:
|
|
2612
|
-
sourceCite:
|
|
4265
|
+
var RawPAAItemSchema = import_zod2.z.object({
|
|
4266
|
+
question: import_zod2.z.string().min(1),
|
|
4267
|
+
answer: import_zod2.z.string().optional(),
|
|
4268
|
+
sourceTitle: import_zod2.z.string().optional(),
|
|
4269
|
+
sourceSite: import_zod2.z.string().optional(),
|
|
4270
|
+
sourceCite: import_zod2.z.string().optional()
|
|
2613
4271
|
});
|
|
2614
|
-
var RawMapsOverviewSchema =
|
|
2615
|
-
name:
|
|
2616
|
-
rating:
|
|
2617
|
-
reviewCount:
|
|
2618
|
-
category:
|
|
2619
|
-
address:
|
|
2620
|
-
hoursSummary:
|
|
2621
|
-
phone:
|
|
2622
|
-
phoneDisplay:
|
|
2623
|
-
website:
|
|
2624
|
-
plusCode:
|
|
2625
|
-
bookingUrl:
|
|
4272
|
+
var RawMapsOverviewSchema = import_zod2.z.object({
|
|
4273
|
+
name: import_zod2.z.string().nullable(),
|
|
4274
|
+
rating: import_zod2.z.string().nullable(),
|
|
4275
|
+
reviewCount: import_zod2.z.string().nullable(),
|
|
4276
|
+
category: import_zod2.z.string().nullable(),
|
|
4277
|
+
address: import_zod2.z.string().nullable(),
|
|
4278
|
+
hoursSummary: import_zod2.z.string().nullable(),
|
|
4279
|
+
phone: import_zod2.z.string().nullable(),
|
|
4280
|
+
phoneDisplay: import_zod2.z.string().nullable(),
|
|
4281
|
+
website: import_zod2.z.string().nullable(),
|
|
4282
|
+
plusCode: import_zod2.z.string().nullable(),
|
|
4283
|
+
bookingUrl: import_zod2.z.string().nullable()
|
|
2626
4284
|
});
|
|
2627
|
-
var RawMapsHoursRowSchema =
|
|
2628
|
-
day:
|
|
2629
|
-
hours:
|
|
4285
|
+
var RawMapsHoursRowSchema = import_zod2.z.object({
|
|
4286
|
+
day: import_zod2.z.string(),
|
|
4287
|
+
hours: import_zod2.z.string()
|
|
2630
4288
|
});
|
|
2631
|
-
var RawMapsReviewStatsSchema =
|
|
2632
|
-
reviewHistogram:
|
|
2633
|
-
stars:
|
|
2634
|
-
count:
|
|
4289
|
+
var RawMapsReviewStatsSchema = import_zod2.z.object({
|
|
4290
|
+
reviewHistogram: import_zod2.z.array(import_zod2.z.object({
|
|
4291
|
+
stars: import_zod2.z.number(),
|
|
4292
|
+
count: import_zod2.z.string()
|
|
2635
4293
|
})),
|
|
2636
|
-
reviewTopics:
|
|
2637
|
-
label:
|
|
2638
|
-
count:
|
|
4294
|
+
reviewTopics: import_zod2.z.array(import_zod2.z.object({
|
|
4295
|
+
label: import_zod2.z.string(),
|
|
4296
|
+
count: import_zod2.z.string()
|
|
2639
4297
|
}))
|
|
2640
4298
|
});
|
|
2641
|
-
var RawMapsReviewCardSchema =
|
|
2642
|
-
reviewId:
|
|
2643
|
-
author:
|
|
2644
|
-
stars:
|
|
2645
|
-
date:
|
|
2646
|
-
text:
|
|
2647
|
-
ownerResponse:
|
|
4299
|
+
var RawMapsReviewCardSchema = import_zod2.z.object({
|
|
4300
|
+
reviewId: import_zod2.z.string(),
|
|
4301
|
+
author: import_zod2.z.string().nullable(),
|
|
4302
|
+
stars: import_zod2.z.string().nullable(),
|
|
4303
|
+
date: import_zod2.z.string().nullable(),
|
|
4304
|
+
text: import_zod2.z.string().nullable(),
|
|
4305
|
+
ownerResponse: import_zod2.z.string().nullable()
|
|
2648
4306
|
});
|
|
2649
|
-
var RawMapsAboutAttributeSchema =
|
|
2650
|
-
section:
|
|
2651
|
-
attribute:
|
|
4307
|
+
var RawMapsAboutAttributeSchema = import_zod2.z.object({
|
|
4308
|
+
section: import_zod2.z.string(),
|
|
4309
|
+
attribute: import_zod2.z.string()
|
|
2652
4310
|
});
|
|
2653
4311
|
|
|
2654
4312
|
// src/mcp/mcp-tool-schemas.ts
|
|
2655
4313
|
var HarvestPaaInputSchema = {
|
|
2656
|
-
query:
|
|
2657
|
-
location:
|
|
2658
|
-
maxQuestions:
|
|
2659
|
-
gl:
|
|
2660
|
-
hl:
|
|
2661
|
-
device:
|
|
2662
|
-
proxyMode:
|
|
2663
|
-
proxyZip:
|
|
2664
|
-
debug:
|
|
4314
|
+
query: import_zod3.z.string().min(1).describe('Core search topic only. If the user says "best hvac company in Denver CO", use query="best hvac company" and location="Denver, CO". Do not include the location in query when it can be separated.'),
|
|
4315
|
+
location: import_zod3.z.string().optional().describe('City, region, or country for geo-targeted results, inferred from the user request when present, e.g. "Denver, CO", "Tokyo, Japan", "London, UK".'),
|
|
4316
|
+
maxQuestions: import_zod3.z.number().int().min(1).max(200).default(30).describe("Number of PAA questions to extract. Default 30. Maximum 200. Use 10 for quick probes, 30 for normal research, 100-200 when the user asks for everything/full/deep research. Larger harvests get a longer server time budget (151-200 questions \u2192 up to 280s). Credits are charged by extracted question; unused request hold is refunded."),
|
|
4317
|
+
gl: import_zod3.z.string().length(2).default("us").describe("Google country code inferred from location or user language. Examples: United States us, United Kingdom gb, Japan jp, Canada ca, Australia au."),
|
|
4318
|
+
hl: import_zod3.z.string().default("en").describe("Google interface/content language inferred from the user request. Use en unless the user asks for another language or locale."),
|
|
4319
|
+
device: import_zod3.z.enum(["desktop", "mobile"]).default("desktop").describe("SERP device context. Use desktop by default; use mobile only when the user asks for mobile rankings."),
|
|
4320
|
+
proxyMode: import_zod3.z.enum(["location", "configured", "none"]).default(DEFAULT_PROXY_MODE).describe("Proxy targeting mode. Default configured uses the service proxy without city/ZIP targeting for the highest general success rate. Use location only when the user explicitly needs city/ZIP-targeted residential proxy evidence. Use none only for direct-network debugging."),
|
|
4321
|
+
proxyZip: import_zod3.z.string().regex(/^\d{5}$/).optional().describe("Optional US ZIP override for residential location proxy targeting. Use only with proxyMode location when the user gives a specific ZIP or city-center targeting needs to be forced."),
|
|
4322
|
+
debug: import_zod3.z.boolean().default(false).describe("Include sanitized browser/session/location diagnostics in the response. Use true when debugging localization, CAPTCHA, or proxy behavior.")
|
|
2665
4323
|
};
|
|
2666
4324
|
var ExtractUrlInputSchema = {
|
|
2667
|
-
url:
|
|
2668
|
-
screenshot:
|
|
2669
|
-
screenshotDevice:
|
|
2670
|
-
extractBranding:
|
|
2671
|
-
downloadMedia:
|
|
2672
|
-
mediaTypes:
|
|
2673
|
-
allowLocal:
|
|
4325
|
+
url: import_zod3.z.string().url().describe("Public http/https URL to extract. Use this when the user provides one specific page URL."),
|
|
4326
|
+
screenshot: import_zod3.z.boolean().default(false).describe("Also capture a full-page screenshot of the URL. Saved to ~/Downloads/mcp-scraper/screenshots/ and returned inline. Use when the user asks to see or capture the page visually."),
|
|
4327
|
+
screenshotDevice: import_zod3.z.enum(["desktop", "mobile"]).default("desktop").describe("Viewport for screenshot. desktop = 1440\xD7900. mobile = 390\xD7844. Default desktop."),
|
|
4328
|
+
extractBranding: import_zod3.z.boolean().default(false).describe("Extract brand colors, fonts, logo, and favicon using a rendered browser session. Returns colorScheme (light/dark), colors (primary/accent/background/text/heading as hex), fonts (heading/body family names), and assets (logo URL, favicon URL). Use when the user asks about brand colors, site theme, or brand assets."),
|
|
4329
|
+
downloadMedia: import_zod3.z.boolean().default(false).describe("Extract and download all page media (images, video, audio) to ~/Downloads/mcp-scraper/media/. Ad networks, tracking pixels, and noise URLs are filtered automatically. Use when the user asks to download or harvest assets from a page."),
|
|
4330
|
+
mediaTypes: import_zod3.z.array(import_zod3.z.enum(["image", "video", "audio"])).default(["image", "video", "audio"]).describe("Which media types to download. Default all three."),
|
|
4331
|
+
allowLocal: import_zod3.z.boolean().default(false).describe("Allow localhost and private-network URLs. For local development only.")
|
|
2674
4332
|
};
|
|
2675
4333
|
var MapSiteUrlsInputSchema = {
|
|
2676
|
-
url:
|
|
2677
|
-
maxUrls:
|
|
4334
|
+
url: import_zod3.z.string().url().describe("Public website URL or domain to crawl for internal URLs. Use before extract_site when the user asks to audit/map/crawl a site."),
|
|
4335
|
+
maxUrls: import_zod3.z.number().int().min(1).max(1e4).optional().describe("Maximum URLs to discover. Use 100 for normal maps, up to 10000 for a full inventory. Large maps (over 500 URLs) write the complete inventory to a local file and return only a summary plus the file path instead of the full list inline.")
|
|
2678
4336
|
};
|
|
2679
4337
|
var ExtractSiteInputSchema = {
|
|
2680
|
-
url:
|
|
2681
|
-
maxPages:
|
|
2682
|
-
rotateProxies:
|
|
2683
|
-
rotateProxyEvery:
|
|
2684
|
-
formats:
|
|
4338
|
+
url: import_zod3.z.string().url().describe("Public website URL or domain to crawl for page CONTENT across multiple pages (map + scrape). Use when the user wants the content/text/markdown of a site's pages. For a technical SEO audit (issues, link graph, indexability, headings, image weights) use audit_site instead \u2014 extract_site returns content only, not analysis."),
|
|
4339
|
+
maxPages: import_zod3.z.number().int().min(1).max(1e4).optional().describe("Maximum pages to extract. Use 50 for a normal crawl, up to 10000 for a full-site bulk scrape. Bulk crawls (over 25 pages) switch to folder mode: every page is saved as its own Markdown file in a local folder and the response returns only a summary plus the folder path, so the full content never floods the context window."),
|
|
4340
|
+
rotateProxies: import_zod3.z.boolean().optional().describe("Route page fetches through rotating residential proxies in headful browsers to defeat rate-limiting and bot blocks (403/429). Discovers URLs from the sitemap, fetches in batches with a fresh proxy per batch, retries failures on a new proxy, and automatically parallelizes across the account's concurrency slots. Slower and pricier than the default fetch path, so use only when a site blocks normal crawling."),
|
|
4341
|
+
rotateProxyEvery: import_zod3.z.number().int().min(1).max(100).optional().describe("When rotateProxies is on, how many pages to fetch per browser/proxy before discarding it and minting a fresh one. Default 30. Lower values rotate IPs more aggressively against strict rate limits."),
|
|
4342
|
+
formats: import_zod3.z.array(import_zod3.z.enum(["markdown", "links", "json", "images", "branding"])).optional().describe("Which data formats to include in the per-page output. markdown=page content as Markdown, links=internal/external link graph, json=structured data/schema, images=per-page image URLs (links only, no downloads), branding=site-level logo/colors/fonts captured once from the homepage (requires a browser, adds time). markdown, links, json, and images are always captured cheaply from the HTML; branding is the only opt-in capture. Defaults to markdown+links when omitted.")
|
|
2685
4343
|
};
|
|
2686
4344
|
var AuditSiteInputSchema = {
|
|
2687
|
-
url:
|
|
2688
|
-
maxPages:
|
|
2689
|
-
rotateProxies:
|
|
2690
|
-
rotateProxyEvery:
|
|
4345
|
+
url: import_zod3.z.string().url().describe("Public website URL or domain to run a full technical SEO audit on. Use when the user asks for a technical audit, SEO audit, site health check, or a Screaming-Frog-style crawl \u2014 i.e. they want ANALYSIS (issues, internal link graph, indexability, heading breakdown, image sizes/formats), not just page content. For plain content scraping use extract_site instead."),
|
|
4346
|
+
maxPages: import_zod3.z.number().int().min(1).max(1e4).optional().describe("Maximum pages to crawl and audit. Use 50 for a normal audit, up to 10000 for a full-site audit. The audit always writes a folder of analysis files (report.md, issues.json, pages.jsonl, links.jsonl, link-metrics.jsonl, images.jsonl) plus per-page content, and returns a headline summary plus the folder path."),
|
|
4347
|
+
rotateProxies: import_zod3.z.boolean().optional().describe("Route page fetches through rotating residential proxies to defeat rate-limiting and bot blocks (403/429). Slower and pricier than the default fetch path, so use only when a site blocks normal crawling."),
|
|
4348
|
+
rotateProxyEvery: import_zod3.z.number().int().min(1).max(100).optional().describe("When rotateProxies is on, how many pages to fetch per browser/proxy before discarding it and minting a fresh one. Default 30.")
|
|
2691
4349
|
};
|
|
2692
4350
|
var YoutubeHarvestInputSchema = {
|
|
2693
|
-
mode:
|
|
2694
|
-
query:
|
|
2695
|
-
channelHandle:
|
|
2696
|
-
maxVideos:
|
|
4351
|
+
mode: import_zod3.z.enum(["search", "channel"]).describe("Use search for topic/keyword requests. Use channel when the user provides @handle, channel ID, or channel URL."),
|
|
4352
|
+
query: import_zod3.z.string().optional().describe("Required when mode is search. The YouTube search topic in the user\u2019s words."),
|
|
4353
|
+
channelHandle: import_zod3.z.string().optional().describe("YouTube channel handle, channel ID, or URL. Examples: @mkbhd, UC..., https://youtube.com/@mkbhd."),
|
|
4354
|
+
maxVideos: import_zod3.z.number().int().min(1).max(500).default(50).describe("Number of videos to return. Default 50, maximum 500. Use 10-25 for quick topic discovery, 50 for normal channel/search harvests, and larger values only when the user asks for full channel/history because large responses consume more context.")
|
|
2697
4355
|
};
|
|
2698
4356
|
var YoutubeTranscribeInputSchema = {
|
|
2699
|
-
videoId:
|
|
2700
|
-
url:
|
|
4357
|
+
videoId: import_zod3.z.string().min(1).optional().describe("YouTube video ID, e.g. dQw4w9WgXcQ. Use only an ID returned by youtube_harvest or visible in a YouTube URL; do not invent one."),
|
|
4358
|
+
url: import_zod3.z.string().url().optional().describe("Full YouTube URL, e.g. https://www.youtube.com/watch?v=dQw4w9WgXcQ or https://youtu.be/dQw4w9WgXcQ. Use this when the user pasted a URL instead of an ID. Provide videoId or url.")
|
|
2701
4359
|
};
|
|
2702
4360
|
var FacebookPageIntelInputSchema = {
|
|
2703
|
-
pageId:
|
|
2704
|
-
libraryId:
|
|
2705
|
-
query:
|
|
2706
|
-
maxAds:
|
|
2707
|
-
country:
|
|
4361
|
+
pageId: import_zod3.z.string().optional().describe("Facebook advertiser/page ID. Use only a pageId returned by facebook_ad_search/facebook_page_intel or copied from Facebook Ad Library; do not construct one yourself."),
|
|
4362
|
+
libraryId: import_zod3.z.string().optional().describe("Facebook Ad Library archive ID for a known ad or advertiser sample. Use a libraryId returned by facebook_ad_search, or a libraryId/adArchiveId visible in Ad Library."),
|
|
4363
|
+
query: import_zod3.z.string().optional().describe("Advertiser or brand name when pageId/libraryId is not known. One of pageId, libraryId, or query is required."),
|
|
4364
|
+
maxAds: import_zod3.z.number().int().min(1).max(200).default(50).describe("Maximum ads to inspect. Default 50, maximum 200. Prefer 25-50 for focused advertiser scans; use 100-200 only when the user asks for a broad ad archive sweep."),
|
|
4365
|
+
country: import_zod3.z.string().length(2).default("US").describe("Two-letter Ad Library country code. Default US. Examples: US, CA, GB, AU. Infer from the user request when they name a country.")
|
|
2708
4366
|
};
|
|
2709
4367
|
var FacebookAdSearchInputSchema = {
|
|
2710
|
-
query:
|
|
2711
|
-
country:
|
|
2712
|
-
maxResults:
|
|
4368
|
+
query: import_zod3.z.string().min(1).describe("Advertiser, brand, competitor, niche, or keyword to search in Facebook Ad Library."),
|
|
4369
|
+
country: import_zod3.z.string().length(2).default("US").describe("Two-letter Ad Library country code. Default US. Examples: US, CA, GB, AU."),
|
|
4370
|
+
maxResults: import_zod3.z.number().int().min(1).max(20).default(10).describe("Maximum advertisers to return. Default 10, maximum 20. Prefer tighter search terms over maxing this out.")
|
|
2713
4371
|
};
|
|
2714
4372
|
var FacebookAdTranscribeInputSchema = {
|
|
2715
|
-
videoUrl:
|
|
4373
|
+
videoUrl: import_zod3.z.string().url().describe("Direct Facebook CDN video URL from a facebook_page_intel ad result. Do not pass a public Facebook reel/post/share URL here; use facebook_video_transcribe for organic Facebook URLs.")
|
|
2716
4374
|
};
|
|
2717
4375
|
var FacebookVideoTranscribeInputSchema = {
|
|
2718
|
-
url:
|
|
2719
|
-
quality:
|
|
4376
|
+
url: import_zod3.z.string().url().describe("Organic Facebook reel, video, watch, post, or share URL from facebook.com, m.facebook.com, or fb.watch. The tool renders the page, extracts the best matching public Facebook CDN MP4 URL, then transcribes it. Use this when the user pastes a normal Facebook video page URL and asks for the transcript or downloadable MP4."),
|
|
4377
|
+
quality: import_zod3.z.enum(["best", "hd", "sd"]).default("best").describe("Preferred progressive MP4 quality. Use best by default; hd prefers the highest HD progressive URL; sd forces the SD URL.")
|
|
2720
4378
|
};
|
|
2721
4379
|
var InstagramProfileContentInputSchema = {
|
|
2722
|
-
handle:
|
|
2723
|
-
url:
|
|
2724
|
-
profile:
|
|
2725
|
-
saveProfileChanges:
|
|
2726
|
-
maxItems:
|
|
2727
|
-
maxScrolls:
|
|
2728
|
-
scrollDelayMs:
|
|
2729
|
-
stableScrollLimit:
|
|
4380
|
+
handle: import_zod3.z.string().min(1).optional().describe("Instagram handle, with or without @. Provide handle or url."),
|
|
4381
|
+
url: import_zod3.z.string().url().optional().describe("Instagram profile URL, e.g. https://www.instagram.com/nasaartemis/. Provide handle or url."),
|
|
4382
|
+
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."),
|
|
4383
|
+
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."),
|
|
4384
|
+
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."),
|
|
4385
|
+
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."),
|
|
4386
|
+
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."),
|
|
4387
|
+
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.")
|
|
2730
4388
|
};
|
|
2731
4389
|
var InstagramMediaDownloadInputSchema = {
|
|
2732
|
-
url:
|
|
2733
|
-
profile:
|
|
2734
|
-
saveProfileChanges:
|
|
2735
|
-
mediaTypes:
|
|
2736
|
-
downloadMedia:
|
|
2737
|
-
downloadAllTracks:
|
|
2738
|
-
includeTranscript:
|
|
2739
|
-
mux:
|
|
4390
|
+
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."),
|
|
4391
|
+
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."),
|
|
4392
|
+
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."),
|
|
4393
|
+
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."),
|
|
4394
|
+
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."),
|
|
4395
|
+
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."),
|
|
4396
|
+
includeTranscript: import_zod3.z.boolean().default(false).describe("Transcribe the selected audio track when available. This adds transcription cost and may take longer."),
|
|
4397
|
+
mux: import_zod3.z.boolean().default(true).describe("When video and audio tracks are downloaded separately, try to mux them into a single MP4 if ffmpeg is available. Returns separate tracks when muxing is unavailable.")
|
|
2740
4398
|
};
|
|
2741
4399
|
var MapsPlaceIntelInputSchema = {
|
|
2742
|
-
businessName:
|
|
2743
|
-
location:
|
|
2744
|
-
gl:
|
|
2745
|
-
hl:
|
|
2746
|
-
includeReviews:
|
|
2747
|
-
maxReviews:
|
|
4400
|
+
businessName: import_zod3.z.string().min(1).describe('Business name only. If user says "Elite Roofing Denver CO", use businessName="Elite Roofing" and location="Denver, CO".'),
|
|
4401
|
+
location: import_zod3.z.string().min(1).describe('City/region/country where the business should be searched, e.g. "Denver, CO". Infer from the user request when possible.'),
|
|
4402
|
+
gl: import_zod3.z.string().length(2).default("us").describe("Google country code inferred from location."),
|
|
4403
|
+
hl: import_zod3.z.string().length(2).default("en").describe("Language inferred from user request."),
|
|
4404
|
+
includeReviews: import_zod3.z.boolean().default(false).describe("Whether to fetch individual review cards. Use true when the user asks for reviews, customer pain, complaints, praise, themes, or review evidence."),
|
|
4405
|
+
maxReviews: import_zod3.z.number().int().min(1).max(500).default(50).describe("Max review cards to return when includeReviews is true. Default 50, maximum 500. Use 50 for normal review analysis and larger values only for deep review mining.")
|
|
2748
4406
|
};
|
|
2749
4407
|
var MapsSearchInputSchema = {
|
|
2750
|
-
query:
|
|
2751
|
-
location:
|
|
2752
|
-
gl:
|
|
2753
|
-
hl:
|
|
2754
|
-
maxResults:
|
|
2755
|
-
proxyMode:
|
|
2756
|
-
proxyZip:
|
|
2757
|
-
debug:
|
|
4408
|
+
query: import_zod3.z.string().min(1).describe('Business category, niche, keyword, or search term. If the user says "roofers in Denver CO", use query="roofers" and location="Denver, CO". Do not put the location here when it can be separated.'),
|
|
4409
|
+
location: import_zod3.z.string().optional().describe('City, region, country, or service area for the Maps search, e.g. "Denver, CO". Infer from the user request when present.'),
|
|
4410
|
+
gl: import_zod3.z.string().length(2).default("us").describe("Google country code inferred from location."),
|
|
4411
|
+
hl: import_zod3.z.string().length(2).default("en").describe("Language inferred from user request."),
|
|
4412
|
+
maxResults: import_zod3.z.number().int().min(1).max(50).default(10).describe("Number of Google Maps business/profile candidates to return. Default 10. Maximum 50. Use 10 unless the user asks for more."),
|
|
4413
|
+
proxyMode: import_zod3.z.enum(["location", "configured", "none"]).default(DEFAULT_MAPS_PROXY_MODE).describe("Proxy targeting mode. Maps defaults to location so local market searches get city/state residential proxy targeting and rotation. Use configured to force the service proxy without city/ZIP targeting, and none only for local direct-network debugging."),
|
|
4414
|
+
proxyZip: import_zod3.z.string().regex(/^\d{5}$/).optional().describe("Optional US ZIP override for residential location proxy targeting. Use when the user gives a specific ZIP or city-center ZIP."),
|
|
4415
|
+
debug: import_zod3.z.boolean().default(false).describe("Include sanitized browser/proxy diagnostics when debugging Maps localization, CAPTCHA, or proxy behavior.")
|
|
2758
4416
|
};
|
|
2759
4417
|
var DirectoryWorkflowInputSchema = {
|
|
2760
|
-
query:
|
|
2761
|
-
state:
|
|
2762
|
-
minPopulation:
|
|
2763
|
-
populationYear:
|
|
2764
|
-
maxCities:
|
|
2765
|
-
maxResultsPerCity:
|
|
2766
|
-
concurrency:
|
|
2767
|
-
includeZipGroups:
|
|
2768
|
-
usZipsCsvPath:
|
|
2769
|
-
saveCsv:
|
|
2770
|
-
proxyMode:
|
|
2771
|
-
proxyZip:
|
|
2772
|
-
debug:
|
|
2773
|
-
};
|
|
2774
|
-
var RankTrackerModeSchema =
|
|
4418
|
+
query: import_zod3.z.string().min(1).describe("Business category, niche, or keyword to search on Google Maps for every selected market, e.g. roofers, dentists, med spas. Do not include the city here."),
|
|
4419
|
+
state: import_zod3.z.string().min(2).default("TN").describe("US state abbreviation or state name used to select Census places, e.g. TN or Tennessee."),
|
|
4420
|
+
minPopulation: import_zod3.z.number().int().min(0).default(1e5).describe('Minimum Census place population for market selection. Use 100000 for "cities above 100k population".'),
|
|
4421
|
+
populationYear: import_zod3.z.number().int().min(2020).max(2025).default(2025).describe("Census population estimate year from the 2020-2025 Population Estimates Program city/place dataset."),
|
|
4422
|
+
maxCities: import_zod3.z.number().int().min(1).max(100).default(25).describe("Maximum number of markets to process after sorting by population descending."),
|
|
4423
|
+
maxResultsPerCity: import_zod3.z.number().int().min(1).max(50).default(50).describe("Google Maps business/profile candidates to collect for each city. Maximum 50."),
|
|
4424
|
+
concurrency: import_zod3.z.number().int().min(1).max(5).default(5).describe("How many city Maps searches to run in parallel. Use 5 for broad directory batches unless debugging."),
|
|
4425
|
+
includeZipGroups: import_zod3.z.boolean().default(true).describe("Attach ZIP groups from a configured US ZIPS CSV when available. Set MCP_SCRAPER_USZIPS_CSV_PATH on the API server or pass usZipsCsvPath in local/test mode."),
|
|
4426
|
+
usZipsCsvPath: import_zod3.z.string().optional().describe("Local/test-only path to a US ZIPS CSV with state_abbr, zipcode, county, city columns, such as Lead Magician tools/analytics/data/uszips.csv. Deployed APIs should use MCP_SCRAPER_USZIPS_CSV_PATH instead."),
|
|
4427
|
+
saveCsv: import_zod3.z.boolean().default(true).describe("Save a directory-ready CSV to the MCP Scraper output directory and return its path. CSV rows include source_location, result_position, business_name, review_stars, review_count, category, address, phone, hours_status, website_url, directions_url, place_url, CID fields, population, and ZIP groups."),
|
|
4428
|
+
proxyMode: import_zod3.z.enum(["location", "configured", "none"]).default(DEFAULT_MAPS_PROXY_MODE).describe("Proxy targeting mode for every city Maps search. Maps workflows default to location so each city can use city/state or ZIP-group residential proxy targeting. Use configured to force the service proxy without city/ZIP targeting, and none only for local direct-network debugging."),
|
|
4429
|
+
proxyZip: import_zod3.z.string().regex(/^\d{5}$/).optional().describe("Optional ZIP override for proxy targeting. Normally omit it so each city can use its ZIP group or city/state location."),
|
|
4430
|
+
debug: import_zod3.z.boolean().default(false).describe("Include sanitized browser/proxy diagnostics in each Maps browser session when supported.")
|
|
4431
|
+
};
|
|
4432
|
+
var RankTrackerModeSchema = import_zod3.z.enum(["maps", "organic", "ai_overview", "paa"]);
|
|
2775
4433
|
var RankTrackerBlueprintInputSchema = {
|
|
2776
|
-
projectName:
|
|
2777
|
-
targetDomain:
|
|
2778
|
-
targetBusinessName:
|
|
2779
|
-
trackingModes:
|
|
2780
|
-
keywords:
|
|
2781
|
-
locations:
|
|
2782
|
-
competitors:
|
|
2783
|
-
database:
|
|
2784
|
-
scheduleCadence:
|
|
2785
|
-
customCron:
|
|
2786
|
-
timezone:
|
|
2787
|
-
includeCron:
|
|
2788
|
-
includeDashboard:
|
|
2789
|
-
includeAlerts:
|
|
2790
|
-
notes:
|
|
2791
|
-
};
|
|
2792
|
-
var
|
|
2793
|
-
var MapsSearchAttemptOutput =
|
|
2794
|
-
attemptNumber:
|
|
2795
|
-
maxAttempts:
|
|
2796
|
-
status:
|
|
2797
|
-
outcome:
|
|
2798
|
-
willRetry:
|
|
2799
|
-
durationMs:
|
|
2800
|
-
resultCount:
|
|
2801
|
-
error:
|
|
2802
|
-
proxyMode:
|
|
2803
|
-
proxyResolutionSource:
|
|
2804
|
-
proxyIdSuffix:
|
|
2805
|
-
proxyTargetLevel:
|
|
2806
|
-
proxyTargetLocation:
|
|
2807
|
-
proxyTargetZip:
|
|
2808
|
-
browserSessionIdSuffix:
|
|
2809
|
-
observedIp:
|
|
2810
|
-
observedCity:
|
|
2811
|
-
observedRegion:
|
|
4434
|
+
projectName: import_zod3.z.string().min(1).optional().describe("Optional name for the rank tracker project, client, or campaign."),
|
|
4435
|
+
targetDomain: import_zod3.z.string().min(1).optional().describe("Primary domain to track in organic results, AI Overview citations, and PAA sources, e.g. example.com."),
|
|
4436
|
+
targetBusinessName: import_zod3.z.string().min(1).optional().describe("Primary Google Business Profile or brand/business name to match in Maps results. Required for reliable Maps rank tracking."),
|
|
4437
|
+
trackingModes: import_zod3.z.array(RankTrackerModeSchema).min(1).max(4).default(["maps", "organic", "ai_overview", "paa"]).describe("Rank tracker surfaces to build. maps uses directory_workflow/maps_search. organic uses search_serp. ai_overview uses search_serp/harvest_paa. paa uses harvest_paa source presence."),
|
|
4438
|
+
keywords: import_zod3.z.array(import_zod3.z.string().min(1)).max(200).default([]).describe("Seed keywords or service queries to track. Leave empty when the downstream AI should create the keyword table from user input."),
|
|
4439
|
+
locations: import_zod3.z.array(import_zod3.z.string().min(1)).max(100).default([]).describe("Markets, cities, ZIPs, or service areas to track. Use city/state strings like Denver, CO for localized SERP and Maps checks."),
|
|
4440
|
+
competitors: import_zod3.z.array(import_zod3.z.string().min(1)).max(100).default([]).describe("Optional competitor domains or business names to persist as comparison targets."),
|
|
4441
|
+
database: import_zod3.z.enum(["postgres", "neon", "supabase", "sqlite", "mysql"]).default("postgres").describe("Database family the downstream AI should target when generating migrations."),
|
|
4442
|
+
scheduleCadence: import_zod3.z.enum(["daily", "weekly", "monthly", "custom"]).default("weekly").describe("Default recurring rank check cadence for the generated cron/heartbeat plan."),
|
|
4443
|
+
customCron: import_zod3.z.string().min(1).optional().describe("Cron expression to use when scheduleCadence is custom."),
|
|
4444
|
+
timezone: import_zod3.z.string().min(1).default("UTC").describe("IANA timezone for scheduled rank checks, e.g. America/Denver."),
|
|
4445
|
+
includeCron: import_zod3.z.boolean().default(true).describe("Include a cron or heartbeat worker plan. Keep true for production rank trackers."),
|
|
4446
|
+
includeDashboard: import_zod3.z.boolean().default(true).describe("Include dashboard/reporting requirements in the generated prompt."),
|
|
4447
|
+
includeAlerts: import_zod3.z.boolean().default(true).describe("Include alert rules for rank movement and SERP feature gains/losses."),
|
|
4448
|
+
notes: import_zod3.z.string().max(4e3).optional().describe("Extra product, client, stack, or hosting requirements to include in the implementation prompt.")
|
|
4449
|
+
};
|
|
4450
|
+
var NullableString2 = import_zod3.z.string().nullable();
|
|
4451
|
+
var MapsSearchAttemptOutput = import_zod3.z.object({
|
|
4452
|
+
attemptNumber: import_zod3.z.number().int().min(1),
|
|
4453
|
+
maxAttempts: import_zod3.z.number().int().min(1),
|
|
4454
|
+
status: import_zod3.z.enum(["ok", "failed"]),
|
|
4455
|
+
outcome: import_zod3.z.string(),
|
|
4456
|
+
willRetry: import_zod3.z.boolean(),
|
|
4457
|
+
durationMs: import_zod3.z.number().int().min(0),
|
|
4458
|
+
resultCount: import_zod3.z.number().int().min(0),
|
|
4459
|
+
error: NullableString2,
|
|
4460
|
+
proxyMode: import_zod3.z.enum(["location", "configured", "none"]),
|
|
4461
|
+
proxyResolutionSource: import_zod3.z.enum(["disabled", "location_reused", "location_created", "configured_fallback", "unavailable"]).nullable(),
|
|
4462
|
+
proxyIdSuffix: NullableString2,
|
|
4463
|
+
proxyTargetLevel: import_zod3.z.enum(["zip", "city", "state"]).nullable(),
|
|
4464
|
+
proxyTargetLocation: NullableString2,
|
|
4465
|
+
proxyTargetZip: NullableString2,
|
|
4466
|
+
browserSessionIdSuffix: NullableString2,
|
|
4467
|
+
observedIp: NullableString2,
|
|
4468
|
+
observedCity: NullableString2,
|
|
4469
|
+
observedRegion: NullableString2
|
|
2812
4470
|
});
|
|
2813
4471
|
var MapsSearchOutputSchema = {
|
|
2814
|
-
query:
|
|
2815
|
-
location:
|
|
2816
|
-
searchQuery:
|
|
2817
|
-
searchUrl:
|
|
2818
|
-
extractedAt:
|
|
2819
|
-
requestedMaxResults:
|
|
2820
|
-
resultCount:
|
|
2821
|
-
results:
|
|
2822
|
-
position:
|
|
2823
|
-
name:
|
|
2824
|
-
placeUrl:
|
|
2825
|
-
cid:
|
|
2826
|
-
cidDecimal:
|
|
2827
|
-
rating:
|
|
2828
|
-
reviewCount:
|
|
2829
|
-
category:
|
|
2830
|
-
address:
|
|
2831
|
-
phone:
|
|
2832
|
-
hoursStatus:
|
|
2833
|
-
websiteUrl:
|
|
2834
|
-
directionsUrl:
|
|
2835
|
-
metadata:
|
|
4472
|
+
query: import_zod3.z.string(),
|
|
4473
|
+
location: import_zod3.z.string().nullable(),
|
|
4474
|
+
searchQuery: import_zod3.z.string(),
|
|
4475
|
+
searchUrl: import_zod3.z.string().url(),
|
|
4476
|
+
extractedAt: import_zod3.z.string(),
|
|
4477
|
+
requestedMaxResults: import_zod3.z.number().int().min(1).max(50),
|
|
4478
|
+
resultCount: import_zod3.z.number().int().min(0).max(50),
|
|
4479
|
+
results: import_zod3.z.array(import_zod3.z.object({
|
|
4480
|
+
position: import_zod3.z.number().int().min(1),
|
|
4481
|
+
name: import_zod3.z.string(),
|
|
4482
|
+
placeUrl: import_zod3.z.string().url(),
|
|
4483
|
+
cid: NullableString2,
|
|
4484
|
+
cidDecimal: NullableString2,
|
|
4485
|
+
rating: NullableString2,
|
|
4486
|
+
reviewCount: NullableString2,
|
|
4487
|
+
category: NullableString2,
|
|
4488
|
+
address: NullableString2,
|
|
4489
|
+
phone: NullableString2,
|
|
4490
|
+
hoursStatus: NullableString2,
|
|
4491
|
+
websiteUrl: NullableString2,
|
|
4492
|
+
directionsUrl: NullableString2,
|
|
4493
|
+
metadata: import_zod3.z.array(import_zod3.z.string())
|
|
2836
4494
|
})),
|
|
2837
|
-
attempts:
|
|
2838
|
-
durationMs:
|
|
2839
|
-
};
|
|
2840
|
-
var DirectoryMapsBusinessOutput =
|
|
2841
|
-
position:
|
|
2842
|
-
name:
|
|
2843
|
-
placeUrl:
|
|
2844
|
-
cid:
|
|
2845
|
-
cidDecimal:
|
|
2846
|
-
rating:
|
|
2847
|
-
reviewCount:
|
|
2848
|
-
category:
|
|
2849
|
-
address:
|
|
2850
|
-
phone:
|
|
2851
|
-
hoursStatus:
|
|
2852
|
-
websiteUrl:
|
|
2853
|
-
directionsUrl:
|
|
2854
|
-
metadata:
|
|
4495
|
+
attempts: import_zod3.z.array(MapsSearchAttemptOutput),
|
|
4496
|
+
durationMs: import_zod3.z.number().int().min(0)
|
|
4497
|
+
};
|
|
4498
|
+
var DirectoryMapsBusinessOutput = import_zod3.z.object({
|
|
4499
|
+
position: import_zod3.z.number().int().min(1),
|
|
4500
|
+
name: import_zod3.z.string(),
|
|
4501
|
+
placeUrl: import_zod3.z.string().url(),
|
|
4502
|
+
cid: NullableString2,
|
|
4503
|
+
cidDecimal: NullableString2,
|
|
4504
|
+
rating: NullableString2,
|
|
4505
|
+
reviewCount: NullableString2,
|
|
4506
|
+
category: NullableString2,
|
|
4507
|
+
address: NullableString2,
|
|
4508
|
+
phone: NullableString2,
|
|
4509
|
+
hoursStatus: NullableString2,
|
|
4510
|
+
websiteUrl: NullableString2,
|
|
4511
|
+
directionsUrl: NullableString2,
|
|
4512
|
+
metadata: import_zod3.z.array(import_zod3.z.string())
|
|
2855
4513
|
});
|
|
2856
4514
|
var DirectoryWorkflowOutputSchema = {
|
|
2857
|
-
query:
|
|
2858
|
-
state:
|
|
2859
|
-
minPopulation:
|
|
2860
|
-
populationYear:
|
|
2861
|
-
maxResultsPerCity:
|
|
2862
|
-
concurrency:
|
|
2863
|
-
censusSourceUrl:
|
|
2864
|
-
usZipsSourcePath:
|
|
2865
|
-
warnings:
|
|
2866
|
-
extractedAt:
|
|
2867
|
-
selectedCityCount:
|
|
2868
|
-
totalResultCount:
|
|
2869
|
-
csvPath:
|
|
2870
|
-
cities:
|
|
2871
|
-
city:
|
|
2872
|
-
state:
|
|
2873
|
-
location:
|
|
2874
|
-
cityKey:
|
|
2875
|
-
censusName:
|
|
2876
|
-
population:
|
|
2877
|
-
populationYear:
|
|
2878
|
-
zips:
|
|
2879
|
-
counties:
|
|
2880
|
-
status:
|
|
2881
|
-
error:
|
|
2882
|
-
resultCount:
|
|
2883
|
-
durationMs:
|
|
2884
|
-
attempts:
|
|
2885
|
-
results:
|
|
4515
|
+
query: import_zod3.z.string(),
|
|
4516
|
+
state: import_zod3.z.string(),
|
|
4517
|
+
minPopulation: import_zod3.z.number().int().min(0),
|
|
4518
|
+
populationYear: import_zod3.z.number().int().min(2020).max(2025),
|
|
4519
|
+
maxResultsPerCity: import_zod3.z.number().int().min(1).max(50),
|
|
4520
|
+
concurrency: import_zod3.z.number().int().min(1).max(5),
|
|
4521
|
+
censusSourceUrl: import_zod3.z.string().url(),
|
|
4522
|
+
usZipsSourcePath: NullableString2,
|
|
4523
|
+
warnings: import_zod3.z.array(import_zod3.z.string()),
|
|
4524
|
+
extractedAt: import_zod3.z.string(),
|
|
4525
|
+
selectedCityCount: import_zod3.z.number().int().min(0),
|
|
4526
|
+
totalResultCount: import_zod3.z.number().int().min(0),
|
|
4527
|
+
csvPath: NullableString2,
|
|
4528
|
+
cities: import_zod3.z.array(import_zod3.z.object({
|
|
4529
|
+
city: import_zod3.z.string(),
|
|
4530
|
+
state: import_zod3.z.string(),
|
|
4531
|
+
location: import_zod3.z.string(),
|
|
4532
|
+
cityKey: import_zod3.z.string(),
|
|
4533
|
+
censusName: import_zod3.z.string(),
|
|
4534
|
+
population: import_zod3.z.number().int().min(0),
|
|
4535
|
+
populationYear: import_zod3.z.number().int().min(2020).max(2025),
|
|
4536
|
+
zips: import_zod3.z.array(import_zod3.z.string()),
|
|
4537
|
+
counties: import_zod3.z.array(import_zod3.z.string()),
|
|
4538
|
+
status: import_zod3.z.enum(["ok", "empty", "failed"]),
|
|
4539
|
+
error: NullableString2,
|
|
4540
|
+
resultCount: import_zod3.z.number().int().min(0),
|
|
4541
|
+
durationMs: import_zod3.z.number().int().min(0),
|
|
4542
|
+
attempts: import_zod3.z.array(MapsSearchAttemptOutput),
|
|
4543
|
+
results: import_zod3.z.array(DirectoryMapsBusinessOutput)
|
|
2886
4544
|
})),
|
|
2887
|
-
durationMs:
|
|
4545
|
+
durationMs: import_zod3.z.number().int().min(0)
|
|
2888
4546
|
};
|
|
2889
|
-
var RankTrackerToolPlanOutput =
|
|
2890
|
-
tool:
|
|
2891
|
-
purpose:
|
|
4547
|
+
var RankTrackerToolPlanOutput = import_zod3.z.object({
|
|
4548
|
+
tool: import_zod3.z.string(),
|
|
4549
|
+
purpose: import_zod3.z.string()
|
|
2892
4550
|
});
|
|
2893
|
-
var RankTrackerTableOutput =
|
|
2894
|
-
name:
|
|
2895
|
-
purpose:
|
|
2896
|
-
keyColumns:
|
|
4551
|
+
var RankTrackerTableOutput = import_zod3.z.object({
|
|
4552
|
+
name: import_zod3.z.string(),
|
|
4553
|
+
purpose: import_zod3.z.string(),
|
|
4554
|
+
keyColumns: import_zod3.z.array(import_zod3.z.string())
|
|
2897
4555
|
});
|
|
2898
|
-
var RankTrackerCronJobOutput =
|
|
2899
|
-
name:
|
|
2900
|
-
purpose:
|
|
2901
|
-
modes:
|
|
2902
|
-
recommendedTools:
|
|
4556
|
+
var RankTrackerCronJobOutput = import_zod3.z.object({
|
|
4557
|
+
name: import_zod3.z.string(),
|
|
4558
|
+
purpose: import_zod3.z.string(),
|
|
4559
|
+
modes: import_zod3.z.array(RankTrackerModeSchema),
|
|
4560
|
+
recommendedTools: import_zod3.z.array(import_zod3.z.string())
|
|
2903
4561
|
});
|
|
2904
4562
|
var RankTrackerBlueprintOutputSchema = {
|
|
2905
|
-
projectName:
|
|
2906
|
-
targetDomain:
|
|
2907
|
-
targetBusinessName:
|
|
2908
|
-
trackingModes:
|
|
2909
|
-
database:
|
|
2910
|
-
recommendedTools:
|
|
2911
|
-
tables:
|
|
2912
|
-
cron:
|
|
2913
|
-
enabled:
|
|
2914
|
-
cadence:
|
|
2915
|
-
expression:
|
|
2916
|
-
timezone:
|
|
2917
|
-
jobs:
|
|
4563
|
+
projectName: import_zod3.z.string(),
|
|
4564
|
+
targetDomain: NullableString2,
|
|
4565
|
+
targetBusinessName: NullableString2,
|
|
4566
|
+
trackingModes: import_zod3.z.array(RankTrackerModeSchema),
|
|
4567
|
+
database: import_zod3.z.string(),
|
|
4568
|
+
recommendedTools: import_zod3.z.array(RankTrackerToolPlanOutput),
|
|
4569
|
+
tables: import_zod3.z.array(RankTrackerTableOutput),
|
|
4570
|
+
cron: import_zod3.z.object({
|
|
4571
|
+
enabled: import_zod3.z.boolean(),
|
|
4572
|
+
cadence: import_zod3.z.string(),
|
|
4573
|
+
expression: import_zod3.z.string(),
|
|
4574
|
+
timezone: import_zod3.z.string(),
|
|
4575
|
+
jobs: import_zod3.z.array(RankTrackerCronJobOutput)
|
|
2918
4576
|
}),
|
|
2919
|
-
metrics:
|
|
2920
|
-
implementationPrompt:
|
|
2921
|
-
};
|
|
2922
|
-
var OrganicResultOutput =
|
|
2923
|
-
position:
|
|
2924
|
-
title:
|
|
2925
|
-
url:
|
|
2926
|
-
domain:
|
|
2927
|
-
snippet:
|
|
4577
|
+
metrics: import_zod3.z.array(import_zod3.z.string()),
|
|
4578
|
+
implementationPrompt: import_zod3.z.string()
|
|
4579
|
+
};
|
|
4580
|
+
var OrganicResultOutput = import_zod3.z.object({
|
|
4581
|
+
position: import_zod3.z.number().int(),
|
|
4582
|
+
title: import_zod3.z.string(),
|
|
4583
|
+
url: import_zod3.z.string(),
|
|
4584
|
+
domain: import_zod3.z.string(),
|
|
4585
|
+
snippet: NullableString2
|
|
2928
4586
|
});
|
|
2929
|
-
var AiOverviewOutput =
|
|
2930
|
-
detected:
|
|
2931
|
-
text:
|
|
4587
|
+
var AiOverviewOutput = import_zod3.z.object({
|
|
4588
|
+
detected: import_zod3.z.boolean(),
|
|
4589
|
+
text: NullableString2
|
|
2932
4590
|
}).nullable();
|
|
2933
|
-
var EntityIdsOutput =
|
|
2934
|
-
kgIds:
|
|
2935
|
-
cids:
|
|
2936
|
-
gcids:
|
|
4591
|
+
var EntityIdsOutput = import_zod3.z.object({
|
|
4592
|
+
kgIds: import_zod3.z.array(import_zod3.z.string()),
|
|
4593
|
+
cids: import_zod3.z.array(import_zod3.z.string()),
|
|
4594
|
+
gcids: import_zod3.z.array(import_zod3.z.string())
|
|
2937
4595
|
}).nullable();
|
|
2938
4596
|
var HarvestPaaOutputSchema = {
|
|
2939
|
-
query:
|
|
2940
|
-
location:
|
|
2941
|
-
questionCount:
|
|
2942
|
-
completionStatus:
|
|
2943
|
-
questions:
|
|
2944
|
-
question:
|
|
2945
|
-
answer:
|
|
2946
|
-
sourceTitle:
|
|
2947
|
-
sourceSite:
|
|
4597
|
+
query: import_zod3.z.string(),
|
|
4598
|
+
location: NullableString2,
|
|
4599
|
+
questionCount: import_zod3.z.number().int().min(0),
|
|
4600
|
+
completionStatus: NullableString2,
|
|
4601
|
+
questions: import_zod3.z.array(import_zod3.z.object({
|
|
4602
|
+
question: import_zod3.z.string(),
|
|
4603
|
+
answer: NullableString2,
|
|
4604
|
+
sourceTitle: NullableString2,
|
|
4605
|
+
sourceSite: NullableString2
|
|
2948
4606
|
})),
|
|
2949
|
-
organicResults:
|
|
4607
|
+
organicResults: import_zod3.z.array(OrganicResultOutput),
|
|
2950
4608
|
aiOverview: AiOverviewOutput,
|
|
2951
4609
|
entityIds: EntityIdsOutput,
|
|
2952
|
-
durationMs:
|
|
4610
|
+
durationMs: import_zod3.z.number().min(0).nullable()
|
|
2953
4611
|
};
|
|
2954
4612
|
var SearchSerpOutputSchema = {
|
|
2955
|
-
query:
|
|
2956
|
-
location:
|
|
2957
|
-
organicResults:
|
|
2958
|
-
localPack:
|
|
2959
|
-
position:
|
|
2960
|
-
name:
|
|
2961
|
-
rating:
|
|
2962
|
-
reviewCount:
|
|
2963
|
-
websiteUrl:
|
|
4613
|
+
query: import_zod3.z.string(),
|
|
4614
|
+
location: NullableString2,
|
|
4615
|
+
organicResults: import_zod3.z.array(OrganicResultOutput),
|
|
4616
|
+
localPack: import_zod3.z.array(import_zod3.z.object({
|
|
4617
|
+
position: import_zod3.z.number().int(),
|
|
4618
|
+
name: import_zod3.z.string(),
|
|
4619
|
+
rating: NullableString2,
|
|
4620
|
+
reviewCount: NullableString2,
|
|
4621
|
+
websiteUrl: NullableString2
|
|
2964
4622
|
})),
|
|
2965
4623
|
aiOverview: AiOverviewOutput,
|
|
2966
4624
|
entityIds: EntityIdsOutput
|
|
2967
4625
|
};
|
|
2968
4626
|
var ExtractUrlOutputSchema = {
|
|
2969
|
-
url:
|
|
2970
|
-
title:
|
|
2971
|
-
headings:
|
|
2972
|
-
level:
|
|
2973
|
-
text:
|
|
4627
|
+
url: import_zod3.z.string(),
|
|
4628
|
+
title: NullableString2,
|
|
4629
|
+
headings: import_zod3.z.array(import_zod3.z.object({
|
|
4630
|
+
level: import_zod3.z.number().int(),
|
|
4631
|
+
text: import_zod3.z.string()
|
|
2974
4632
|
})),
|
|
2975
|
-
schemaBlockCount:
|
|
2976
|
-
entityName:
|
|
2977
|
-
entityTypes:
|
|
2978
|
-
napScore:
|
|
2979
|
-
missingSchemaFields:
|
|
2980
|
-
screenshotSaved:
|
|
4633
|
+
schemaBlockCount: import_zod3.z.number().int().min(0),
|
|
4634
|
+
entityName: NullableString2,
|
|
4635
|
+
entityTypes: import_zod3.z.array(import_zod3.z.string()),
|
|
4636
|
+
napScore: import_zod3.z.number().nullable(),
|
|
4637
|
+
missingSchemaFields: import_zod3.z.array(import_zod3.z.string()),
|
|
4638
|
+
screenshotSaved: NullableString2
|
|
2981
4639
|
};
|
|
2982
4640
|
var ExtractSiteOutputSchema = {
|
|
2983
|
-
url:
|
|
2984
|
-
pageCount:
|
|
2985
|
-
pages:
|
|
2986
|
-
url:
|
|
2987
|
-
title:
|
|
2988
|
-
schemaTypes:
|
|
4641
|
+
url: import_zod3.z.string(),
|
|
4642
|
+
pageCount: import_zod3.z.number().int().min(0),
|
|
4643
|
+
pages: import_zod3.z.array(import_zod3.z.object({
|
|
4644
|
+
url: import_zod3.z.string(),
|
|
4645
|
+
title: NullableString2,
|
|
4646
|
+
schemaTypes: import_zod3.z.array(import_zod3.z.string())
|
|
2989
4647
|
})),
|
|
2990
|
-
durationMs:
|
|
4648
|
+
durationMs: import_zod3.z.number().min(0)
|
|
2991
4649
|
};
|
|
2992
4650
|
var AuditSiteOutputSchema = {
|
|
2993
|
-
url:
|
|
2994
|
-
pageCount:
|
|
2995
|
-
durationMs:
|
|
2996
|
-
bulkFolder:
|
|
2997
|
-
issues:
|
|
2998
|
-
images:
|
|
2999
|
-
unique:
|
|
3000
|
-
totalBytes:
|
|
3001
|
-
over100kb:
|
|
3002
|
-
legacyFormat:
|
|
4651
|
+
url: import_zod3.z.string(),
|
|
4652
|
+
pageCount: import_zod3.z.number().int().min(0),
|
|
4653
|
+
durationMs: import_zod3.z.number().min(0),
|
|
4654
|
+
bulkFolder: import_zod3.z.string().nullable(),
|
|
4655
|
+
issues: import_zod3.z.record(import_zod3.z.string(), import_zod3.z.number()),
|
|
4656
|
+
images: import_zod3.z.object({
|
|
4657
|
+
unique: import_zod3.z.number().int().min(0),
|
|
4658
|
+
totalBytes: import_zod3.z.number().min(0),
|
|
4659
|
+
over100kb: import_zod3.z.number().int().min(0),
|
|
4660
|
+
legacyFormat: import_zod3.z.number().int().min(0)
|
|
4661
|
+
}),
|
|
4662
|
+
links: import_zod3.z.object({
|
|
4663
|
+
internal: import_zod3.z.number().int().min(0),
|
|
4664
|
+
external: import_zod3.z.number().int().min(0),
|
|
4665
|
+
orphans: import_zod3.z.number().int().min(0),
|
|
4666
|
+
brokenInternal: import_zod3.z.number().int().min(0),
|
|
4667
|
+
externalDomains: import_zod3.z.number().int().min(0)
|
|
3003
4668
|
})
|
|
3004
4669
|
};
|
|
3005
4670
|
var MapsPlaceIntelOutputSchema = {
|
|
3006
|
-
name:
|
|
3007
|
-
rating:
|
|
3008
|
-
reviewCount:
|
|
3009
|
-
category:
|
|
3010
|
-
address:
|
|
3011
|
-
phone:
|
|
3012
|
-
website:
|
|
3013
|
-
hoursSummary:
|
|
3014
|
-
bookingUrl:
|
|
3015
|
-
kgmid:
|
|
3016
|
-
cidDecimal:
|
|
3017
|
-
cidUrl:
|
|
3018
|
-
lat:
|
|
3019
|
-
lng:
|
|
3020
|
-
reviewsStatus:
|
|
3021
|
-
reviewsCollected:
|
|
3022
|
-
reviewTopics:
|
|
3023
|
-
label:
|
|
3024
|
-
count:
|
|
4671
|
+
name: import_zod3.z.string(),
|
|
4672
|
+
rating: NullableString2,
|
|
4673
|
+
reviewCount: NullableString2,
|
|
4674
|
+
category: NullableString2,
|
|
4675
|
+
address: NullableString2,
|
|
4676
|
+
phone: NullableString2,
|
|
4677
|
+
website: NullableString2,
|
|
4678
|
+
hoursSummary: NullableString2,
|
|
4679
|
+
bookingUrl: NullableString2,
|
|
4680
|
+
kgmid: NullableString2,
|
|
4681
|
+
cidDecimal: NullableString2,
|
|
4682
|
+
cidUrl: NullableString2,
|
|
4683
|
+
lat: import_zod3.z.number().nullable(),
|
|
4684
|
+
lng: import_zod3.z.number().nullable(),
|
|
4685
|
+
reviewsStatus: import_zod3.z.string(),
|
|
4686
|
+
reviewsCollected: import_zod3.z.number().int().min(0),
|
|
4687
|
+
reviewTopics: import_zod3.z.array(import_zod3.z.object({
|
|
4688
|
+
label: import_zod3.z.string(),
|
|
4689
|
+
count: import_zod3.z.string()
|
|
3025
4690
|
}))
|
|
3026
4691
|
};
|
|
3027
4692
|
var CreditsInfoOutputSchema = {
|
|
3028
|
-
balanceCredits:
|
|
3029
|
-
matchedCost:
|
|
3030
|
-
label:
|
|
3031
|
-
credits:
|
|
3032
|
-
unit:
|
|
3033
|
-
notes:
|
|
4693
|
+
balanceCredits: import_zod3.z.number().nullable(),
|
|
4694
|
+
matchedCost: import_zod3.z.object({
|
|
4695
|
+
label: import_zod3.z.string(),
|
|
4696
|
+
credits: import_zod3.z.number(),
|
|
4697
|
+
unit: import_zod3.z.string(),
|
|
4698
|
+
notes: NullableString2
|
|
3034
4699
|
}).nullable(),
|
|
3035
|
-
costs:
|
|
3036
|
-
key:
|
|
3037
|
-
label:
|
|
3038
|
-
credits:
|
|
3039
|
-
unit:
|
|
3040
|
-
notes:
|
|
4700
|
+
costs: import_zod3.z.array(import_zod3.z.object({
|
|
4701
|
+
key: import_zod3.z.string(),
|
|
4702
|
+
label: import_zod3.z.string(),
|
|
4703
|
+
credits: import_zod3.z.number(),
|
|
4704
|
+
unit: import_zod3.z.string(),
|
|
4705
|
+
notes: NullableString2
|
|
3041
4706
|
})),
|
|
3042
|
-
ledger:
|
|
3043
|
-
createdAt:
|
|
3044
|
-
operation:
|
|
3045
|
-
credits:
|
|
3046
|
-
description:
|
|
4707
|
+
ledger: import_zod3.z.array(import_zod3.z.object({
|
|
4708
|
+
createdAt: import_zod3.z.string(),
|
|
4709
|
+
operation: import_zod3.z.string(),
|
|
4710
|
+
credits: import_zod3.z.number(),
|
|
4711
|
+
description: NullableString2
|
|
3047
4712
|
})),
|
|
3048
|
-
concurrency:
|
|
3049
|
-
currentExtraSlots:
|
|
3050
|
-
currentLimit:
|
|
3051
|
-
hasSubscription:
|
|
3052
|
-
upgrade:
|
|
3053
|
-
product:
|
|
3054
|
-
priceLabel:
|
|
3055
|
-
unitAmountUsd:
|
|
3056
|
-
currency:
|
|
3057
|
-
interval:
|
|
3058
|
-
billingUrl:
|
|
3059
|
-
terminalCommand:
|
|
3060
|
-
terminalCommandWithApiKeyEnv:
|
|
4713
|
+
concurrency: import_zod3.z.object({
|
|
4714
|
+
currentExtraSlots: import_zod3.z.number().int().min(0),
|
|
4715
|
+
currentLimit: import_zod3.z.number().int().min(1),
|
|
4716
|
+
hasSubscription: import_zod3.z.boolean(),
|
|
4717
|
+
upgrade: import_zod3.z.object({
|
|
4718
|
+
product: import_zod3.z.string(),
|
|
4719
|
+
priceLabel: import_zod3.z.string(),
|
|
4720
|
+
unitAmountUsd: import_zod3.z.number(),
|
|
4721
|
+
currency: import_zod3.z.string(),
|
|
4722
|
+
interval: import_zod3.z.string(),
|
|
4723
|
+
billingUrl: import_zod3.z.string().url(),
|
|
4724
|
+
terminalCommand: import_zod3.z.string(),
|
|
4725
|
+
terminalCommandWithApiKeyEnv: import_zod3.z.string()
|
|
3061
4726
|
})
|
|
3062
4727
|
}).nullable()
|
|
3063
4728
|
};
|
|
3064
4729
|
var MapSiteUrlsOutputSchema = {
|
|
3065
|
-
startUrl:
|
|
3066
|
-
totalFound:
|
|
3067
|
-
truncated:
|
|
3068
|
-
okCount:
|
|
3069
|
-
redirectCount:
|
|
3070
|
-
brokenCount:
|
|
3071
|
-
urls:
|
|
3072
|
-
url:
|
|
3073
|
-
status:
|
|
4730
|
+
startUrl: import_zod3.z.string(),
|
|
4731
|
+
totalFound: import_zod3.z.number().int().min(0),
|
|
4732
|
+
truncated: import_zod3.z.boolean(),
|
|
4733
|
+
okCount: import_zod3.z.number().int().min(0),
|
|
4734
|
+
redirectCount: import_zod3.z.number().int().min(0),
|
|
4735
|
+
brokenCount: import_zod3.z.number().int().min(0),
|
|
4736
|
+
urls: import_zod3.z.array(import_zod3.z.object({
|
|
4737
|
+
url: import_zod3.z.string(),
|
|
4738
|
+
status: import_zod3.z.number().int().nullable()
|
|
3074
4739
|
})),
|
|
3075
|
-
durationMs:
|
|
4740
|
+
durationMs: import_zod3.z.number().min(0)
|
|
3076
4741
|
};
|
|
3077
4742
|
var YoutubeHarvestOutputSchema = {
|
|
3078
|
-
mode:
|
|
3079
|
-
videoCount:
|
|
3080
|
-
channel:
|
|
3081
|
-
title:
|
|
3082
|
-
subscriberCount:
|
|
4743
|
+
mode: import_zod3.z.string(),
|
|
4744
|
+
videoCount: import_zod3.z.number().int().min(0),
|
|
4745
|
+
channel: import_zod3.z.object({
|
|
4746
|
+
title: NullableString2,
|
|
4747
|
+
subscriberCount: NullableString2
|
|
3083
4748
|
}).nullable(),
|
|
3084
|
-
videos:
|
|
3085
|
-
videoId:
|
|
3086
|
-
title:
|
|
3087
|
-
channelName:
|
|
3088
|
-
views:
|
|
3089
|
-
duration:
|
|
3090
|
-
url:
|
|
4749
|
+
videos: import_zod3.z.array(import_zod3.z.object({
|
|
4750
|
+
videoId: import_zod3.z.string(),
|
|
4751
|
+
title: import_zod3.z.string(),
|
|
4752
|
+
channelName: NullableString2,
|
|
4753
|
+
views: NullableString2,
|
|
4754
|
+
duration: NullableString2,
|
|
4755
|
+
url: NullableString2
|
|
3091
4756
|
}))
|
|
3092
4757
|
};
|
|
3093
4758
|
var FacebookAdSearchOutputSchema = {
|
|
3094
|
-
query:
|
|
3095
|
-
advertiserCount:
|
|
3096
|
-
advertisers:
|
|
3097
|
-
name:
|
|
3098
|
-
pageId:
|
|
3099
|
-
pageUrl:
|
|
3100
|
-
adCount:
|
|
3101
|
-
libraryId:
|
|
3102
|
-
sampleLibraryId:
|
|
4759
|
+
query: import_zod3.z.string(),
|
|
4760
|
+
advertiserCount: import_zod3.z.number().int().min(0),
|
|
4761
|
+
advertisers: import_zod3.z.array(import_zod3.z.object({
|
|
4762
|
+
name: NullableString2,
|
|
4763
|
+
pageId: NullableString2,
|
|
4764
|
+
pageUrl: NullableString2,
|
|
4765
|
+
adCount: import_zod3.z.number().int().nullable(),
|
|
4766
|
+
libraryId: NullableString2,
|
|
4767
|
+
sampleLibraryId: NullableString2
|
|
3103
4768
|
}))
|
|
3104
4769
|
};
|
|
3105
4770
|
var FacebookPageIntelOutputSchema = {
|
|
3106
|
-
advertiserName:
|
|
3107
|
-
totalAds:
|
|
3108
|
-
activeCount:
|
|
3109
|
-
videoCount:
|
|
3110
|
-
imageCount:
|
|
3111
|
-
ads:
|
|
3112
|
-
libraryId:
|
|
3113
|
-
status:
|
|
3114
|
-
creativeType:
|
|
3115
|
-
primaryText:
|
|
3116
|
-
headline:
|
|
3117
|
-
cta:
|
|
3118
|
-
startDate:
|
|
3119
|
-
landingUrl:
|
|
3120
|
-
domain:
|
|
3121
|
-
videoUrl:
|
|
3122
|
-
imageUrl:
|
|
3123
|
-
videoPoster:
|
|
3124
|
-
variations:
|
|
4771
|
+
advertiserName: NullableString2,
|
|
4772
|
+
totalAds: import_zod3.z.number().int().min(0),
|
|
4773
|
+
activeCount: import_zod3.z.number().int().min(0),
|
|
4774
|
+
videoCount: import_zod3.z.number().int().min(0),
|
|
4775
|
+
imageCount: import_zod3.z.number().int().min(0),
|
|
4776
|
+
ads: import_zod3.z.array(import_zod3.z.object({
|
|
4777
|
+
libraryId: NullableString2,
|
|
4778
|
+
status: NullableString2,
|
|
4779
|
+
creativeType: NullableString2,
|
|
4780
|
+
primaryText: NullableString2,
|
|
4781
|
+
headline: NullableString2,
|
|
4782
|
+
cta: NullableString2,
|
|
4783
|
+
startDate: NullableString2,
|
|
4784
|
+
landingUrl: NullableString2,
|
|
4785
|
+
domain: NullableString2,
|
|
4786
|
+
videoUrl: NullableString2,
|
|
4787
|
+
imageUrl: NullableString2,
|
|
4788
|
+
videoPoster: NullableString2,
|
|
4789
|
+
variations: import_zod3.z.number().int().nullable()
|
|
3125
4790
|
}))
|
|
3126
4791
|
};
|
|
3127
4792
|
var FacebookVideoTranscribeOutputSchema = {
|
|
3128
|
-
sourceUrl:
|
|
3129
|
-
pageUrl:
|
|
3130
|
-
videoId:
|
|
3131
|
-
ownerName:
|
|
3132
|
-
selectedQuality:
|
|
3133
|
-
bitrate:
|
|
3134
|
-
videoDurationSec:
|
|
3135
|
-
videoUrl:
|
|
3136
|
-
wordCount:
|
|
3137
|
-
chunkCount:
|
|
3138
|
-
transcriptText:
|
|
3139
|
-
chunks:
|
|
3140
|
-
startSec:
|
|
3141
|
-
endSec:
|
|
3142
|
-
text:
|
|
4793
|
+
sourceUrl: import_zod3.z.string().url(),
|
|
4794
|
+
pageUrl: import_zod3.z.string().url(),
|
|
4795
|
+
videoId: NullableString2,
|
|
4796
|
+
ownerName: NullableString2,
|
|
4797
|
+
selectedQuality: import_zod3.z.string(),
|
|
4798
|
+
bitrate: import_zod3.z.number().int().nullable(),
|
|
4799
|
+
videoDurationSec: import_zod3.z.number().nullable(),
|
|
4800
|
+
videoUrl: import_zod3.z.string().url(),
|
|
4801
|
+
wordCount: import_zod3.z.number().int().min(0),
|
|
4802
|
+
chunkCount: import_zod3.z.number().int().min(0),
|
|
4803
|
+
transcriptText: import_zod3.z.string(),
|
|
4804
|
+
chunks: import_zod3.z.array(import_zod3.z.object({
|
|
4805
|
+
startSec: import_zod3.z.number(),
|
|
4806
|
+
endSec: import_zod3.z.number(),
|
|
4807
|
+
text: import_zod3.z.string()
|
|
3143
4808
|
}))
|
|
3144
4809
|
};
|
|
3145
|
-
var TranscriptChunkOutput =
|
|
3146
|
-
startSec:
|
|
3147
|
-
endSec:
|
|
3148
|
-
text:
|
|
4810
|
+
var TranscriptChunkOutput = import_zod3.z.object({
|
|
4811
|
+
startSec: import_zod3.z.number(),
|
|
4812
|
+
endSec: import_zod3.z.number(),
|
|
4813
|
+
text: import_zod3.z.string()
|
|
3149
4814
|
});
|
|
3150
|
-
var InstagramBrowserOutput =
|
|
3151
|
-
mode:
|
|
3152
|
-
requestedMode:
|
|
3153
|
-
profileName:
|
|
3154
|
-
profileSource:
|
|
3155
|
-
profileDirConfigured:
|
|
3156
|
-
executablePathConfigured:
|
|
4815
|
+
var InstagramBrowserOutput = import_zod3.z.object({
|
|
4816
|
+
mode: import_zod3.z.literal("hosted"),
|
|
4817
|
+
requestedMode: import_zod3.z.literal("hosted"),
|
|
4818
|
+
profileName: NullableString2,
|
|
4819
|
+
profileSource: import_zod3.z.literal("hosted"),
|
|
4820
|
+
profileDirConfigured: import_zod3.z.boolean(),
|
|
4821
|
+
executablePathConfigured: import_zod3.z.boolean()
|
|
3157
4822
|
});
|
|
3158
|
-
var InstagramPaginationOutput =
|
|
3159
|
-
maxItems:
|
|
3160
|
-
maxScrolls:
|
|
3161
|
-
attemptedScrolls:
|
|
3162
|
-
stableScrolls:
|
|
3163
|
-
stableScrollLimit:
|
|
3164
|
-
scrollDelayMs:
|
|
3165
|
-
reachedMaxItems:
|
|
3166
|
-
reachedReportedPostCount:
|
|
3167
|
-
finalScrollHeight:
|
|
3168
|
-
stoppedReason:
|
|
3169
|
-
stages:
|
|
3170
|
-
stage:
|
|
3171
|
-
itemCount:
|
|
3172
|
-
addedCount:
|
|
3173
|
-
scrollY:
|
|
3174
|
-
scrollHeight:
|
|
4823
|
+
var InstagramPaginationOutput = import_zod3.z.object({
|
|
4824
|
+
maxItems: import_zod3.z.number().int().min(1).max(2e3),
|
|
4825
|
+
maxScrolls: import_zod3.z.number().int().min(0).max(250),
|
|
4826
|
+
attemptedScrolls: import_zod3.z.number().int().min(0),
|
|
4827
|
+
stableScrolls: import_zod3.z.number().int().min(0),
|
|
4828
|
+
stableScrollLimit: import_zod3.z.number().int().min(1).max(10),
|
|
4829
|
+
scrollDelayMs: import_zod3.z.number().int().min(250).max(5e3),
|
|
4830
|
+
reachedMaxItems: import_zod3.z.boolean(),
|
|
4831
|
+
reachedReportedPostCount: import_zod3.z.boolean(),
|
|
4832
|
+
finalScrollHeight: import_zod3.z.number().int().nullable(),
|
|
4833
|
+
stoppedReason: import_zod3.z.enum(["max_items", "reported_post_count", "stable_scrolls", "max_scrolls", "no_scrolls"]),
|
|
4834
|
+
stages: import_zod3.z.array(import_zod3.z.object({
|
|
4835
|
+
stage: import_zod3.z.string(),
|
|
4836
|
+
itemCount: import_zod3.z.number().int().min(0),
|
|
4837
|
+
addedCount: import_zod3.z.number().int().min(0),
|
|
4838
|
+
scrollY: import_zod3.z.number().nullable(),
|
|
4839
|
+
scrollHeight: import_zod3.z.number().nullable()
|
|
3175
4840
|
}))
|
|
3176
4841
|
});
|
|
3177
4842
|
var InstagramProfileContentOutputSchema = {
|
|
3178
|
-
handle:
|
|
3179
|
-
profileUrl:
|
|
3180
|
-
pageUrl:
|
|
4843
|
+
handle: import_zod3.z.string(),
|
|
4844
|
+
profileUrl: import_zod3.z.string().url(),
|
|
4845
|
+
pageUrl: import_zod3.z.string().url(),
|
|
3181
4846
|
browser: InstagramBrowserOutput,
|
|
3182
|
-
profileName:
|
|
3183
|
-
reportedPostCount:
|
|
3184
|
-
reportedPostCountText:
|
|
3185
|
-
followerCountText:
|
|
3186
|
-
followingCountText:
|
|
3187
|
-
collectedContentCount:
|
|
3188
|
-
typeCounts:
|
|
3189
|
-
post:
|
|
3190
|
-
reel:
|
|
3191
|
-
tv:
|
|
4847
|
+
profileName: NullableString2,
|
|
4848
|
+
reportedPostCount: import_zod3.z.number().int().nullable(),
|
|
4849
|
+
reportedPostCountText: NullableString2,
|
|
4850
|
+
followerCountText: NullableString2,
|
|
4851
|
+
followingCountText: NullableString2,
|
|
4852
|
+
collectedContentCount: import_zod3.z.number().int().min(0),
|
|
4853
|
+
typeCounts: import_zod3.z.object({
|
|
4854
|
+
post: import_zod3.z.number().int().min(0),
|
|
4855
|
+
reel: import_zod3.z.number().int().min(0),
|
|
4856
|
+
tv: import_zod3.z.number().int().min(0)
|
|
3192
4857
|
}),
|
|
3193
4858
|
pagination: InstagramPaginationOutput,
|
|
3194
|
-
limited:
|
|
3195
|
-
limitations:
|
|
3196
|
-
items:
|
|
3197
|
-
url:
|
|
3198
|
-
type:
|
|
3199
|
-
shortcode:
|
|
3200
|
-
anchorText:
|
|
3201
|
-
firstSeenStage:
|
|
4859
|
+
limited: import_zod3.z.boolean(),
|
|
4860
|
+
limitations: import_zod3.z.array(import_zod3.z.string()),
|
|
4861
|
+
items: import_zod3.z.array(import_zod3.z.object({
|
|
4862
|
+
url: import_zod3.z.string().url(),
|
|
4863
|
+
type: import_zod3.z.enum(["post", "reel", "tv"]),
|
|
4864
|
+
shortcode: import_zod3.z.string(),
|
|
4865
|
+
anchorText: NullableString2,
|
|
4866
|
+
firstSeenStage: import_zod3.z.string()
|
|
3202
4867
|
}))
|
|
3203
4868
|
};
|
|
3204
|
-
var InstagramMediaTrackOutput =
|
|
3205
|
-
url:
|
|
3206
|
-
streamType:
|
|
3207
|
-
bitrate:
|
|
3208
|
-
durationSec:
|
|
3209
|
-
vencodeTag:
|
|
3210
|
-
width:
|
|
3211
|
-
height:
|
|
4869
|
+
var InstagramMediaTrackOutput = import_zod3.z.object({
|
|
4870
|
+
url: import_zod3.z.string().url(),
|
|
4871
|
+
streamType: import_zod3.z.enum(["video", "audio", "unknown"]),
|
|
4872
|
+
bitrate: import_zod3.z.number().int().nullable(),
|
|
4873
|
+
durationSec: import_zod3.z.number().nullable(),
|
|
4874
|
+
vencodeTag: NullableString2,
|
|
4875
|
+
width: import_zod3.z.number().int().nullable(),
|
|
4876
|
+
height: import_zod3.z.number().int().nullable()
|
|
3212
4877
|
});
|
|
3213
|
-
var InstagramDownloadOutput =
|
|
3214
|
-
kind:
|
|
3215
|
-
url:
|
|
3216
|
-
savedPath:
|
|
3217
|
-
sizeBytes:
|
|
3218
|
-
mimeType:
|
|
3219
|
-
error:
|
|
4878
|
+
var InstagramDownloadOutput = import_zod3.z.object({
|
|
4879
|
+
kind: import_zod3.z.enum(["text", "image", "video", "audio", "muxed_video"]),
|
|
4880
|
+
url: import_zod3.z.string().url().nullable(),
|
|
4881
|
+
savedPath: NullableString2,
|
|
4882
|
+
sizeBytes: import_zod3.z.number().int().nullable(),
|
|
4883
|
+
mimeType: NullableString2,
|
|
4884
|
+
error: NullableString2
|
|
3220
4885
|
});
|
|
3221
4886
|
var InstagramMediaDownloadOutputSchema = {
|
|
3222
|
-
sourceUrl:
|
|
3223
|
-
pageUrl:
|
|
4887
|
+
sourceUrl: import_zod3.z.string().url(),
|
|
4888
|
+
pageUrl: import_zod3.z.string().url(),
|
|
3224
4889
|
browser: InstagramBrowserOutput,
|
|
3225
|
-
type:
|
|
3226
|
-
shortcode:
|
|
3227
|
-
ownerName:
|
|
3228
|
-
caption:
|
|
3229
|
-
imageUrl:
|
|
3230
|
-
trackCount:
|
|
4890
|
+
type: import_zod3.z.enum(["post", "reel", "tv"]).nullable(),
|
|
4891
|
+
shortcode: NullableString2,
|
|
4892
|
+
ownerName: NullableString2,
|
|
4893
|
+
caption: NullableString2,
|
|
4894
|
+
imageUrl: import_zod3.z.string().url().nullable(),
|
|
4895
|
+
trackCount: import_zod3.z.number().int().min(0),
|
|
3231
4896
|
selectedVideoTrack: InstagramMediaTrackOutput.nullable(),
|
|
3232
4897
|
selectedAudioTrack: InstagramMediaTrackOutput.nullable(),
|
|
3233
|
-
downloads:
|
|
3234
|
-
outputDir:
|
|
3235
|
-
warnings:
|
|
3236
|
-
limitations:
|
|
3237
|
-
transcript:
|
|
3238
|
-
wordCount:
|
|
3239
|
-
chunkCount:
|
|
3240
|
-
durationMs:
|
|
3241
|
-
transcriptText:
|
|
3242
|
-
chunks:
|
|
4898
|
+
downloads: import_zod3.z.array(InstagramDownloadOutput),
|
|
4899
|
+
outputDir: NullableString2,
|
|
4900
|
+
warnings: import_zod3.z.array(import_zod3.z.string()),
|
|
4901
|
+
limitations: import_zod3.z.array(import_zod3.z.string()),
|
|
4902
|
+
transcript: import_zod3.z.object({
|
|
4903
|
+
wordCount: import_zod3.z.number().int().min(0),
|
|
4904
|
+
chunkCount: import_zod3.z.number().int().min(0),
|
|
4905
|
+
durationMs: import_zod3.z.number().nullable(),
|
|
4906
|
+
transcriptText: import_zod3.z.string(),
|
|
4907
|
+
chunks: import_zod3.z.array(TranscriptChunkOutput)
|
|
3243
4908
|
}).nullable()
|
|
3244
4909
|
};
|
|
3245
4910
|
var YoutubeTranscribeOutputSchema = {
|
|
3246
|
-
videoId:
|
|
3247
|
-
url:
|
|
3248
|
-
wordCount:
|
|
3249
|
-
chunkCount:
|
|
3250
|
-
durationMs:
|
|
3251
|
-
transcriptText:
|
|
3252
|
-
chunks:
|
|
3253
|
-
resolvedInputs:
|
|
3254
|
-
videoId:
|
|
3255
|
-
url:
|
|
4911
|
+
videoId: NullableString2,
|
|
4912
|
+
url: NullableString2,
|
|
4913
|
+
wordCount: import_zod3.z.number().int().min(0),
|
|
4914
|
+
chunkCount: import_zod3.z.number().int().min(0),
|
|
4915
|
+
durationMs: import_zod3.z.number().nullable(),
|
|
4916
|
+
transcriptText: import_zod3.z.string(),
|
|
4917
|
+
chunks: import_zod3.z.array(TranscriptChunkOutput),
|
|
4918
|
+
resolvedInputs: import_zod3.z.object({
|
|
4919
|
+
videoId: NullableString2,
|
|
4920
|
+
url: NullableString2
|
|
3256
4921
|
})
|
|
3257
4922
|
};
|
|
3258
4923
|
var FacebookAdTranscribeOutputSchema = {
|
|
3259
|
-
videoUrl:
|
|
3260
|
-
wordCount:
|
|
3261
|
-
chunkCount:
|
|
3262
|
-
durationMs:
|
|
3263
|
-
transcriptText:
|
|
3264
|
-
chunks:
|
|
3265
|
-
resolvedInputs:
|
|
3266
|
-
videoUrl:
|
|
4924
|
+
videoUrl: import_zod3.z.string().url(),
|
|
4925
|
+
wordCount: import_zod3.z.number().int().min(0),
|
|
4926
|
+
chunkCount: import_zod3.z.number().int().min(0),
|
|
4927
|
+
durationMs: import_zod3.z.number().nullable(),
|
|
4928
|
+
transcriptText: import_zod3.z.string(),
|
|
4929
|
+
chunks: import_zod3.z.array(TranscriptChunkOutput),
|
|
4930
|
+
resolvedInputs: import_zod3.z.object({
|
|
4931
|
+
videoUrl: import_zod3.z.string().url()
|
|
3267
4932
|
})
|
|
3268
4933
|
};
|
|
3269
4934
|
var CaptureSerpSnapshotOutputSchema = {
|
|
3270
|
-
schemaVersion:
|
|
3271
|
-
status:
|
|
3272
|
-
query:
|
|
3273
|
-
location:
|
|
3274
|
-
capturedAt:
|
|
3275
|
-
resultCount:
|
|
3276
|
-
snapshotId:
|
|
3277
|
-
resolvedInputs:
|
|
3278
|
-
artifacts:
|
|
3279
|
-
diagnostics:
|
|
3280
|
-
providerPayload:
|
|
4935
|
+
schemaVersion: import_zod3.z.literal("serp-intelligence.capture.v1"),
|
|
4936
|
+
status: import_zod3.z.string(),
|
|
4937
|
+
query: NullableString2,
|
|
4938
|
+
location: NullableString2,
|
|
4939
|
+
capturedAt: NullableString2,
|
|
4940
|
+
resultCount: import_zod3.z.number().int().min(0).nullable(),
|
|
4941
|
+
snapshotId: NullableString2,
|
|
4942
|
+
resolvedInputs: import_zod3.z.record(import_zod3.z.unknown()),
|
|
4943
|
+
artifacts: import_zod3.z.array(import_zod3.z.record(import_zod3.z.unknown())),
|
|
4944
|
+
diagnostics: import_zod3.z.record(import_zod3.z.unknown()).nullable(),
|
|
4945
|
+
providerPayload: import_zod3.z.record(import_zod3.z.unknown())
|
|
3281
4946
|
};
|
|
3282
4947
|
var CaptureSerpPageSnapshotsOutputSchema = {
|
|
3283
|
-
schemaVersion:
|
|
3284
|
-
status:
|
|
3285
|
-
count:
|
|
3286
|
-
failedCount:
|
|
3287
|
-
captures:
|
|
3288
|
-
resolvedInputs:
|
|
3289
|
-
diagnostics:
|
|
3290
|
-
providerPayload:
|
|
4948
|
+
schemaVersion: import_zod3.z.literal("serp-intelligence.page-snapshots.v1"),
|
|
4949
|
+
status: import_zod3.z.string(),
|
|
4950
|
+
count: import_zod3.z.number().int().min(0),
|
|
4951
|
+
failedCount: import_zod3.z.number().int().min(0),
|
|
4952
|
+
captures: import_zod3.z.array(import_zod3.z.record(import_zod3.z.unknown())),
|
|
4953
|
+
resolvedInputs: import_zod3.z.record(import_zod3.z.unknown()),
|
|
4954
|
+
diagnostics: import_zod3.z.record(import_zod3.z.unknown()).nullable(),
|
|
4955
|
+
providerPayload: import_zod3.z.record(import_zod3.z.unknown())
|
|
3291
4956
|
};
|
|
3292
4957
|
var CreditsInfoInputSchema = {
|
|
3293
|
-
item:
|
|
3294
|
-
includeLedger:
|
|
4958
|
+
item: import_zod3.z.string().optional().describe('Optional tool, action, or feature to look up, e.g. "maps reviews", "extract_url", "YouTube transcription", or "concurrency"'),
|
|
4959
|
+
includeLedger: import_zod3.z.boolean().default(false).describe("Whether to include recent credit ledger entries")
|
|
3295
4960
|
};
|
|
3296
|
-
var WorkflowIdSchema =
|
|
4961
|
+
var WorkflowIdSchema = import_zod3.z.enum([
|
|
3297
4962
|
"directory",
|
|
3298
4963
|
"agent-packet",
|
|
3299
4964
|
"local-competitive-audit",
|
|
@@ -3303,129 +4968,129 @@ var WorkflowIdSchema = import_zod2.z.enum([
|
|
|
3303
4968
|
"ai-overview-language"
|
|
3304
4969
|
]);
|
|
3305
4970
|
var WorkflowListInputSchema = {
|
|
3306
|
-
includeRecipes:
|
|
4971
|
+
includeRecipes: import_zod3.z.boolean().default(true).describe("Include high-level AI-facing recipes such as market analysis, ICP research, forum/review research, brand design briefings, CRO audits, positioning briefs, content gaps, and AI search visibility audits.")
|
|
3307
4972
|
};
|
|
3308
4973
|
var WorkflowSuggestInputSchema = {
|
|
3309
|
-
goal:
|
|
3310
|
-
query:
|
|
3311
|
-
keyword:
|
|
3312
|
-
domain:
|
|
3313
|
-
url:
|
|
3314
|
-
location:
|
|
3315
|
-
state:
|
|
3316
|
-
maxSuggestions:
|
|
4974
|
+
goal: import_zod3.z.string().min(1).describe('The user goal or job to route, e.g. "market analysis for roofers in Tennessee", "ICP research for med spas", "CRO audit for this URL", or "brand design briefing".'),
|
|
4975
|
+
query: import_zod3.z.string().optional().describe("Business category, niche, or Maps query when known."),
|
|
4976
|
+
keyword: import_zod3.z.string().optional().describe("Search keyword, audience problem, or content topic when known."),
|
|
4977
|
+
domain: import_zod3.z.string().optional().describe("Target domain or brand domain when known."),
|
|
4978
|
+
url: import_zod3.z.string().url().optional().describe("Target URL when the workflow should inspect a specific page."),
|
|
4979
|
+
location: import_zod3.z.string().optional().describe("City/region/country for localized research, e.g. Denver, CO."),
|
|
4980
|
+
state: import_zod3.z.string().optional().describe("US state abbreviation or name for state-wide market research."),
|
|
4981
|
+
maxSuggestions: import_zod3.z.number().int().min(1).max(8).default(3).describe("Number of matching workflow recipes to return.")
|
|
3317
4982
|
};
|
|
3318
4983
|
var WorkflowRunInputSchema = {
|
|
3319
4984
|
workflowId: WorkflowIdSchema.describe("Workflow to run: directory, agent-packet, local-competitive-audit, map-comparison, serp-comparison, paa-expansion-brief, ai-overview-language. Use only these values; call workflow_list or workflow_suggest first when unsure."),
|
|
3320
|
-
input:
|
|
3321
|
-
webhookUrl:
|
|
4985
|
+
input: import_zod3.z.record(import_zod3.z.unknown()).default({}).describe("Workflow-specific input object. Examples: agent-packet uses {keyword, domain?, location?, maxQuestions?}; local-competitive-audit uses {query, state, minPopulation?, maxCities?, maxResultsPerCity?, hydrateTop?, maxReviews?}; serp-comparison uses {keyword, domain?, url?, location?, extractTop?}."),
|
|
4986
|
+
webhookUrl: import_zod3.z.string().url().optional().describe("Optional HTTPS webhook to receive the completed hosted workflow run event.")
|
|
3322
4987
|
};
|
|
3323
4988
|
var WorkflowStepInputSchema = {
|
|
3324
|
-
runId:
|
|
4989
|
+
runId: import_zod3.z.string().min(1).describe("Workflow run id returned by workflow_run, workflow_step, or workflow_status. Use only a returned runId; do not construct one yourself. Advances the run by exactly one step (one logical leg, e.g. one live harvest).")
|
|
3325
4990
|
};
|
|
3326
4991
|
var WorkflowStatusInputSchema = {
|
|
3327
|
-
runId:
|
|
4992
|
+
runId: import_zod3.z.string().min(1).describe("Workflow run id returned by workflow_run, workflow_step, or workflow_status. Use only a returned runId; do not construct one yourself.")
|
|
3328
4993
|
};
|
|
3329
4994
|
var WorkflowArtifactReadInputSchema = {
|
|
3330
|
-
runId:
|
|
3331
|
-
artifactId:
|
|
3332
|
-
maxBytes:
|
|
3333
|
-
};
|
|
3334
|
-
var WorkflowRecipeOutput =
|
|
3335
|
-
id:
|
|
3336
|
-
title:
|
|
3337
|
-
description:
|
|
3338
|
-
primaryWorkflowId:
|
|
3339
|
-
recommendedTools:
|
|
3340
|
-
requiredInputs:
|
|
3341
|
-
optionalInputs:
|
|
3342
|
-
produces:
|
|
3343
|
-
runHint:
|
|
4995
|
+
runId: import_zod3.z.string().min(1).describe("Workflow run id returned by workflow_run, workflow_step, or workflow_status. Use only a returned runId; do not construct one yourself."),
|
|
4996
|
+
artifactId: import_zod3.z.string().min(1).describe("Artifact id from the run artifact list returned by workflow_run, workflow_step, or workflow_status. Use only a returned artifactId; do not construct one yourself."),
|
|
4997
|
+
maxBytes: import_zod3.z.number().int().min(1e3).max(1e6).default(2e5).describe("Maximum bytes of artifact text to return inline. Use lower values for large CSV/JSON artifacts; call again with the downloadUrl if needed outside MCP.")
|
|
4998
|
+
};
|
|
4999
|
+
var WorkflowRecipeOutput = import_zod3.z.object({
|
|
5000
|
+
id: import_zod3.z.string(),
|
|
5001
|
+
title: import_zod3.z.string(),
|
|
5002
|
+
description: import_zod3.z.string(),
|
|
5003
|
+
primaryWorkflowId: import_zod3.z.string().nullable(),
|
|
5004
|
+
recommendedTools: import_zod3.z.array(import_zod3.z.string()),
|
|
5005
|
+
requiredInputs: import_zod3.z.array(import_zod3.z.string()),
|
|
5006
|
+
optionalInputs: import_zod3.z.array(import_zod3.z.string()),
|
|
5007
|
+
produces: import_zod3.z.array(import_zod3.z.string()),
|
|
5008
|
+
runHint: import_zod3.z.string()
|
|
3344
5009
|
});
|
|
3345
|
-
var WorkflowDefinitionOutput =
|
|
3346
|
-
id:
|
|
3347
|
-
title:
|
|
3348
|
-
description:
|
|
5010
|
+
var WorkflowDefinitionOutput = import_zod3.z.object({
|
|
5011
|
+
id: import_zod3.z.string(),
|
|
5012
|
+
title: import_zod3.z.string(),
|
|
5013
|
+
description: import_zod3.z.string()
|
|
3349
5014
|
});
|
|
3350
|
-
var WorkflowArtifactOutput =
|
|
5015
|
+
var WorkflowArtifactOutput = import_zod3.z.record(import_zod3.z.unknown());
|
|
3351
5016
|
var WorkflowListOutputSchema = {
|
|
3352
|
-
workflows:
|
|
3353
|
-
recipes:
|
|
5017
|
+
workflows: import_zod3.z.array(WorkflowDefinitionOutput),
|
|
5018
|
+
recipes: import_zod3.z.array(WorkflowRecipeOutput)
|
|
3354
5019
|
};
|
|
3355
5020
|
var WorkflowSuggestOutputSchema = {
|
|
3356
|
-
goal:
|
|
3357
|
-
suggestions:
|
|
5021
|
+
goal: import_zod3.z.string(),
|
|
5022
|
+
suggestions: import_zod3.z.array(WorkflowRecipeOutput)
|
|
3358
5023
|
};
|
|
3359
5024
|
var WorkflowRunOutputSchema = {
|
|
3360
|
-
workflowId:
|
|
3361
|
-
input:
|
|
3362
|
-
run:
|
|
3363
|
-
summary:
|
|
3364
|
-
step:
|
|
3365
|
-
nextStep:
|
|
3366
|
-
done:
|
|
3367
|
-
artifacts:
|
|
5025
|
+
workflowId: import_zod3.z.string(),
|
|
5026
|
+
input: import_zod3.z.record(import_zod3.z.unknown()),
|
|
5027
|
+
run: import_zod3.z.record(import_zod3.z.unknown()).optional(),
|
|
5028
|
+
summary: import_zod3.z.record(import_zod3.z.unknown()).optional(),
|
|
5029
|
+
step: import_zod3.z.record(import_zod3.z.unknown()).optional(),
|
|
5030
|
+
nextStep: import_zod3.z.record(import_zod3.z.unknown()).nullable().optional(),
|
|
5031
|
+
done: import_zod3.z.boolean().optional(),
|
|
5032
|
+
artifacts: import_zod3.z.array(WorkflowArtifactOutput)
|
|
3368
5033
|
};
|
|
3369
5034
|
var WorkflowStepOutputSchema = {
|
|
3370
|
-
runId:
|
|
3371
|
-
run:
|
|
3372
|
-
summary:
|
|
3373
|
-
step:
|
|
3374
|
-
nextStep:
|
|
3375
|
-
done:
|
|
3376
|
-
artifacts:
|
|
5035
|
+
runId: import_zod3.z.string(),
|
|
5036
|
+
run: import_zod3.z.record(import_zod3.z.unknown()).optional(),
|
|
5037
|
+
summary: import_zod3.z.record(import_zod3.z.unknown()).nullable().optional(),
|
|
5038
|
+
step: import_zod3.z.record(import_zod3.z.unknown()).optional(),
|
|
5039
|
+
nextStep: import_zod3.z.record(import_zod3.z.unknown()).nullable().optional(),
|
|
5040
|
+
done: import_zod3.z.boolean(),
|
|
5041
|
+
artifacts: import_zod3.z.array(WorkflowArtifactOutput)
|
|
3377
5042
|
};
|
|
3378
5043
|
var WorkflowStatusOutputSchema = {
|
|
3379
|
-
run:
|
|
3380
|
-
artifacts:
|
|
5044
|
+
run: import_zod3.z.record(import_zod3.z.unknown()).optional(),
|
|
5045
|
+
artifacts: import_zod3.z.array(WorkflowArtifactOutput)
|
|
3381
5046
|
};
|
|
3382
5047
|
var WorkflowArtifactReadOutputSchema = {
|
|
3383
|
-
runId:
|
|
3384
|
-
artifactId:
|
|
3385
|
-
contentType:
|
|
3386
|
-
bytes:
|
|
3387
|
-
truncated:
|
|
3388
|
-
text:
|
|
5048
|
+
runId: import_zod3.z.string(),
|
|
5049
|
+
artifactId: import_zod3.z.string(),
|
|
5050
|
+
contentType: import_zod3.z.string(),
|
|
5051
|
+
bytes: import_zod3.z.number().int().min(0),
|
|
5052
|
+
truncated: import_zod3.z.boolean(),
|
|
5053
|
+
text: import_zod3.z.string()
|
|
3389
5054
|
};
|
|
3390
5055
|
var SearchSerpInputSchema = {
|
|
3391
|
-
query:
|
|
3392
|
-
location:
|
|
3393
|
-
gl:
|
|
3394
|
-
hl:
|
|
3395
|
-
device:
|
|
3396
|
-
proxyMode:
|
|
3397
|
-
proxyZip:
|
|
3398
|
-
debug:
|
|
3399
|
-
pages:
|
|
5056
|
+
query: import_zod3.z.string().min(1).describe('Core search topic only. Separate location when possible. If user says "best dentist in Brooklyn NY serp", use query="best dentist" and location="Brooklyn, NY".'),
|
|
5057
|
+
location: import_zod3.z.string().optional().describe("City, region, or country for geo-targeted results, inferred from user request when present."),
|
|
5058
|
+
gl: import_zod3.z.string().length(2).default("us").describe("Google country code inferred from location or user language."),
|
|
5059
|
+
hl: import_zod3.z.string().default("en").describe("Google interface/content language inferred from user request."),
|
|
5060
|
+
device: import_zod3.z.enum(["desktop", "mobile"]).default("desktop").describe("SERP device context. Use desktop by default; use mobile only when the user asks for mobile rankings."),
|
|
5061
|
+
proxyMode: import_zod3.z.enum(["location", "configured", "none"]).default(DEFAULT_PROXY_MODE).describe("Proxy targeting mode. Default configured uses the service proxy without city/ZIP targeting for the highest general SERP success rate. Use location only when the user explicitly needs city/ZIP-targeted residential proxy evidence. Use none only for direct-network debugging."),
|
|
5062
|
+
proxyZip: import_zod3.z.string().regex(/^\d{5}$/).optional().describe("Optional US ZIP override for residential location proxy targeting. Use only with proxyMode location when the user gives a specific ZIP or city-center targeting needs to be forced."),
|
|
5063
|
+
debug: import_zod3.z.boolean().default(false).describe("Include sanitized browser/session/location diagnostics in the response. Use true when debugging localization, CAPTCHA, or proxy behavior."),
|
|
5064
|
+
pages: import_zod3.z.number().int().min(1).max(2).default(1).describe("Number of result pages to fetch (1\u20132)")
|
|
3400
5065
|
};
|
|
3401
5066
|
var CaptureSerpSnapshotInputSchema = {
|
|
3402
|
-
query:
|
|
3403
|
-
location:
|
|
3404
|
-
gl:
|
|
3405
|
-
hl:
|
|
3406
|
-
device:
|
|
3407
|
-
proxyMode:
|
|
3408
|
-
proxyZip:
|
|
3409
|
-
pages:
|
|
3410
|
-
debug:
|
|
3411
|
-
includePageSnapshots:
|
|
3412
|
-
pageSnapshotLimit:
|
|
5067
|
+
query: import_zod3.z.string().min(1).describe("Core search query to capture as a structured SERP Intelligence snapshot. Separate the place into location when the user gives a city, region, country, or ZIP."),
|
|
5068
|
+
location: import_zod3.z.string().optional().describe("City, region, country, or service area used for localized Google results. MCP Scraper records location evidence; UULE alone is not proof of localization."),
|
|
5069
|
+
gl: import_zod3.z.string().length(2).default("us").describe("Google country code inferred from the requested market, e.g. us, gb, ca, au."),
|
|
5070
|
+
hl: import_zod3.z.string().default("en").describe("Google interface/content language inferred from the user request."),
|
|
5071
|
+
device: import_zod3.z.enum(["desktop", "mobile"]).default("desktop").describe("SERP device context. Use mobile only when the user asks for mobile rankings or mobile SERP evidence."),
|
|
5072
|
+
proxyMode: import_zod3.z.enum(["location", "configured", "none"]).default(DEFAULT_PROXY_MODE).describe("Proxy behavior for capture. Default configured uses the service proxy without city/ZIP targeting for the highest general capture success rate. Use location only when the user explicitly needs city/ZIP-targeted residential proxy evidence. Use none only for direct-network debugging."),
|
|
5073
|
+
proxyZip: import_zod3.z.string().regex(/^\d{5}$/).optional().describe("Optional US ZIP override for residential location proxy targeting. Use only with proxyMode location when a precise city-center or ZIP proxy is needed."),
|
|
5074
|
+
pages: import_zod3.z.number().int().min(1).max(2).default(1).describe("Number of Google result pages to capture. Use 1 normally and 2 only when the user needs deeper ranking evidence."),
|
|
5075
|
+
debug: import_zod3.z.boolean().default(false).describe("Include sanitized browser, proxy, and location diagnostics. Use true when debugging localization, CAPTCHA, proxy selection, or capture reliability."),
|
|
5076
|
+
includePageSnapshots: import_zod3.z.boolean().default(false).describe("Also capture ranking-page snapshots for selected SERP URLs through the same product capture path."),
|
|
5077
|
+
pageSnapshotLimit: import_zod3.z.number().int().min(0).max(10).default(0).describe("Maximum ranking-page snapshots to capture when includePageSnapshots is true. Use 0 when only SERP evidence is needed.")
|
|
3413
5078
|
};
|
|
3414
5079
|
var ScreenshotInputSchema = {
|
|
3415
|
-
url:
|
|
3416
|
-
device:
|
|
3417
|
-
allowLocal:
|
|
5080
|
+
url: import_zod3.z.string().url().describe("URL to capture as a full-page screenshot. Use http or https. Pass allowLocal: true to capture localhost or private-network URLs during development."),
|
|
5081
|
+
device: import_zod3.z.enum(["desktop", "mobile"]).default("desktop").describe("Viewport profile. desktop = 1440\xD7900. mobile = 390\xD7844. Use desktop by default; use mobile when the user asks for a mobile view."),
|
|
5082
|
+
allowLocal: import_zod3.z.boolean().default(false).describe("Allow localhost and private-network URLs (127.x, 192.168.x, 10.x, etc.). For local development only \u2014 not for production use.")
|
|
3418
5083
|
};
|
|
3419
5084
|
var CaptureSerpPageSnapshotsInputSchema = {
|
|
3420
|
-
urls:
|
|
3421
|
-
targets:
|
|
3422
|
-
url:
|
|
3423
|
-
sourceKind:
|
|
3424
|
-
sourcePosition:
|
|
5085
|
+
urls: import_zod3.z.array(import_zod3.z.string().url()).min(1).max(25).describe("Public HTTP/HTTPS URLs to capture as SERP Intelligence page snapshots. Do not pass localhost, private IPs, file URLs, or internal admin URLs."),
|
|
5086
|
+
targets: import_zod3.z.array(import_zod3.z.object({
|
|
5087
|
+
url: import_zod3.z.string().url().describe("Public HTTP/HTTPS URL to capture."),
|
|
5088
|
+
sourceKind: import_zod3.z.enum(["organic", "ai_citation", "local_pack_website", "configured_target", "site_subject"]).default("configured_target").describe("Why this page is being captured for SERP Intelligence evidence."),
|
|
5089
|
+
sourcePosition: import_zod3.z.number().int().min(1).optional().describe("Ranking or citation position when the page came from SERP evidence.")
|
|
3425
5090
|
}).strict()).min(1).max(25).optional().describe("Structured page snapshot targets. Use this instead of urls when source kind or position should be preserved."),
|
|
3426
|
-
maxConcurrency:
|
|
3427
|
-
timeoutMs:
|
|
3428
|
-
debug:
|
|
5091
|
+
maxConcurrency: import_zod3.z.number().int().min(1).max(5).default(2).describe("Parallel page captures. Use 2 normally; higher values can increase proxy/browser pressure."),
|
|
5092
|
+
timeoutMs: import_zod3.z.number().int().min(1e3).max(6e4).default(15e3).describe("Per-page capture timeout in milliseconds. Increase for slow pages; timeout artifacts are returned as structured capture failures."),
|
|
5093
|
+
debug: import_zod3.z.boolean().default(false).describe("Include sanitized browser/proxy diagnostics for page snapshot debugging. Use true for capture, network, or proxy troubleshooting.")
|
|
3429
5094
|
};
|
|
3430
5095
|
|
|
3431
5096
|
// src/mcp/rank-tracker-blueprint.ts
|
|
@@ -3772,8 +5437,8 @@ function localPlanningToolAnnotations(title) {
|
|
|
3772
5437
|
}
|
|
3773
5438
|
function listSavedReports() {
|
|
3774
5439
|
try {
|
|
3775
|
-
const dir =
|
|
3776
|
-
return (0,
|
|
5440
|
+
const dir = outputBaseDir3();
|
|
5441
|
+
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);
|
|
3777
5442
|
} catch {
|
|
3778
5443
|
return [];
|
|
3779
5444
|
}
|
|
@@ -3781,7 +5446,7 @@ function listSavedReports() {
|
|
|
3781
5446
|
function registerSavedReportResources(server2) {
|
|
3782
5447
|
server2.registerResource(
|
|
3783
5448
|
"saved-report",
|
|
3784
|
-
new
|
|
5449
|
+
new import_mcp2.ResourceTemplate("report://{filename}", {
|
|
3785
5450
|
list: () => ({
|
|
3786
5451
|
resources: listSavedReports().map((r) => ({
|
|
3787
5452
|
uri: `report://${encodeURIComponent(r.filename)}`,
|
|
@@ -3797,19 +5462,14 @@ function registerSavedReportResources(server2) {
|
|
|
3797
5462
|
},
|
|
3798
5463
|
async (uri, variables) => {
|
|
3799
5464
|
const requested = Array.isArray(variables.filename) ? variables.filename[0] : variables.filename;
|
|
3800
|
-
const filename = (0,
|
|
5465
|
+
const filename = (0, import_node_path5.basename)(decodeURIComponent(String(requested ?? "")));
|
|
3801
5466
|
if (!filename.endsWith(".md")) throw new Error("Only saved .md reports can be read");
|
|
3802
|
-
const text = (0,
|
|
5467
|
+
const text = (0, import_node_fs4.readFileSync)((0, import_node_path5.join)(outputBaseDir3(), filename), "utf8");
|
|
3803
5468
|
return { contents: [{ uri: uri.href, mimeType: "text/markdown", text }] };
|
|
3804
5469
|
}
|
|
3805
5470
|
);
|
|
3806
5471
|
}
|
|
3807
|
-
function
|
|
3808
|
-
const server2 = new import_mcp.McpServer({ name: "mcp-scraper", version: PACKAGE_VERSION }, { instructions: SERVER_INSTRUCTIONS });
|
|
3809
|
-
registerPaaExtractorMcpTools(server2, executor2, options);
|
|
3810
|
-
return server2;
|
|
3811
|
-
}
|
|
3812
|
-
function registerPaaExtractorMcpTools(server2, executor2, options = {}) {
|
|
5472
|
+
function registerPaaExtractorMcpTools(server2, executor, options = {}) {
|
|
3813
5473
|
const savesReports = options.savesReportsLocally !== false;
|
|
3814
5474
|
const reportNote = savesReports ? " Saves a full Markdown report to disk." : " Reports are returned inline; no files are saved on this hosted endpoint.";
|
|
3815
5475
|
const withReportNote = (description) => `${description}${reportNote}`;
|
|
@@ -3820,126 +5480,126 @@ function registerPaaExtractorMcpTools(server2, executor2, options = {}) {
|
|
|
3820
5480
|
inputSchema: HarvestPaaInputSchema,
|
|
3821
5481
|
outputSchema: HarvestPaaOutputSchema,
|
|
3822
5482
|
annotations: liveWebToolAnnotations("Google PAA + SERP Harvest")
|
|
3823
|
-
}, async (input) => formatHarvestPaa(await
|
|
5483
|
+
}, async (input) => formatHarvestPaa(await executor.harvestPaa(input), input));
|
|
3824
5484
|
server2.registerTool("search_serp", {
|
|
3825
5485
|
title: "Google SERP Lookup",
|
|
3826
5486
|
description: withReportNote("Fast Google SERP lookup without PAA expansion. Use when the user asks for rankings, organic results, local pack, quick SERP, or positions. Split topic from location and infer gl/hl from the user request. Omit proxyMode for normal use; the service defaults to the configured browser-service proxy without city/ZIP targeting. Use proxyMode location and proxyZip only when the user explicitly needs city/ZIP-targeted residential proxy evidence."),
|
|
3827
5487
|
inputSchema: SearchSerpInputSchema,
|
|
3828
5488
|
outputSchema: SearchSerpOutputSchema,
|
|
3829
5489
|
annotations: liveWebToolAnnotations("Google SERP Lookup")
|
|
3830
|
-
}, async (input) => formatSearchSerp(await
|
|
5490
|
+
}, async (input) => formatSearchSerp(await executor.searchSerp(input), input));
|
|
3831
5491
|
server2.registerTool("extract_url", {
|
|
3832
5492
|
title: "Single URL Extract",
|
|
3833
5493
|
description: withReportNote("Extract structured data from one public URL when the user provides one page, asks to inspect/scrape a page, or needs page content, schema, headings, metadata, screenshots, branding, or media assets. Returns structured page fields plus artifact handles for saved reports/screenshots/media when requested. Use map_site_urls before extracting a site inventory; use extract_site for multi-page crawling."),
|
|
3834
5494
|
inputSchema: ExtractUrlInputSchema,
|
|
3835
5495
|
outputSchema: ExtractUrlOutputSchema,
|
|
3836
5496
|
annotations: liveWebToolAnnotations("Single URL Extract")
|
|
3837
|
-
}, async (input) => formatExtractUrl(await
|
|
5497
|
+
}, async (input) => formatExtractUrl(await executor.extractUrl(input), input));
|
|
3838
5498
|
server2.registerTool("map_site_urls", {
|
|
3839
5499
|
title: "Site URL Map",
|
|
3840
5500
|
description: withReportNote("Map/crawl a public website when the user asks for a sitemap, URL inventory, broken-link scan, redirect scan, or crawl planning. Returns internal URLs with HTTP status and truncation metadata. Supports up to maxUrls=10000; maps over 500 URLs write the complete inventory to a local CSV file and return a summary plus the file path instead of the full list inline. Use this before extract_site when choosing pages for an audit; use extract_url for one known page."),
|
|
3841
5501
|
inputSchema: MapSiteUrlsInputSchema,
|
|
3842
5502
|
outputSchema: MapSiteUrlsOutputSchema,
|
|
3843
5503
|
annotations: liveWebToolAnnotations("Site URL Map")
|
|
3844
|
-
}, async (input) => formatMapSiteUrls(await
|
|
5504
|
+
}, async (input) => formatMapSiteUrls(await executor.mapSiteUrls(input), input));
|
|
3845
5505
|
server2.registerTool("extract_site", {
|
|
3846
5506
|
title: "Multi-Page Site Content Crawl",
|
|
3847
5507
|
description: withReportNote("Crawl a public website and return the page CONTENT across multiple pages (map + scrape). Returns per-page titles and Markdown content. Supports up to maxPages=10000; bulk crawls over 25 pages switch to folder mode: each page is saved as its own Markdown file in a local folder and the response returns a summary plus the folder path instead of inlining all content. This tool returns content only \u2014 for a technical SEO audit (issues, internal link graph, indexability, heading breakdown, image sizes/formats) use audit_site. Use map_site_urls first when URL selection matters; use extract_url for one page."),
|
|
3848
5508
|
inputSchema: ExtractSiteInputSchema,
|
|
3849
5509
|
outputSchema: ExtractSiteOutputSchema,
|
|
3850
5510
|
annotations: liveWebToolAnnotations("Multi-Page Site Content Crawl")
|
|
3851
|
-
}, async (input) => formatExtractSite(await
|
|
5511
|
+
}, async (input) => formatExtractSite(await executor.extractSite(input), input));
|
|
3852
5512
|
server2.registerTool("audit_site", {
|
|
3853
5513
|
title: "Technical SEO Audit",
|
|
3854
|
-
description: withReportNote("Run a full technical SEO audit (Screaming-Frog-style) on a public website. Crawls up to maxPages and produces a complete on-page + crawl analysis: per-page title/meta lengths, heading breakdown, indexability and canonical status, schema, an internal link graph
|
|
5514
|
+
description: withReportNote("Run a full technical SEO audit (Screaming-Frog-style) on a public website. Crawls up to maxPages and produces a complete on-page + crawl analysis: per-page title/meta lengths, heading breakdown, indexability and canonical status, schema, an internal link graph (inlinks/orphans/crawl-depth), a link analysis (top internal pages by inlinks, top external domains by link count, broken/orphan counts), an issues report, and an image audit (byte size, format, over-100KB and legacy-format flags). Writes everything to a local folder (report.md, issues.json, pages.jsonl, links.jsonl, link-metrics.jsonl, link-report.md, links-summary.json, external-domains.json, images.jsonl) plus per-page content, and returns a headline summary plus the folder path. Use this when the user wants an audit or analysis; use extract_site when they just want page content."),
|
|
3855
5515
|
inputSchema: AuditSiteInputSchema,
|
|
3856
5516
|
outputSchema: AuditSiteOutputSchema,
|
|
3857
5517
|
annotations: liveWebToolAnnotations("Technical SEO Audit")
|
|
3858
|
-
}, async (input) => formatAuditSite(await
|
|
5518
|
+
}, async (input) => formatAuditSite(await executor.auditSite(input), input));
|
|
3859
5519
|
server2.registerTool("youtube_harvest", {
|
|
3860
5520
|
title: "YouTube Video Harvest",
|
|
3861
5521
|
description: withReportNote('Harvest YouTube video metadata when the user wants to find videos by topic, inspect a channel library, compare video angles, or get videoIds for later transcription. Use mode "search" for keyword/topic requests and mode "channel" for @handles, channel IDs, or channel URLs. Returns titles, views, durations, URLs, and videoIds for follow-up transcription. Use youtube_transcribe after selecting one video.'),
|
|
3862
5522
|
inputSchema: YoutubeHarvestInputSchema,
|
|
3863
5523
|
outputSchema: YoutubeHarvestOutputSchema,
|
|
3864
5524
|
annotations: liveWebToolAnnotations("YouTube Video Harvest")
|
|
3865
|
-
}, async (input) => formatYoutubeHarvest(await
|
|
5525
|
+
}, async (input) => formatYoutubeHarvest(await executor.youtubeHarvest(input), input));
|
|
3866
5526
|
server2.registerTool("youtube_transcribe", {
|
|
3867
5527
|
title: "YouTube Transcription",
|
|
3868
5528
|
description: withReportNote("Fetch and transcribe captions from a YouTube video. Use this when the user asks what was said in a YouTube video, wants claims/offers/lessons extracted from a video, or provides a YouTube URL for transcript work. Returns full transcript, timestamped chunks, word count, and resolvedInputs. Pass videoId from youtube_harvest results, or pass url when the user pasted a YouTube URL. Use youtube_harvest first when you need to discover videos by topic/channel."),
|
|
3869
5529
|
inputSchema: YoutubeTranscribeInputSchema,
|
|
3870
5530
|
outputSchema: YoutubeTranscribeOutputSchema,
|
|
3871
5531
|
annotations: liveWebToolAnnotations("YouTube Transcription")
|
|
3872
|
-
}, async (input) => formatYoutubeTranscribe(await
|
|
5532
|
+
}, async (input) => formatYoutubeTranscribe(await executor.youtubeTranscribe(input), input));
|
|
3873
5533
|
server2.registerTool("facebook_page_intel", {
|
|
3874
5534
|
title: "Facebook Advertiser Ad Intel",
|
|
3875
5535
|
description: withReportNote("Harvest ads from a Facebook advertiser when the user wants current ad copy, creative angles, CTAs, video URLs, or competitive ad intelligence for one brand/page. Returns ad copy, headlines, CTAs, creative type, status, landing URLs, and direct ad video URLs ready for facebook_ad_transcribe. Accepts pageId, libraryId, or a brand/advertiser name as query. Use facebook_ad_search first when the advertiser handle is unknown. For normal public Facebook reels/posts/watch/share URLs, use facebook_video_transcribe instead."),
|
|
3876
5536
|
inputSchema: FacebookPageIntelInputSchema,
|
|
3877
5537
|
outputSchema: FacebookPageIntelOutputSchema,
|
|
3878
5538
|
annotations: liveWebToolAnnotations("Facebook Advertiser Ad Intel")
|
|
3879
|
-
}, async (input) => formatFacebookPageIntel(await
|
|
5539
|
+
}, async (input) => formatFacebookPageIntel(await executor.facebookPageIntel(input), input));
|
|
3880
5540
|
server2.registerTool("facebook_ad_search", {
|
|
3881
5541
|
title: "Facebook Ad Library Search",
|
|
3882
5542
|
description: withReportNote("Search Facebook Ad Library when the user wants to find advertisers by brand, competitor, niche, or keyword. Returns advertisers with ad counts and library IDs. Use this to discover competitor/page handles, then pass libraryId or pageId to facebook_page_intel for ad details."),
|
|
3883
5543
|
inputSchema: FacebookAdSearchInputSchema,
|
|
3884
5544
|
outputSchema: FacebookAdSearchOutputSchema,
|
|
3885
5545
|
annotations: liveWebToolAnnotations("Facebook Ad Library Search")
|
|
3886
|
-
}, async (input) => formatFacebookAdSearch(await
|
|
5546
|
+
}, async (input) => formatFacebookAdSearch(await executor.facebookAdSearch(input), input));
|
|
3887
5547
|
server2.registerTool("facebook_ad_transcribe", {
|
|
3888
5548
|
title: "Facebook Ad Transcription",
|
|
3889
5549
|
description: "Transcribe audio from a Facebook ad video CDN URL. Use this when facebook_page_intel returned a direct videoUrl and the user asks what the ad says, what claims it makes, or wants ad-message extraction. Returns full transcript, timestamped chunks, word count, and resolvedInputs. Use only with the direct videoUrl value from facebook_page_intel results; do not pass public Facebook post/reel/share URLs. Use facebook_video_transcribe for organic Facebook URLs, and facebook_page_intel first when you only have a brand/page/ad library handle.",
|
|
3890
5550
|
inputSchema: FacebookAdTranscribeInputSchema,
|
|
3891
5551
|
outputSchema: FacebookAdTranscribeOutputSchema,
|
|
3892
5552
|
annotations: liveWebToolAnnotations("Facebook Ad Transcription")
|
|
3893
|
-
}, async (input) => formatFacebookAdTranscribe(await
|
|
5553
|
+
}, async (input) => formatFacebookAdTranscribe(await executor.facebookAdTranscribe(input), input));
|
|
3894
5554
|
server2.registerTool("facebook_video_transcribe", {
|
|
3895
5555
|
title: "Facebook Organic Video Transcription",
|
|
3896
5556
|
description: withReportNote("Transcribe audio from an organic Facebook reel, video, watch, post, or share URL, including fb.watch links. Use this when the user pastes a normal Facebook video page URL and wants the transcript or downloadable MP4. Renders the Facebook page in a browser, selects the best matching public Facebook CDN MP4 URL from page state, then returns sourceUrl, resolved pageUrl, videoId, ownerName, selectedQuality, bitrate, videoDurationSec, extracted MP4 URL, full transcript, and timestamped chunks."),
|
|
3897
5557
|
inputSchema: FacebookVideoTranscribeInputSchema,
|
|
3898
5558
|
outputSchema: FacebookVideoTranscribeOutputSchema,
|
|
3899
5559
|
annotations: liveWebToolAnnotations("Facebook Organic Video Transcription")
|
|
3900
|
-
}, async (input) => formatFacebookVideoTranscribe(await
|
|
5560
|
+
}, async (input) => formatFacebookVideoTranscribe(await executor.facebookVideoTranscribe(input), input));
|
|
3901
5561
|
server2.registerTool("instagram_profile_content", {
|
|
3902
5562
|
title: "Instagram Profile Content Discovery",
|
|
3903
5563
|
description: withReportNote("Discover Instagram profile grid content links for a handle or profile URL. Use this when the user wants a person or brand account content inventory before selecting posts/reels to download. Returns profile stats, collected post/reel/tv URLs, shortcodes, type counts, browser details, pagination attempts, stop reason, and limitations."),
|
|
3904
5564
|
inputSchema: InstagramProfileContentInputSchema,
|
|
3905
5565
|
outputSchema: InstagramProfileContentOutputSchema,
|
|
3906
5566
|
annotations: liveWebToolAnnotations("Instagram Profile Content Discovery")
|
|
3907
|
-
}, async (input) => formatInstagramProfileContent(await
|
|
5567
|
+
}, async (input) => formatInstagramProfileContent(await executor.instagramProfileContent(input), input));
|
|
3908
5568
|
server2.registerTool("instagram_media_download", {
|
|
3909
5569
|
title: "Instagram Post/Reel Media Download",
|
|
3910
5570
|
description: withReportNote("Extract and download media from one Instagram post, reel, or tv URL. Use after instagram_profile_content or when the user gives a specific Instagram URL and wants the image, caption/text, reel audio/video tracks, optional muxed MP4, or optional transcript. Reels commonly expose separate video-only and audio-only MP4 tracks; this tool selects the best video and audio tracks and attempts muxing when ffmpeg is available."),
|
|
3911
5571
|
inputSchema: InstagramMediaDownloadInputSchema,
|
|
3912
5572
|
outputSchema: InstagramMediaDownloadOutputSchema,
|
|
3913
5573
|
annotations: liveWebToolAnnotations("Instagram Post/Reel Media Download")
|
|
3914
|
-
}, async (input) => formatInstagramMediaDownload(await
|
|
5574
|
+
}, async (input) => formatInstagramMediaDownload(await executor.instagramMediaDownload(input), input));
|
|
3915
5575
|
server2.registerTool("maps_place_intel", {
|
|
3916
5576
|
title: "Google Maps Business Profile Details",
|
|
3917
5577
|
description: withReportNote('Extract Google Maps business intelligence for one known/named business: rating, review count, category, address, phone, website, hours, booking URL, review histogram, review topics, about attributes, entity IDs, and optional review cards. Do not use this for category searches, local market prospect lists, or requests for multiple GMB/GBP profiles; use maps_search first for those. Split business name from location (e.g. "Elite Roofing Denver CO" => businessName "Elite Roofing", location "Denver, CO"). Pass includeReviews true when the user asks for reviews/customer pain.'),
|
|
3918
5578
|
inputSchema: MapsPlaceIntelInputSchema,
|
|
3919
5579
|
outputSchema: MapsPlaceIntelOutputSchema,
|
|
3920
5580
|
annotations: liveWebToolAnnotations("Google Maps Business Profile Details")
|
|
3921
|
-
}, async (input) => formatMapsPlaceIntel(await
|
|
5581
|
+
}, async (input) => formatMapsPlaceIntel(await executor.mapsPlaceIntel(input), input));
|
|
3922
5582
|
server2.registerTool("maps_search", {
|
|
3923
5583
|
title: "Google Maps Business Search",
|
|
3924
5584
|
description: withReportNote('Search Google Maps for multiple businesses/profiles by category, niche, keyword, or local market. Use this when the user asks for several Google Business Profiles, GMBs, GBPs, leads, prospects, competitors, or "more than the 3-pack." Maps is the default location-targeted surface: omit proxyMode for normal Maps use so the service creates residential proxy evidence for the requested market and rotates on retryable failures. Pass proxyZip only when a specific ZIP or city-center ZIP is known. Use proxyMode configured only when you explicitly do not want city/ZIP proxy targeting. Returns up to 50 candidates with names, place URLs, CIDs when available, ratings, review counts, profile metadata, and sanitized attempt telemetry. Default maxResults is 10; maximum is 50. Use maps_place_intel afterward only when a selected business needs full details and reviews.'),
|
|
3925
5585
|
inputSchema: MapsSearchInputSchema,
|
|
3926
5586
|
outputSchema: MapsSearchOutputSchema,
|
|
3927
5587
|
annotations: liveWebToolAnnotations("Google Maps Business Search")
|
|
3928
|
-
}, async (input) => formatMapsSearch(await
|
|
5588
|
+
}, async (input) => formatMapsSearch(await executor.mapsSearch(input), input));
|
|
3929
5589
|
server2.registerTool("directory_workflow", {
|
|
3930
5590
|
title: "Directory Workflow: Markets + Maps",
|
|
3931
5591
|
description: withReportNote('Build directory/prospecting datasets by selecting US city markets from the free Census Population Estimates city/place dataset, optionally joining configured US ZIPS/Lead Magician ZIP groups, then running Google Maps business searches for each city in parallel. Use this when the user wants "all cities over 100k population in a state", "build a directory CSV", "find markets then get Maps data", or similar location-database + Maps workflows. Set minPopulation, state, query, maxResultsPerCity, and concurrency. Maps workflows default to location-targeted proxying so each city can use city/state or ZIP-group residential proxy evidence; use proxyMode configured only when you explicitly do not want city/ZIP proxy targeting. Saved CSV rows include source_location, result_position, business_name, review_stars, review_count, category, address, phone, hours_status, website_url, directions_url, place_url, cid, cid_decimal, city population, and ZIP groups. Structured city results include sanitized attempt telemetry. Use maps_place_intel only when a selected profile needs deeper review topics, profile review count confirmation, or review cards. For local Lead Magician ZIP enrichment, set MCP_SCRAPER_USZIPS_CSV_PATH on the API server or pass usZipsCsvPath only in local/test mode.'),
|
|
3932
5592
|
inputSchema: DirectoryWorkflowInputSchema,
|
|
3933
5593
|
outputSchema: DirectoryWorkflowOutputSchema,
|
|
3934
5594
|
annotations: liveWebToolAnnotations("Directory Workflow: Markets + Maps")
|
|
3935
|
-
}, async (input) => formatDirectoryWorkflow(await
|
|
5595
|
+
}, async (input) => formatDirectoryWorkflow(await executor.directoryWorkflow(input), input));
|
|
3936
5596
|
server2.registerTool("workflow_list", {
|
|
3937
5597
|
title: "Workflow Catalog",
|
|
3938
5598
|
description: "List MCP Scraper higher-level workflows and AI-facing recipes. Use this when the user asks what MCP Scraper can do beyond single tools, or asks for market analysis, ICP research, forum/review acquisition, full brand design briefings, CRO audits, competitive positioning, content gap briefs, or AI search visibility audits. Returns runnable workflow ids plus recipe guidance for the best tool chain.",
|
|
3939
5599
|
inputSchema: WorkflowListInputSchema,
|
|
3940
5600
|
outputSchema: WorkflowListOutputSchema,
|
|
3941
5601
|
annotations: localPlanningToolAnnotations("Workflow Catalog")
|
|
3942
|
-
}, async (input) => formatWorkflowList(await
|
|
5602
|
+
}, async (input) => formatWorkflowList(await executor.workflowList(input), input));
|
|
3943
5603
|
server2.registerTool("workflow_suggest", {
|
|
3944
5604
|
title: "Workflow Intent Router",
|
|
3945
5605
|
description: 'Route a high-level business or research goal to the right MCP Scraper workflow/tool chain. Use before running work when the user says things like "do market analysis", "research my ICP", "acquire forum and review language", "build a brand design brief", "run a CRO audit", "compare competitors", "make a content gap brief", or "audit AI search visibility". This tool does not spend credits; it tells the AI exactly which workflow/tool chain to run next.',
|
|
@@ -3953,28 +5613,28 @@ function registerPaaExtractorMcpTools(server2, executor2, options = {}) {
|
|
|
3953
5613
|
inputSchema: WorkflowRunInputSchema,
|
|
3954
5614
|
outputSchema: WorkflowRunOutputSchema,
|
|
3955
5615
|
annotations: liveWebToolAnnotations("Run Workflow")
|
|
3956
|
-
}, async (input) => formatWorkflowRun(await
|
|
5616
|
+
}, async (input) => formatWorkflowRun(await executor.workflowRun(input), input));
|
|
3957
5617
|
server2.registerTool("workflow_step", {
|
|
3958
5618
|
title: "Advance Workflow Step",
|
|
3959
5619
|
description: withReportNote("Run the next leg of a stepwise MCP Scraper workflow started with workflow_run. Pass the runId. Each call executes exactly one logical step (typically one live harvest), persists that step's artifacts, and returns the step output plus nextStep. Keep calling workflow_step with the same runId until done is true, reporting each step result to the user as it lands. Use this instead of waiting on one long workflow call \u2014 it avoids client timeouts on long multi-step jobs."),
|
|
3960
5620
|
inputSchema: WorkflowStepInputSchema,
|
|
3961
5621
|
outputSchema: WorkflowStepOutputSchema,
|
|
3962
5622
|
annotations: liveWebToolAnnotations("Advance Workflow Step")
|
|
3963
|
-
}, async (input) => formatWorkflowStep(await
|
|
5623
|
+
}, async (input) => formatWorkflowStep(await executor.workflowStep(input), input));
|
|
3964
5624
|
server2.registerTool("workflow_status", {
|
|
3965
5625
|
title: "Workflow Status",
|
|
3966
5626
|
description: "Fetch a hosted workflow run by id and list its current status and artifacts. Use when a workflow may still be running, when the model needs to re-open a run, inspect artifact ids, recover from a long workflow, or decide whether to call workflow_step or workflow_artifact_read next. Use only a runId returned by workflow_run/workflow_step/workflow_status; do not construct one yourself.",
|
|
3967
5627
|
inputSchema: WorkflowStatusInputSchema,
|
|
3968
5628
|
outputSchema: WorkflowStatusOutputSchema,
|
|
3969
5629
|
annotations: liveWebToolAnnotations("Workflow Status")
|
|
3970
|
-
}, async (input) => formatWorkflowStatus(await
|
|
5630
|
+
}, async (input) => formatWorkflowStatus(await executor.workflowStatus(input), input));
|
|
3971
5631
|
server2.registerTool("workflow_artifact_read", {
|
|
3972
5632
|
title: "Read Workflow Artifact",
|
|
3973
5633
|
description: "Read a workflow artifact back into MCP context by run id and artifact id. Use this before writing final deliverables so the answer is grounded in generated evidence.json, CSVs, Markdown briefs, reports, and task files instead of memory. Use workflow_status first when artifact ids are unknown. Use only artifactId values returned by workflow_run/workflow_step/workflow_status; do not construct one yourself. Use maxBytes to limit large CSV/JSON artifacts.",
|
|
3974
5634
|
inputSchema: WorkflowArtifactReadInputSchema,
|
|
3975
5635
|
outputSchema: WorkflowArtifactReadOutputSchema,
|
|
3976
5636
|
annotations: liveWebToolAnnotations("Read Workflow Artifact")
|
|
3977
|
-
}, async (input) => formatWorkflowArtifactRead(await
|
|
5637
|
+
}, async (input) => formatWorkflowArtifactRead(await executor.workflowArtifactRead(input), input));
|
|
3978
5638
|
server2.registerTool("rank_tracker_workflow", {
|
|
3979
5639
|
title: "Rank Tracker Blueprint Builder",
|
|
3980
5640
|
description: "Generate a build-ready database, cron/heartbeat, ingestion, metrics, and AI implementation prompt for a rank tracker powered by MCP Scraper. Supports Maps rankings through directory_workflow/maps_search, organic rankings through search_serp, AI Overview citation tracking, and People Also Ask source presence tracking. This tool is local planning only; it does not call the web or spend credits.",
|
|
@@ -3994,16 +5654,127 @@ function registerPaaExtractorMcpTools(server2, executor2, options = {}) {
|
|
|
3994
5654
|
idempotentHint: true,
|
|
3995
5655
|
openWorldHint: false
|
|
3996
5656
|
}
|
|
3997
|
-
}, async (input) => formatCreditsInfo(await
|
|
5657
|
+
}, async (input) => formatCreditsInfo(await executor.creditsInfo(input), input));
|
|
5658
|
+
}
|
|
5659
|
+
|
|
5660
|
+
// src/install-terminal.ts
|
|
5661
|
+
var CODES = {
|
|
5662
|
+
reset: "\x1B[0m",
|
|
5663
|
+
cyan: "\x1B[36m",
|
|
5664
|
+
lime: "\x1B[32m",
|
|
5665
|
+
amber: "\x1B[33m",
|
|
5666
|
+
red: "\x1B[31m",
|
|
5667
|
+
muted: "\x1B[90m",
|
|
5668
|
+
bold: "\x1B[1m"
|
|
5669
|
+
};
|
|
5670
|
+
function colorize(value, tone, enabled) {
|
|
5671
|
+
if (!enabled) return value;
|
|
5672
|
+
return `${CODES[tone]}${value}${CODES.reset}`;
|
|
5673
|
+
}
|
|
5674
|
+
function toolRow(label, tools, enabled) {
|
|
5675
|
+
const padded = label.padEnd(9, " ");
|
|
5676
|
+
return ` ${colorize(padded, "muted", enabled)} ${tools.join(colorize(" . ", "muted", enabled))}`;
|
|
5677
|
+
}
|
|
5678
|
+
function renderInstallTerminal(options) {
|
|
5679
|
+
const color = options.color ?? true;
|
|
5680
|
+
const apiKeyValue = options.apiKeyConfigured ? "$MCP_SCRAPER_API_KEY" : "sk_live_your_key";
|
|
5681
|
+
const ascii = String.raw`
|
|
5682
|
+
__ __ ____ ____
|
|
5683
|
+
| \/ |/ ___| _ \
|
|
5684
|
+
| |\/| | | | |_) |
|
|
5685
|
+
| | | | |___| __/
|
|
5686
|
+
|_| |_|\____|_|
|
|
5687
|
+
|
|
5688
|
+
____ ____ ____ _ ____ _____ ____
|
|
5689
|
+
/ ___| / ___| _ \ / \ | _ \| ____| _ \
|
|
5690
|
+
\___ \| | | |_) | / _ \ | |_) | _| | |_) |
|
|
5691
|
+
___) | |___| _ < / ___ \| __/| |___| _ <
|
|
5692
|
+
|____/ \____|_| \_\/_/ \_\_| |_____|_| \_\
|
|
5693
|
+
`;
|
|
5694
|
+
const claudeCommand = [
|
|
5695
|
+
`MCP_SCRAPER_API_KEY=${apiKeyValue} npx -y -p mcp-scraper@latest \\`,
|
|
5696
|
+
" mcp-scraper-cli agent install claude --apply"
|
|
5697
|
+
].join("\n");
|
|
5698
|
+
const codexConfig = [
|
|
5699
|
+
"[mcp_servers.mcp-scraper]",
|
|
5700
|
+
'command = "npx"',
|
|
5701
|
+
'args = ["-y", "-p", "mcp-scraper@latest", "mcp-scraper"]',
|
|
5702
|
+
`env = { MCP_SCRAPER_API_KEY = "${apiKeyValue}" }`
|
|
5703
|
+
].join("\n");
|
|
5704
|
+
return [
|
|
5705
|
+
colorize(`mcp-scraper v${options.version}`, "bold", color),
|
|
5706
|
+
colorize("> mcp-scraper-install", "muted", color),
|
|
5707
|
+
colorize(ascii, "amber", color),
|
|
5708
|
+
`${colorize("MCP Scraper Agent", "cyan", color)} . v${options.version} . mcpscraper.dev`,
|
|
5709
|
+
"1/1 install surfaces ready",
|
|
5710
|
+
colorize("Newest: saved browser profiles that hold MANY site logins. Share the watch_url, user signs in, then verify with browser_profile_list. Browser sessions are direct/no-proxy by default.", "lime", color),
|
|
5711
|
+
"",
|
|
5712
|
+
`${colorize("Tools", "cyan", color)} ${colorize("(45 MCP tools)", "muted", color)}`,
|
|
5713
|
+
toolRow("search", ["harvest_paa", "search_serp", "maps_search", "maps_place_intel"], color),
|
|
5714
|
+
toolRow("extract", ["extract_url", "map_site_urls", "extract_site", "audit_site", "directory_workflow"], color),
|
|
5715
|
+
toolRow("build", ["rank_tracker_workflow", "cron plan", "database prompt"], color),
|
|
5716
|
+
toolRow("media", ["youtube_harvest", "youtube_transcribe", "facebook_ad_search", "facebook_page_intel", "facebook_ad_transcribe", "facebook_video_transcribe", "instagram_profile_content", "instagram_media_download"], color),
|
|
5717
|
+
toolRow("browser", ["browser_open", "browser_profile_connect", "browser_profile_list", "browser_close", "browser_screenshot", "browser_read", "browser_locate", "browser_replay_mark", "browser_replay_annotate"], color),
|
|
5718
|
+
toolRow("account", ["credits_info", "reports", "MCP resources"], color),
|
|
5719
|
+
`${colorize("Workflows", "cyan", color)} ${colorize("(MCP + CLI + API)", "muted", color)}`,
|
|
5720
|
+
toolRow("route", ["workflow_list", "workflow_suggest", "workflow_run", "workflow_step", "workflow_status", "workflow_artifact_read"], color),
|
|
5721
|
+
toolRow("seo", ["directory", "agent-packet", "competitive audit", "map/serp comparison", "PAA/AIO briefs", "scheduled runs"], color),
|
|
5722
|
+
"",
|
|
5723
|
+
colorize("Usage tips:", "amber", color),
|
|
5724
|
+
"Run mcp-scraper-install for this visible card. Run mcp-scraper-cli for setup utilities and subcommands.",
|
|
5725
|
+
"Run mcp-scraper in a human terminal to print this card; MCP clients get the same command as a silent stdio server.",
|
|
5726
|
+
"Explicit card command: npx -y -p mcp-scraper@latest mcp-scraper-install",
|
|
5727
|
+
"Customer auth setup: run browser_profile_connect, send the watch_url, let the user sign in, then call browser_profile_list until AUTHENTICATED.",
|
|
5728
|
+
"Stack logins / reconnect: run browser_profile_connect again with the same profile name and another domain to add accounts or refresh a login.",
|
|
5729
|
+
"Start with workflow_suggest for broad jobs like market analysis, ICP research, CRO audits, brand briefs, content gaps, and AI visibility.",
|
|
5730
|
+
"For MCP clients, use mcp-scraper so one install can mix SERP, Maps, browser, reports, and saved MCP resources.",
|
|
5731
|
+
"If you hit the concurrency limit, add an extra slot for $5/month with mcp-scraper-cli billing concurrency checkout.",
|
|
5732
|
+
"",
|
|
5733
|
+
`${colorize("Ready.", "lime", color)} Install the combined MCP server with one command:`,
|
|
5734
|
+
"",
|
|
5735
|
+
colorize("Setup doctor", "amber", color),
|
|
5736
|
+
"npx -y -p mcp-scraper@latest mcp-scraper-cli doctor",
|
|
5737
|
+
"",
|
|
5738
|
+
colorize("Hosted profile setup", "amber", color),
|
|
5739
|
+
'In your MCP client, call browser_profile_connect with email="seo@example.com" and domain="chatgpt.com".',
|
|
5740
|
+
"Give the returned watch_url to the user. After they sign in, call browser_profile_list, then browser_open with the returned profile. Add more logins by calling browser_profile_connect again with the same profile and a new domain.",
|
|
5741
|
+
"",
|
|
5742
|
+
colorize("Claude Code one-command setup", "amber", color),
|
|
5743
|
+
claudeCommand,
|
|
5744
|
+
"Then fully exit Claude Code and open a new Claude terminal. Check with: claude mcp list",
|
|
5745
|
+
"",
|
|
5746
|
+
colorize("Codex config", "amber", color),
|
|
5747
|
+
codexConfig,
|
|
5748
|
+
"",
|
|
5749
|
+
colorize("Claude Desktop Extension", "amber", color),
|
|
5750
|
+
"Download: https://mcpscraper.dev/downloads/mcp-scraper.mcpb",
|
|
5751
|
+
"",
|
|
5752
|
+
colorize("Safety note:", "muted", color),
|
|
5753
|
+
"mcp-scraper prints this card only when stdin/stdout are an interactive TTY. In MCP clients it writes only JSON-RPC to stdout.",
|
|
5754
|
+
"Use --stdio or MCP_SCRAPER_FORCE_STDIO=1 to force server mode from a terminal.",
|
|
5755
|
+
""
|
|
5756
|
+
].join("\n");
|
|
3998
5757
|
}
|
|
3999
5758
|
|
|
4000
5759
|
// bin/mcp-stdio-server.ts
|
|
5760
|
+
var forceStdio = process.argv.includes("--stdio") || process.env.MCP_SCRAPER_FORCE_STDIO === "1";
|
|
5761
|
+
var interactiveTerminal = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
5762
|
+
var wantsHelp = process.argv.includes("--help") || process.argv.includes("-h");
|
|
5763
|
+
if (!forceStdio && (interactiveTerminal || wantsHelp)) {
|
|
5764
|
+
const noColor = process.argv.includes("--no-color") || process.env.NO_COLOR !== void 0 || process.env.FORCE_COLOR === "0" || !process.stdout.isTTY;
|
|
5765
|
+
process.stdout.write(renderInstallTerminal({
|
|
5766
|
+
version: PACKAGE_VERSION,
|
|
5767
|
+
color: !noColor,
|
|
5768
|
+
apiKeyConfigured: Boolean(process.env.MCP_SCRAPER_API_KEY?.trim())
|
|
5769
|
+
}));
|
|
5770
|
+
process.exit(0);
|
|
5771
|
+
}
|
|
4001
5772
|
function readApiKeyFile() {
|
|
4002
5773
|
const explicitPath = process.env.MCP_SCRAPER_KEY_PATH?.trim();
|
|
4003
|
-
const paths = [explicitPath, (0,
|
|
5774
|
+
const paths = [explicitPath, (0, import_node_path6.join)((0, import_node_os5.homedir)(), ".mcp-scraper-key")].filter(Boolean);
|
|
4004
5775
|
for (const path of paths) {
|
|
4005
5776
|
try {
|
|
4006
|
-
const value = (0,
|
|
5777
|
+
const value = (0, import_node_fs5.readFileSync)(path, "utf8").trim();
|
|
4007
5778
|
if (value) return value;
|
|
4008
5779
|
} catch {
|
|
4009
5780
|
}
|
|
@@ -4016,8 +5787,10 @@ if (!apiKey) {
|
|
|
4016
5787
|
process.exit(1);
|
|
4017
5788
|
}
|
|
4018
5789
|
var baseUrl = process.env.MCP_SCRAPER_BASE_URL?.trim() || process.env.MCP_BASE_URL?.trim() || "https://mcpscraper.dev";
|
|
4019
|
-
var
|
|
4020
|
-
var server =
|
|
5790
|
+
var consoleBaseUrl = process.env.BROWSER_AGENT_CONSOLE_URL?.trim() || baseUrl;
|
|
5791
|
+
var server = new import_mcp3.McpServer({ name: "mcp-scraper", version: PACKAGE_VERSION }, { instructions: SERVER_INSTRUCTIONS });
|
|
5792
|
+
registerPaaExtractorMcpTools(server, new HttpMcpToolExecutor(baseUrl, apiKey));
|
|
5793
|
+
registerBrowserAgentMcpTools(server, { baseUrl, apiKey, consoleBaseUrl });
|
|
4021
5794
|
var transport = new import_stdio.StdioServerTransport();
|
|
4022
5795
|
async function main() {
|
|
4023
5796
|
await server.connect(transport);
|