mcp-scraper 0.3.10 → 0.3.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/dist/bin/api-server.cjs +115 -40
- package/dist/bin/api-server.cjs.map +1 -1
- package/dist/bin/api-server.js +2 -2
- package/dist/bin/browser-agent-stdio-server.cjs +206 -31
- package/dist/bin/browser-agent-stdio-server.cjs.map +1 -1
- package/dist/bin/browser-agent-stdio-server.js +3 -2
- package/dist/bin/browser-agent-stdio-server.js.map +1 -1
- package/dist/bin/mcp-scraper-cli.cjs +1 -1
- package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
- package/dist/bin/mcp-scraper-cli.js +1 -1
- package/dist/bin/mcp-scraper-combined-stdio-server.cjs +262 -60
- package/dist/bin/mcp-scraper-combined-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-scraper-combined-stdio-server.js +4 -3
- package/dist/bin/mcp-scraper-combined-stdio-server.js.map +1 -1
- package/dist/bin/mcp-scraper-install.cjs +1 -1
- package/dist/bin/mcp-scraper-install.cjs.map +1 -1
- package/dist/bin/mcp-scraper-install.js +1 -1
- package/dist/bin/mcp-stdio-server.cjs +38 -11
- package/dist/bin/mcp-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-stdio-server.js +2 -2
- package/dist/bin/paa-harvest.cjs +21 -1
- package/dist/bin/paa-harvest.cjs.map +1 -1
- package/dist/bin/paa-harvest.js +1 -1
- package/dist/chunk-3YGKXXUG.js +133 -0
- package/dist/chunk-3YGKXXUG.js.map +1 -0
- package/dist/{chunk-UWSG3C5J.js → chunk-4OPKIDON.js} +22 -2
- package/dist/chunk-4OPKIDON.js.map +1 -0
- package/dist/{chunk-CED7X4WB.js → chunk-7R7VBQRV.js} +39 -12
- package/dist/chunk-7R7VBQRV.js.map +1 -0
- package/dist/{chunk-QPFF3V2R.js → chunk-BW3DGFNQ.js} +36 -9
- package/dist/chunk-BW3DGFNQ.js.map +1 -0
- package/dist/chunk-FB5ZJGFN.js +7 -0
- package/dist/chunk-FB5ZJGFN.js.map +1 -0
- package/dist/index.cjs +21 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/dist/{server-QTV2EUKA.js → server-CUPDJLWM.js} +44 -142
- package/dist/server-CUPDJLWM.js.map +1 -0
- package/dist/{worker-56IXWOQU.js → worker-FG7ZWEGA.js} +2 -2
- package/docs/mcp-tool-craft-lint.generated.md +1 -1
- package/docs/mcp-tool-manifest.generated.json +3 -3
- package/package.json +1 -1
- package/dist/chunk-CED7X4WB.js.map +0 -1
- package/dist/chunk-NEGW2ZEJ.js +0 -7
- package/dist/chunk-NEGW2ZEJ.js.map +0 -1
- package/dist/chunk-QPFF3V2R.js.map +0 -1
- package/dist/chunk-UWSG3C5J.js.map +0 -1
- package/dist/server-QTV2EUKA.js.map +0 -1
- /package/dist/{worker-56IXWOQU.js.map → worker-FG7ZWEGA.js.map} +0 -0
package/dist/bin/api-server.js
CHANGED
|
@@ -17,8 +17,8 @@ loadDotEnv();
|
|
|
17
17
|
async function main() {
|
|
18
18
|
const [{ serve }, { app }, { startWorker }, { migrate }] = await Promise.all([
|
|
19
19
|
import("@hono/node-server"),
|
|
20
|
-
import("../server-
|
|
21
|
-
import("../worker-
|
|
20
|
+
import("../server-CUPDJLWM.js"),
|
|
21
|
+
import("../worker-FG7ZWEGA.js"),
|
|
22
22
|
import("../db-BE4JVB3V.js")
|
|
23
23
|
]);
|
|
24
24
|
const PORT = parseInt(process.env.PORT ?? "3001");
|
|
@@ -1,17 +1,39 @@
|
|
|
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/browser-agent-stdio-server.ts
|
|
5
|
-
var
|
|
6
|
-
var
|
|
7
|
-
var
|
|
27
|
+
var import_node_fs3 = require("fs");
|
|
28
|
+
var import_node_os4 = require("os");
|
|
29
|
+
var import_node_path4 = require("path");
|
|
8
30
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
9
31
|
|
|
10
32
|
// src/mcp/browser-agent-mcp-server.ts
|
|
11
33
|
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
12
|
-
var
|
|
13
|
-
var
|
|
14
|
-
var
|
|
34
|
+
var import_node_fs2 = require("fs");
|
|
35
|
+
var import_node_os3 = require("os");
|
|
36
|
+
var import_node_path3 = require("path");
|
|
15
37
|
|
|
16
38
|
// src/lib/browser-service-env.ts
|
|
17
39
|
function browserServiceProfileName() {
|
|
@@ -26,8 +48,137 @@ function browserServiceProfileSaveChanges() {
|
|
|
26
48
|
return void 0;
|
|
27
49
|
}
|
|
28
50
|
|
|
51
|
+
// src/services/fanout/export.ts
|
|
52
|
+
var import_node_fs = require("fs");
|
|
53
|
+
var import_node_os = require("os");
|
|
54
|
+
var import_node_path = require("path");
|
|
55
|
+
var import_papaparse = __toESM(require("papaparse"), 1);
|
|
56
|
+
function outputBaseDir() {
|
|
57
|
+
return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path.join)((0, import_node_os.homedir)(), "Downloads", "mcp-scraper");
|
|
58
|
+
}
|
|
59
|
+
function safe(value) {
|
|
60
|
+
return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "capture";
|
|
61
|
+
}
|
|
62
|
+
function writeTable(path, rows, delimiter) {
|
|
63
|
+
(0, import_node_fs.writeFileSync)(path, import_papaparse.default.unparse(rows.length ? rows : [{}], { delimiter }));
|
|
64
|
+
}
|
|
65
|
+
function exportFanout(enriched) {
|
|
66
|
+
const stamp = safe(enriched.capturedAt.replace(/[:.]/g, "-"));
|
|
67
|
+
const outputDir = outputBaseDir();
|
|
68
|
+
const relativeDir = (0, import_node_path.join)("fanout", `${stamp}-${safe(enriched.platform)}`);
|
|
69
|
+
const dir = (0, import_node_path.join)(outputDir, relativeDir);
|
|
70
|
+
(0, import_node_fs.mkdirSync)(dir, { recursive: true });
|
|
71
|
+
const queryRows = enriched.queries.map((q, i) => ({ index: i + 1, query: q }));
|
|
72
|
+
const citationRows = enriched.aggregates.citationOrder.map((c) => {
|
|
73
|
+
const src = enriched.citedUrls.find((s) => s.url === c.url);
|
|
74
|
+
return { rank: c.rank, domain: c.domain, url: c.url, timesCited: c.timesCited, siteType: src?.siteType ?? "", title: src?.title ?? "" };
|
|
75
|
+
});
|
|
76
|
+
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 ?? "" }));
|
|
77
|
+
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 ?? "" }));
|
|
78
|
+
const snippetRows = enriched.snippets.map((s) => ({ domain: s.domain, url: s.url, title: s.title, text: s.text }));
|
|
79
|
+
const domainRows = enriched.aggregates.topSites.map((d) => ({ domain: d.domain, count: d.count, cited: d.cited, timesCited: d.timesCited, siteType: d.siteType }));
|
|
80
|
+
const relativePaths = {
|
|
81
|
+
relativeTo: "MCP_SCRAPER_OUTPUT_DIR or ~/Downloads/mcp-scraper",
|
|
82
|
+
dir: relativeDir,
|
|
83
|
+
json: (0, import_node_path.join)(relativeDir, "fanout.json"),
|
|
84
|
+
queriesCsv: (0, import_node_path.join)(relativeDir, "queries.csv"),
|
|
85
|
+
queriesTsv: (0, import_node_path.join)(relativeDir, "queries.tsv"),
|
|
86
|
+
citationsCsv: (0, import_node_path.join)(relativeDir, "citations.csv"),
|
|
87
|
+
sourcesCsv: (0, import_node_path.join)(relativeDir, "sources.csv"),
|
|
88
|
+
browsedOnlyCsv: (0, import_node_path.join)(relativeDir, "browsed-only.csv"),
|
|
89
|
+
snippetsCsv: (0, import_node_path.join)(relativeDir, "snippets.csv"),
|
|
90
|
+
domainsCsv: (0, import_node_path.join)(relativeDir, "domains.csv"),
|
|
91
|
+
report: (0, import_node_path.join)(relativeDir, "report.html")
|
|
92
|
+
};
|
|
93
|
+
(0, import_node_fs.writeFileSync)((0, import_node_path.join)(outputDir, relativePaths.json), JSON.stringify(enriched, null, 2));
|
|
94
|
+
writeTable((0, import_node_path.join)(outputDir, relativePaths.queriesCsv), queryRows, ",");
|
|
95
|
+
writeTable((0, import_node_path.join)(outputDir, relativePaths.queriesTsv), queryRows, " ");
|
|
96
|
+
writeTable((0, import_node_path.join)(outputDir, relativePaths.citationsCsv), citationRows, ",");
|
|
97
|
+
writeTable((0, import_node_path.join)(outputDir, relativePaths.sourcesCsv), sourceRows, ",");
|
|
98
|
+
writeTable((0, import_node_path.join)(outputDir, relativePaths.browsedOnlyCsv), browsedOnlyRows, ",");
|
|
99
|
+
writeTable((0, import_node_path.join)(outputDir, relativePaths.snippetsCsv), snippetRows, ",");
|
|
100
|
+
writeTable((0, import_node_path.join)(outputDir, relativePaths.domainsCsv), domainRows, ",");
|
|
101
|
+
(0, import_node_fs.writeFileSync)((0, import_node_path.join)(outputDir, relativePaths.report), renderReportHtml(enriched));
|
|
102
|
+
return relativePaths;
|
|
103
|
+
}
|
|
104
|
+
function esc(s) {
|
|
105
|
+
return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
106
|
+
}
|
|
107
|
+
function renderReportHtml(enriched) {
|
|
108
|
+
const data = JSON.stringify(enriched).replace(/</g, "\\u003c");
|
|
109
|
+
return `<!doctype html>
|
|
110
|
+
<html lang="en">
|
|
111
|
+
<head>
|
|
112
|
+
<meta charset="utf-8" />
|
|
113
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
114
|
+
<title>AI Search Fan-Out \u2014 ${esc(enriched.platform)}</title>
|
|
115
|
+
<style>
|
|
116
|
+
:root{--bg:#0b0d12;--panel:#151922;--line:#252b38;--ink:#e8ecf3;--mut:#8b95a7;--acc:#5b9dff;--cite:#39d98a;--browse:#5a6677}
|
|
117
|
+
*{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}
|
|
118
|
+
.wrap{max-width:980px;margin:0 auto;padding:28px 20px 80px}
|
|
119
|
+
h1{font-size:20px;margin:0 0 2px}.sub{color:var(--mut);margin:0 0 20px;font-size:13px}
|
|
120
|
+
.stat{font-size:30px;font-weight:700}.stat small{font-size:13px;font-weight:400;color:var(--mut)}
|
|
121
|
+
.cards{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin:18px 0}
|
|
122
|
+
.card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:12px 14px}
|
|
123
|
+
.card b{font-size:22px;display:block}.card span{color:var(--mut);font-size:12px}
|
|
124
|
+
.bars{margin:8px 0 22px}.bar{display:flex;align-items:center;gap:8px;margin:4px 0}
|
|
125
|
+
.bar .lab{width:150px;color:var(--mut);font-size:12px;text-align:right}.bar .track{flex:1;background:#10141c;border-radius:5px;overflow:hidden}
|
|
126
|
+
.bar .fill{background:var(--acc);height:16px}.bar .n{width:34px;font-size:12px;color:var(--mut)}
|
|
127
|
+
.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}
|
|
128
|
+
.tab.on{background:var(--acc);border-color:var(--acc);color:#04101f;font-weight:600}
|
|
129
|
+
.chips{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:12px}
|
|
130
|
+
.chip{background:var(--panel);border:1px solid var(--line);color:var(--mut);padding:4px 10px;border-radius:20px;cursor:pointer;font-size:12px}
|
|
131
|
+
.chip.on{border-color:var(--acc);color:var(--ink)}.chip b{color:var(--ink)}
|
|
132
|
+
.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}
|
|
133
|
+
.row .idx{color:var(--mut);min-width:22px}.row .main{flex:1}
|
|
134
|
+
.tag{font-size:11px;padding:2px 7px;border-radius:6px;border:1px solid var(--line);color:var(--mut);margin-right:5px}
|
|
135
|
+
.dom{font-weight:600}.url{color:var(--mut);font-size:12px;word-break:break-all}
|
|
136
|
+
.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}
|
|
137
|
+
.copy{background:none;border:1px solid var(--line);color:var(--mut);border-radius:6px;cursor:pointer;font-size:11px;padding:2px 8px}
|
|
138
|
+
.hide{display:none}
|
|
139
|
+
</style>
|
|
140
|
+
</head>
|
|
141
|
+
<body><div class="wrap">
|
|
142
|
+
<h1>AI Search Fan-Out</h1>
|
|
143
|
+
<p class="sub" id="sub"></p>
|
|
144
|
+
<div class="stat" id="stat"></div>
|
|
145
|
+
<div class="cards" id="cards"></div>
|
|
146
|
+
<div id="charts"></div>
|
|
147
|
+
<div class="tabs"><button class="tab on" data-tab="q">Queries</button><button class="tab" data-tab="u">URLs</button></div>
|
|
148
|
+
<div class="chips" id="chips"></div>
|
|
149
|
+
<div id="list"></div>
|
|
150
|
+
</div>
|
|
151
|
+
<script>
|
|
152
|
+
const D=${data};
|
|
153
|
+
const el=(t,c,h)=>{const e=document.createElement(t);if(c)e.className=c;if(h!=null)e.innerHTML=h;return e};
|
|
154
|
+
document.getElementById('sub').textContent=D.platform+' \xB7 '+(D.meta.model||'model n/a')+' \xB7 '+D.meta.rounds+' search round(s) \xB7 prompt: '+(D.prompt||'');
|
|
155
|
+
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>';
|
|
156
|
+
const cards=[['Sub-queries',D.counts.subQueries],['Researched',D.counts.browsed],['Cited',D.counts.cited],['Browsed-only',D.counts.browsedOnly]];
|
|
157
|
+
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)});
|
|
158
|
+
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}
|
|
159
|
+
const charts=document.getElementById('charts');
|
|
160
|
+
charts.appendChild(barChart('URL Categories',D.aggregates.byCategory));
|
|
161
|
+
let tab='q',filter=null;
|
|
162
|
+
const chips=document.getElementById('chips'),list=document.getElementById('list');
|
|
163
|
+
function render(){
|
|
164
|
+
chips.innerHTML='';list.innerHTML='';
|
|
165
|
+
if(tab==='q'){
|
|
166
|
+
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)});
|
|
167
|
+
} else {
|
|
168
|
+
const counts={};D.browsedUrls.forEach(s=>{counts[s.siteType]=(counts[s.siteType]||0)+1});
|
|
169
|
+
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)});
|
|
170
|
+
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)});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function esc(s){return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')}
|
|
174
|
+
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()});
|
|
175
|
+
render();
|
|
176
|
+
</script>
|
|
177
|
+
</body></html>`;
|
|
178
|
+
}
|
|
179
|
+
|
|
29
180
|
// src/version.ts
|
|
30
|
-
var PACKAGE_VERSION = "0.3.
|
|
181
|
+
var PACKAGE_VERSION = "0.3.12";
|
|
31
182
|
|
|
32
183
|
// src/mcp/browser-agent-tool-schemas.ts
|
|
33
184
|
var import_zod = require("zod");
|
|
@@ -164,7 +315,7 @@ var BrowserCaptureFanoutInputSchema = {
|
|
|
164
315
|
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."),
|
|
165
316
|
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."),
|
|
166
317
|
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."),
|
|
167
|
-
export: import_zod.z.boolean().default(false).describe("When true, write JSON, CSV, TSV exports (queries, citations, sources, domains) and a self-contained HTML report to MCP_SCRAPER_OUTPUT_DIR/fanout
|
|
318
|
+
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.")
|
|
168
319
|
};
|
|
169
320
|
var FanoutSourceOutput = import_zod.z.object({
|
|
170
321
|
url: import_zod.z.string(),
|
|
@@ -206,15 +357,18 @@ var BrowserCaptureFanoutOutputSchema = {
|
|
|
206
357
|
}).describe("Objective aggregates: top sourced sites by frequency, citation order, and URL-category counts."),
|
|
207
358
|
first_party_domain: import_zod.z.string().nullable(),
|
|
208
359
|
exports: import_zod.z.object({
|
|
360
|
+
relativeTo: import_zod.z.string(),
|
|
209
361
|
dir: import_zod.z.string(),
|
|
210
362
|
json: import_zod.z.string(),
|
|
211
363
|
queriesCsv: import_zod.z.string(),
|
|
212
364
|
queriesTsv: import_zod.z.string(),
|
|
213
365
|
citationsCsv: import_zod.z.string(),
|
|
214
366
|
sourcesCsv: import_zod.z.string(),
|
|
367
|
+
browsedOnlyCsv: import_zod.z.string(),
|
|
368
|
+
snippetsCsv: import_zod.z.string(),
|
|
215
369
|
domainsCsv: import_zod.z.string(),
|
|
216
370
|
report: import_zod.z.string()
|
|
217
|
-
}).nullable().describe("
|
|
371
|
+
}).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."),
|
|
218
372
|
debug: import_zod.z.object({
|
|
219
373
|
interceptorReady: import_zod.z.boolean(),
|
|
220
374
|
rawBytes: import_zod.z.number().int().min(0),
|
|
@@ -366,8 +520,8 @@ var BrowserListSessionsOutputSchema = {
|
|
|
366
520
|
// src/mcp/replay-annotator.ts
|
|
367
521
|
var import_node_child_process = require("child_process");
|
|
368
522
|
var import_promises = require("fs/promises");
|
|
369
|
-
var
|
|
370
|
-
var
|
|
523
|
+
var import_node_os2 = require("os");
|
|
524
|
+
var import_node_path2 = require("path");
|
|
371
525
|
var import_node_util = require("util");
|
|
372
526
|
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
373
527
|
function finiteNumber(value) {
|
|
@@ -377,11 +531,11 @@ function clamp(value, min, max) {
|
|
|
377
531
|
return Math.min(max, Math.max(min, value));
|
|
378
532
|
}
|
|
379
533
|
function formatAssTime(seconds) {
|
|
380
|
-
const
|
|
381
|
-
const hours = Math.floor(
|
|
382
|
-
const minutes = Math.floor(
|
|
383
|
-
const wholeSeconds = Math.floor(
|
|
384
|
-
const centiseconds = Math.floor((
|
|
534
|
+
const safe2 = Math.max(0, seconds);
|
|
535
|
+
const hours = Math.floor(safe2 / 3600);
|
|
536
|
+
const minutes = Math.floor(safe2 % 3600 / 60);
|
|
537
|
+
const wholeSeconds = Math.floor(safe2 % 60);
|
|
538
|
+
const centiseconds = Math.floor((safe2 - Math.floor(safe2)) * 100);
|
|
385
539
|
return `${hours}:${String(minutes).padStart(2, "0")}:${String(wholeSeconds).padStart(2, "0")}.${String(centiseconds).padStart(2, "0")}`;
|
|
386
540
|
}
|
|
387
541
|
function cssHexToAssColor(color) {
|
|
@@ -590,8 +744,8 @@ function ffmpegFilterPath(path) {
|
|
|
590
744
|
async function annotateReplayVideo(inputFilePath, outputFilePath, options) {
|
|
591
745
|
if (!options.annotations.length) throw new Error("annotations must include at least one item");
|
|
592
746
|
const size = await videoSize(inputFilePath);
|
|
593
|
-
const tmp = await (0, import_promises.mkdtemp)((0,
|
|
594
|
-
const assPath = (0,
|
|
747
|
+
const tmp = await (0, import_promises.mkdtemp)((0, import_node_path2.join)((0, import_node_os2.tmpdir)(), "mcp-scraper-ass-"));
|
|
748
|
+
const assPath = (0, import_node_path2.join)(tmp, "annotations.ass");
|
|
595
749
|
try {
|
|
596
750
|
await (0, import_promises.writeFile)(assPath, buildAssSubtitle(options, size), "utf8");
|
|
597
751
|
await execFileAsync("ffmpeg", [
|
|
@@ -659,8 +813,8 @@ function actionResult(tool, sessionId, ok, data, nextRecommendedTool = "browser_
|
|
|
659
813
|
nextRecommendedTool
|
|
660
814
|
});
|
|
661
815
|
}
|
|
662
|
-
function
|
|
663
|
-
return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0,
|
|
816
|
+
function outputBaseDir2() {
|
|
817
|
+
return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path3.join)((0, import_node_os3.homedir)(), "Downloads", "mcp-scraper");
|
|
664
818
|
}
|
|
665
819
|
function safeFilePart(value) {
|
|
666
820
|
return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 120) || "replay";
|
|
@@ -669,7 +823,7 @@ function replayFilePath(sessionId, replayId, filename) {
|
|
|
669
823
|
const requested = filename?.trim();
|
|
670
824
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
671
825
|
const name = requested ? safeFilePart(requested).replace(/\.mp4$/i, "") : `${stamp}-${safeFilePart(sessionId)}-${safeFilePart(replayId)}`;
|
|
672
|
-
return (0,
|
|
826
|
+
return (0, import_node_path3.join)(outputBaseDir2(), "browser-replays", `${name}.mp4`);
|
|
673
827
|
}
|
|
674
828
|
function slugPart(value) {
|
|
675
829
|
const trimmed = value?.trim().toLowerCase();
|
|
@@ -682,6 +836,11 @@ function savedProfileNameFromEmail(email, domain) {
|
|
|
682
836
|
const domainPart = slugPart(domain);
|
|
683
837
|
return `${value}${domainPart ? `-${domainPart}` : ""}`.slice(0, 80) || null;
|
|
684
838
|
}
|
|
839
|
+
function isEnrichedFanout(value) {
|
|
840
|
+
if (!value || typeof value !== "object") return false;
|
|
841
|
+
const candidate = value;
|
|
842
|
+
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;
|
|
843
|
+
}
|
|
685
844
|
function annotatedReplayFilePath(sessionId, replayId, filename) {
|
|
686
845
|
const requested = filename?.trim();
|
|
687
846
|
if (requested) return replayFilePath(sessionId, replayId, requested);
|
|
@@ -744,8 +903,8 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
744
903
|
}
|
|
745
904
|
const bytes = Buffer.from(await res.arrayBuffer());
|
|
746
905
|
const filePath = replayFilePath(sessionId, replayId, filename);
|
|
747
|
-
(0,
|
|
748
|
-
(0,
|
|
906
|
+
(0, import_node_fs2.mkdirSync)((0, import_node_path3.join)(outputBaseDir2(), "browser-replays"), { recursive: true });
|
|
907
|
+
(0, import_node_fs2.writeFileSync)(filePath, bytes);
|
|
749
908
|
return {
|
|
750
909
|
ok: true,
|
|
751
910
|
data: {
|
|
@@ -1204,7 +1363,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
1204
1363
|
try {
|
|
1205
1364
|
const sourcePath = String(downloaded.data.file_path);
|
|
1206
1365
|
const outputPath = annotatedReplayFilePath(input.session_id, input.replay_id, input.filename);
|
|
1207
|
-
(0,
|
|
1366
|
+
(0, import_node_fs2.mkdirSync)((0, import_node_path3.join)(outputBaseDir2(), "browser-replays"), { recursive: true });
|
|
1208
1367
|
const result = await annotateReplayVideo(sourcePath, outputPath, {
|
|
1209
1368
|
annotations: input.annotations,
|
|
1210
1369
|
sourceWidth: input.source_width,
|
|
@@ -1277,13 +1436,13 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
1277
1436
|
"browser_capture_fanout",
|
|
1278
1437
|
{
|
|
1279
1438
|
title: "Capture AI Search Fan-Out",
|
|
1280
|
-
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. 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_onboard, give the user the watch_url, let them complete login, then call browser_profile_status 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.`,
|
|
1439
|
+
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_onboard, give the user the watch_url, let them complete login, then call browser_profile_status 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.`,
|
|
1281
1440
|
inputSchema: BrowserCaptureFanoutInputSchema,
|
|
1282
1441
|
outputSchema: BrowserCaptureFanoutOutputSchema,
|
|
1283
1442
|
annotations: annotations("Capture AI Search Fan-Out")
|
|
1284
1443
|
},
|
|
1285
1444
|
async (input) => {
|
|
1286
|
-
const emit = (result,
|
|
1445
|
+
const emit = (result, exports3) => structuredResult({
|
|
1287
1446
|
ok: true,
|
|
1288
1447
|
tool: "browser_capture_fanout",
|
|
1289
1448
|
session_id: input.session_id,
|
|
@@ -1299,18 +1458,34 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
1299
1458
|
counts: result.counts,
|
|
1300
1459
|
aggregates: result.aggregates,
|
|
1301
1460
|
first_party_domain: result.firstPartyDomain,
|
|
1302
|
-
exports:
|
|
1461
|
+
exports: exports3 ?? null,
|
|
1303
1462
|
...result.debug ? { debug: result.debug } : {}
|
|
1304
1463
|
});
|
|
1305
1464
|
const res = await req("POST", `/agent/sessions/${input.session_id}/capture-fanout`, {
|
|
1306
1465
|
prompt: input.prompt,
|
|
1307
1466
|
wait_ms: input.wait_ms,
|
|
1308
1467
|
first_party_domain: input.first_party_domain,
|
|
1309
|
-
|
|
1468
|
+
reset: input.reset,
|
|
1469
|
+
export: input.export
|
|
1310
1470
|
});
|
|
1311
1471
|
if (!res.ok) return errorResult("browser_capture_fanout", res.data, input.session_id);
|
|
1312
1472
|
const hosted = res.data?.result ?? res.data;
|
|
1313
|
-
|
|
1473
|
+
let exports2 = res.data?.exports ?? null;
|
|
1474
|
+
if (input.export && !exports2 && isEnrichedFanout(hosted)) {
|
|
1475
|
+
try {
|
|
1476
|
+
exports2 = exportFanout(hosted);
|
|
1477
|
+
} catch (err) {
|
|
1478
|
+
return errorResult(
|
|
1479
|
+
"browser_capture_fanout",
|
|
1480
|
+
{
|
|
1481
|
+
error: `Fan-out captured but local export failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1482
|
+
result: hosted
|
|
1483
|
+
},
|
|
1484
|
+
input.session_id
|
|
1485
|
+
);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
return emit(hosted, exports2);
|
|
1314
1489
|
}
|
|
1315
1490
|
);
|
|
1316
1491
|
}
|
|
@@ -1318,10 +1493,10 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
1318
1493
|
// bin/browser-agent-stdio-server.ts
|
|
1319
1494
|
function readApiKeyFile() {
|
|
1320
1495
|
const explicitPath = process.env.MCP_SCRAPER_KEY_PATH?.trim();
|
|
1321
|
-
const paths = [explicitPath, (0,
|
|
1496
|
+
const paths = [explicitPath, (0, import_node_path4.join)((0, import_node_os4.homedir)(), ".mcp-scraper-key")].filter(Boolean);
|
|
1322
1497
|
for (const path of paths) {
|
|
1323
1498
|
try {
|
|
1324
|
-
const value = (0,
|
|
1499
|
+
const value = (0, import_node_fs3.readFileSync)(path, "utf8").trim();
|
|
1325
1500
|
if (value) return value;
|
|
1326
1501
|
} catch {
|
|
1327
1502
|
}
|