mcp-scraper 0.2.6 → 0.2.8
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 +13 -6
- package/dist/bin/api-server.cjs +582 -171
- 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 +442 -9
- package/dist/bin/browser-agent-stdio-server.cjs.map +1 -1
- package/dist/bin/browser-agent-stdio-server.js +2 -2
- package/dist/bin/mcp-scraper-combined-stdio-server.cjs +519 -31
- package/dist/bin/mcp-scraper-combined-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-scraper-combined-stdio-server.js +3 -3
- package/dist/bin/mcp-scraper-install.cjs +2 -1
- package/dist/bin/mcp-scraper-install.cjs.map +1 -1
- package/dist/bin/mcp-scraper-install.js +2 -1
- package/dist/bin/mcp-scraper-install.js.map +1 -1
- package/dist/bin/mcp-stdio-server.cjs +68 -13
- 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 +42 -6
- package/dist/bin/paa-harvest.cjs.map +1 -1
- package/dist/bin/paa-harvest.js +1 -1
- package/dist/chunk-5HMOPP76.js +793 -0
- package/dist/chunk-5HMOPP76.js.map +1 -0
- package/dist/{chunk-7WB2W6FO.js → chunk-6NEXSNSA.js} +69 -14
- package/dist/chunk-6NEXSNSA.js.map +1 -0
- package/dist/{chunk-MY3S7EX7.js → chunk-CQTAKXBN.js} +43 -7
- package/dist/chunk-CQTAKXBN.js.map +1 -0
- package/dist/chunk-I26QN7WQ.js +7 -0
- package/dist/chunk-I26QN7WQ.js.map +1 -0
- package/dist/index.cjs +42 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/dist/{server-MKCU3M7Y.js → server-BTTDFPSQ.js} +456 -142
- package/dist/server-BTTDFPSQ.js.map +1 -0
- package/dist/{worker-NAKGTIF5.js → worker-5O44YBF4.js} +2 -2
- package/docs/mcp-tool-quality-spec.md +3 -1
- package/package.json +1 -1
- package/dist/chunk-7WB2W6FO.js.map +0 -1
- package/dist/chunk-MY3S7EX7.js.map +0 -1
- package/dist/chunk-T6SIPPOQ.js +0 -7
- package/dist/chunk-T6SIPPOQ.js.map +0 -1
- package/dist/chunk-XL4V7VKD.js +0 -360
- package/dist/chunk-XL4V7VKD.js.map +0 -1
- package/dist/server-MKCU3M7Y.js.map +0 -1
- /package/dist/{worker-NAKGTIF5.js.map → worker-5O44YBF4.js.map} +0 -0
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
// bin/mcp-scraper-combined-stdio-server.ts
|
|
5
5
|
var import_node_fs4 = require("fs");
|
|
6
|
-
var
|
|
7
|
-
var
|
|
6
|
+
var import_node_os4 = require("os");
|
|
7
|
+
var import_node_path5 = require("path");
|
|
8
8
|
var import_mcp3 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
9
9
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
10
10
|
|
|
@@ -134,11 +134,11 @@ var HttpMcpToolExecutor = class {
|
|
|
134
134
|
// src/mcp/browser-agent-mcp-server.ts
|
|
135
135
|
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
136
136
|
var import_node_fs = require("fs");
|
|
137
|
-
var
|
|
138
|
-
var
|
|
137
|
+
var import_node_os2 = require("os");
|
|
138
|
+
var import_node_path2 = require("path");
|
|
139
139
|
|
|
140
140
|
// src/version.ts
|
|
141
|
-
var PACKAGE_VERSION = "0.2.
|
|
141
|
+
var PACKAGE_VERSION = "0.2.8";
|
|
142
142
|
|
|
143
143
|
// src/mcp/browser-agent-tool-schemas.ts
|
|
144
144
|
var import_zod = require("zod");
|
|
@@ -151,6 +151,19 @@ var BrowserOpenInputSchema = {
|
|
|
151
151
|
var BrowserSessionInputSchema = {
|
|
152
152
|
session_id: import_zod.z.string().describe("The session id returned by browser_open.")
|
|
153
153
|
};
|
|
154
|
+
var BrowserLocateTargetSchema = import_zod.z.object({
|
|
155
|
+
name: import_zod.z.string().optional().describe("Optional label for this target, echoed in the result."),
|
|
156
|
+
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"].'),
|
|
157
|
+
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."),
|
|
158
|
+
match: import_zod.z.enum(["contains", "exact"]).default("contains").describe("How to match text targets. Defaults to contains."),
|
|
159
|
+
index: import_zod.z.number().int().min(0).default(0).describe("Zero-based match index when multiple elements match.")
|
|
160
|
+
}).refine((value) => Boolean(value.selector || value.text), {
|
|
161
|
+
message: "target requires selector or text"
|
|
162
|
+
});
|
|
163
|
+
var BrowserLocateInputSchema = {
|
|
164
|
+
session_id: import_zod.z.string().describe("The session id returned by browser_open."),
|
|
165
|
+
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.")
|
|
166
|
+
};
|
|
154
167
|
var BrowserGotoInputSchema = {
|
|
155
168
|
session_id: import_zod.z.string().describe("The session id returned by browser_open."),
|
|
156
169
|
url: import_zod.z.string().url().describe("URL to navigate the browser to.")
|
|
@@ -187,16 +200,315 @@ var BrowserReplayDownloadInputSchema = {
|
|
|
187
200
|
replay_id: import_zod.z.string().describe("The replay id returned by browser_replay_start or browser_list_replays."),
|
|
188
201
|
filename: import_zod.z.string().optional().describe("Optional local MP4 filename. Defaults to a timestamped replay filename.")
|
|
189
202
|
};
|
|
203
|
+
var BrowserReplayAnnotationSchema = import_zod.z.object({
|
|
204
|
+
type: import_zod.z.enum(["box", "circle", "underline", "arrow", "label"]).default("box").describe("Annotation style to draw over the replay."),
|
|
205
|
+
start_seconds: import_zod.z.number().min(0).default(0).describe("When the annotation should appear in the replay."),
|
|
206
|
+
end_seconds: import_zod.z.number().min(0).optional().describe("When the annotation should disappear. Defaults to two seconds after start_seconds."),
|
|
207
|
+
left: import_zod.z.number().optional().describe("Target left edge in browser screenshot pixels. Use element.left from browser_screenshot/browser_read."),
|
|
208
|
+
top: import_zod.z.number().optional().describe("Target top edge in browser screenshot pixels. Use element.top from browser_screenshot/browser_read."),
|
|
209
|
+
width: import_zod.z.number().positive().optional().describe("Target width in browser screenshot pixels. Use element.width from browser_screenshot/browser_read."),
|
|
210
|
+
height: import_zod.z.number().positive().optional().describe("Target height in browser screenshot pixels. Use element.height from browser_screenshot/browser_read."),
|
|
211
|
+
x: import_zod.z.number().optional().describe("Point target x coordinate in browser screenshot pixels when no box is available."),
|
|
212
|
+
y: import_zod.z.number().optional().describe("Point target y coordinate in browser screenshot pixels when no box is available."),
|
|
213
|
+
from_x: import_zod.z.number().optional().describe("Arrow start x coordinate in browser screenshot pixels. Defaults near the target."),
|
|
214
|
+
from_y: import_zod.z.number().optional().describe("Arrow start y coordinate in browser screenshot pixels. Defaults near the target."),
|
|
215
|
+
to_x: import_zod.z.number().optional().describe("Arrow target x coordinate in browser screenshot pixels. Defaults to the target box center."),
|
|
216
|
+
to_y: import_zod.z.number().optional().describe("Arrow target y coordinate in browser screenshot pixels. Defaults to the target box center."),
|
|
217
|
+
label: import_zod.z.string().max(120).optional().describe("Optional text callout to render near the annotation."),
|
|
218
|
+
color: import_zod.z.string().regex(/^#?[0-9a-fA-F]{6}$/).optional().describe("Annotation color as hex, for example #ff3b30."),
|
|
219
|
+
thickness: import_zod.z.number().min(1).max(24).optional().describe("Stroke thickness in pixels. Defaults to 5.")
|
|
220
|
+
});
|
|
221
|
+
var BrowserReplayMarkInputSchema = {
|
|
222
|
+
session_id: import_zod.z.string().describe("The session id returned by browser_open. A replay must already be recording."),
|
|
223
|
+
target: BrowserLocateTargetSchema.describe("The exact DOM element or text range to mark in the current viewport."),
|
|
224
|
+
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."),
|
|
225
|
+
label: import_zod.z.string().max(120).optional().describe("Optional callout text to render near the target."),
|
|
226
|
+
color: import_zod.z.string().regex(/^#?[0-9a-fA-F]{6}$/).optional().describe("Annotation color as hex, for example #ff3b30."),
|
|
227
|
+
thickness: import_zod.z.number().min(1).max(24).optional().describe("Stroke thickness in pixels. Defaults to 5."),
|
|
228
|
+
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."),
|
|
229
|
+
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."),
|
|
230
|
+
duration_seconds: import_zod.z.number().min(0.5).max(30).default(4).describe("How long the generated annotation should remain visible.")
|
|
231
|
+
};
|
|
232
|
+
var BrowserReplayAnnotateInputSchema = {
|
|
233
|
+
session_id: import_zod.z.string().describe("The session id returned by browser_open."),
|
|
234
|
+
replay_id: import_zod.z.string().describe("The replay id returned by browser_replay_start or browser_list_replays."),
|
|
235
|
+
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."),
|
|
236
|
+
filename: import_zod.z.string().optional().describe("Optional output MP4 filename. Defaults to a timestamped annotated replay filename."),
|
|
237
|
+
source_width: import_zod.z.number().positive().optional().describe("Width of the screenshot coordinate space used for annotations. Defaults to the replay video width."),
|
|
238
|
+
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."),
|
|
239
|
+
source_left_offset: import_zod.z.number().min(0).optional().describe("Optional explicit X offset from annotation coordinates to replay video coordinates. Usually omitted."),
|
|
240
|
+
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.")
|
|
241
|
+
};
|
|
190
242
|
var BrowserListInputSchema = {
|
|
191
243
|
include_closed: import_zod.z.boolean().default(false).describe("Include closed sessions in the list.")
|
|
192
244
|
};
|
|
193
245
|
|
|
246
|
+
// src/mcp/replay-annotator.ts
|
|
247
|
+
var import_node_child_process = require("child_process");
|
|
248
|
+
var import_promises = require("fs/promises");
|
|
249
|
+
var import_node_os = require("os");
|
|
250
|
+
var import_node_path = require("path");
|
|
251
|
+
var import_node_util = require("util");
|
|
252
|
+
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
253
|
+
function finiteNumber(value) {
|
|
254
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
255
|
+
}
|
|
256
|
+
function clamp(value, min, max) {
|
|
257
|
+
return Math.min(max, Math.max(min, value));
|
|
258
|
+
}
|
|
259
|
+
function formatAssTime(seconds) {
|
|
260
|
+
const safe = Math.max(0, seconds);
|
|
261
|
+
const hours = Math.floor(safe / 3600);
|
|
262
|
+
const minutes = Math.floor(safe % 3600 / 60);
|
|
263
|
+
const wholeSeconds = Math.floor(safe % 60);
|
|
264
|
+
const centiseconds = Math.floor((safe - Math.floor(safe)) * 100);
|
|
265
|
+
return `${hours}:${String(minutes).padStart(2, "0")}:${String(wholeSeconds).padStart(2, "0")}.${String(centiseconds).padStart(2, "0")}`;
|
|
266
|
+
}
|
|
267
|
+
function cssHexToAssColor(color) {
|
|
268
|
+
const normalized = color?.trim().match(/^#?([0-9a-fA-F]{6})$/)?.[1] ?? "ff3b30";
|
|
269
|
+
const red = normalized.slice(0, 2);
|
|
270
|
+
const green = normalized.slice(2, 4);
|
|
271
|
+
const blue = normalized.slice(4, 6);
|
|
272
|
+
return `&H${blue}${green}${red}&`;
|
|
273
|
+
}
|
|
274
|
+
function escapeAssText(value) {
|
|
275
|
+
return value.replace(/\\/g, "\\\\").replace(/\{/g, "\\{").replace(/\}/g, "\\}").replace(/\r?\n/g, "\\N");
|
|
276
|
+
}
|
|
277
|
+
function resolveCoordinateTransform(video, source, options) {
|
|
278
|
+
const explicitOffsetX = finiteNumber(options.sourceLeftOffset) ? options.sourceLeftOffset : null;
|
|
279
|
+
const explicitOffsetY = finiteNumber(options.sourceTopOffset) ? options.sourceTopOffset : null;
|
|
280
|
+
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;
|
|
281
|
+
const offsetX = explicitOffsetX ?? 0;
|
|
282
|
+
const offsetY = explicitOffsetY ?? inferredTopOffset;
|
|
283
|
+
return {
|
|
284
|
+
scaleX: (video.width - offsetX) / source.width,
|
|
285
|
+
scaleY: (video.height - offsetY) / source.height,
|
|
286
|
+
offsetX,
|
|
287
|
+
offsetY
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
function transformPoint(x, y, transform) {
|
|
291
|
+
return {
|
|
292
|
+
x: x * transform.scaleX + transform.offsetX,
|
|
293
|
+
y: y * transform.scaleY + transform.offsetY
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
function scaleRect(annotation, transform) {
|
|
297
|
+
if (finiteNumber(annotation.left) && finiteNumber(annotation.top) && finiteNumber(annotation.width) && finiteNumber(annotation.height)) {
|
|
298
|
+
const origin = transformPoint(annotation.left, annotation.top, transform);
|
|
299
|
+
return {
|
|
300
|
+
left: origin.x,
|
|
301
|
+
top: origin.y,
|
|
302
|
+
width: annotation.width * transform.scaleX,
|
|
303
|
+
height: annotation.height * transform.scaleY
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
if (finiteNumber(annotation.x) && finiteNumber(annotation.y)) {
|
|
307
|
+
const radius = finiteNumber(annotation.width) ? annotation.width / 2 : 28;
|
|
308
|
+
const point = transformPoint(annotation.x, annotation.y, transform);
|
|
309
|
+
return {
|
|
310
|
+
left: point.x - radius * transform.scaleX,
|
|
311
|
+
top: point.y - radius * transform.scaleY,
|
|
312
|
+
width: radius * 2 * transform.scaleX,
|
|
313
|
+
height: radius * 2 * transform.scaleY
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
throw new Error("annotation needs either left/top/width/height or x/y");
|
|
317
|
+
}
|
|
318
|
+
function rectPath(rect) {
|
|
319
|
+
const x1 = Math.round(rect.left);
|
|
320
|
+
const y1 = Math.round(rect.top);
|
|
321
|
+
const x2 = Math.round(rect.left + rect.width);
|
|
322
|
+
const y2 = Math.round(rect.top + rect.height);
|
|
323
|
+
return `m ${x1} ${y1} l ${x2} ${y1} l ${x2} ${y2} l ${x1} ${y2} l ${x1} ${y1}`;
|
|
324
|
+
}
|
|
325
|
+
function ellipsePath(rect) {
|
|
326
|
+
const cx = rect.left + rect.width / 2;
|
|
327
|
+
const cy = rect.top + rect.height / 2;
|
|
328
|
+
const rx = Math.max(4, rect.width / 2);
|
|
329
|
+
const ry = Math.max(4, rect.height / 2);
|
|
330
|
+
const points = [];
|
|
331
|
+
for (let i = 0; i <= 32; i += 1) {
|
|
332
|
+
const t = Math.PI * 2 * i / 32;
|
|
333
|
+
const x = Math.round(cx + Math.cos(t) * rx);
|
|
334
|
+
const y = Math.round(cy + Math.sin(t) * ry);
|
|
335
|
+
points.push(`${i === 0 ? "m" : "l"} ${x} ${y}`);
|
|
336
|
+
}
|
|
337
|
+
return points.join(" ");
|
|
338
|
+
}
|
|
339
|
+
function filledRectPath(left, top, width, height) {
|
|
340
|
+
return rectPath({ left, top, width, height });
|
|
341
|
+
}
|
|
342
|
+
function arrowPath(fromX, fromY, toX, toY, thickness) {
|
|
343
|
+
const dx = toX - fromX;
|
|
344
|
+
const dy = toY - fromY;
|
|
345
|
+
const length = Math.max(1, Math.hypot(dx, dy));
|
|
346
|
+
const ux = dx / length;
|
|
347
|
+
const uy = dy / length;
|
|
348
|
+
const px = -uy;
|
|
349
|
+
const py = ux;
|
|
350
|
+
const half = thickness / 2;
|
|
351
|
+
const head = Math.max(14, thickness * 4);
|
|
352
|
+
const baseX = toX - ux * head;
|
|
353
|
+
const baseY = toY - uy * head;
|
|
354
|
+
const p1x = Math.round(fromX + px * half);
|
|
355
|
+
const p1y = Math.round(fromY + py * half);
|
|
356
|
+
const p2x = Math.round(baseX + px * half);
|
|
357
|
+
const p2y = Math.round(baseY + py * half);
|
|
358
|
+
const p3x = Math.round(baseX + px * head * 0.55);
|
|
359
|
+
const p3y = Math.round(baseY + py * head * 0.55);
|
|
360
|
+
const p4x = Math.round(toX);
|
|
361
|
+
const p4y = Math.round(toY);
|
|
362
|
+
const p5x = Math.round(baseX - px * head * 0.55);
|
|
363
|
+
const p5y = Math.round(baseY - py * head * 0.55);
|
|
364
|
+
const p6x = Math.round(baseX - px * half);
|
|
365
|
+
const p6y = Math.round(baseY - py * half);
|
|
366
|
+
const p7x = Math.round(fromX - px * half);
|
|
367
|
+
const p7y = Math.round(fromY - py * half);
|
|
368
|
+
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}`;
|
|
369
|
+
}
|
|
370
|
+
function eventLine(start, end, body) {
|
|
371
|
+
return `Dialogue: 0,${formatAssTime(start)},${formatAssTime(end)},Default,,0,0,0,,${body}`;
|
|
372
|
+
}
|
|
373
|
+
function labelLine(start, end, x, y, label) {
|
|
374
|
+
const safeX = Math.round(x);
|
|
375
|
+
const safeY = Math.round(y);
|
|
376
|
+
return eventLine(
|
|
377
|
+
start,
|
|
378
|
+
end,
|
|
379
|
+
`{\\an7\\pos(${safeX},${safeY})\\fs30\\bord4\\shad0\\c&HFFFFFF&\\3c&H000000&}${escapeAssText(label)}`
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
function shapeLine(start, end, path, color, thickness, filled = false) {
|
|
383
|
+
const fillAlpha = filled ? "&H00&" : "&HFF&";
|
|
384
|
+
const border = filled ? 0 : thickness;
|
|
385
|
+
return eventLine(
|
|
386
|
+
start,
|
|
387
|
+
end,
|
|
388
|
+
`{\\an7\\pos(0,0)\\p1\\bord${border}\\shad0\\1a${fillAlpha}\\1c${color}\\3c${color}}${path}`
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
function buildAssSubtitle(options, video) {
|
|
392
|
+
const source = {
|
|
393
|
+
width: options.sourceWidth && options.sourceWidth > 0 ? options.sourceWidth : video.width,
|
|
394
|
+
height: options.sourceHeight && options.sourceHeight > 0 ? options.sourceHeight : video.height
|
|
395
|
+
};
|
|
396
|
+
const transform = resolveCoordinateTransform(video, source, options);
|
|
397
|
+
const lines = [];
|
|
398
|
+
for (const annotation of options.annotations) {
|
|
399
|
+
const start = Math.max(0, annotation.start_seconds ?? 0);
|
|
400
|
+
const end = annotation.end_seconds && annotation.end_seconds > start ? annotation.end_seconds : start + 2;
|
|
401
|
+
const type = annotation.type ?? "box";
|
|
402
|
+
const color = cssHexToAssColor(annotation.color);
|
|
403
|
+
const thickness = Math.round(clamp(annotation.thickness ?? 5, 2, 24));
|
|
404
|
+
if (type === "label") {
|
|
405
|
+
if (!annotation.label) throw new Error("label annotation needs label");
|
|
406
|
+
const rect2 = scaleRect(annotation, transform);
|
|
407
|
+
lines.push(labelLine(start, end, rect2.left, Math.max(8, rect2.top), annotation.label));
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
if (type === "arrow") {
|
|
411
|
+
const rect2 = finiteNumber(annotation.to_x) && finiteNumber(annotation.to_y) ? null : scaleRect(annotation, transform);
|
|
412
|
+
const toPoint = finiteNumber(annotation.to_x) && finiteNumber(annotation.to_y) ? transformPoint(annotation.to_x, annotation.to_y, transform) : null;
|
|
413
|
+
const toX = toPoint ? toPoint.x : rect2.left + rect2.width / 2;
|
|
414
|
+
const toY = toPoint ? toPoint.y : rect2.top + rect2.height / 2;
|
|
415
|
+
const fromPoint = finiteNumber(annotation.from_x) && finiteNumber(annotation.from_y) ? transformPoint(annotation.from_x, annotation.from_y, transform) : null;
|
|
416
|
+
const fromX = fromPoint ? fromPoint.x : clamp(toX - 120, 12, video.width - 12);
|
|
417
|
+
const fromY = fromPoint ? fromPoint.y : clamp(toY - 90, 12, video.height - 12);
|
|
418
|
+
lines.push(shapeLine(start, end, arrowPath(fromX, fromY, toX, toY, thickness), color, thickness, true));
|
|
419
|
+
if (annotation.label) lines.push(labelLine(start, end, fromX + 8, Math.max(96, fromY - 36), annotation.label));
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
const rect = scaleRect(annotation, transform);
|
|
423
|
+
if (type === "underline") {
|
|
424
|
+
const underlineTop = rect.top + rect.height + thickness;
|
|
425
|
+
lines.push(shapeLine(start, end, filledRectPath(rect.left, underlineTop, rect.width, thickness), color, 0, true));
|
|
426
|
+
} else if (type === "circle") {
|
|
427
|
+
lines.push(shapeLine(start, end, ellipsePath(rect), color, thickness));
|
|
428
|
+
} else {
|
|
429
|
+
lines.push(shapeLine(start, end, rectPath(rect), color, thickness));
|
|
430
|
+
}
|
|
431
|
+
if (annotation.label) lines.push(labelLine(start, end, rect.left, Math.max(8, rect.top - 38), annotation.label));
|
|
432
|
+
}
|
|
433
|
+
return [
|
|
434
|
+
"[Script Info]",
|
|
435
|
+
"ScriptType: v4.00+",
|
|
436
|
+
`PlayResX: ${Math.round(video.width)}`,
|
|
437
|
+
`PlayResY: ${Math.round(video.height)}`,
|
|
438
|
+
"ScaledBorderAndShadow: yes",
|
|
439
|
+
"",
|
|
440
|
+
"[V4+ Styles]",
|
|
441
|
+
"Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding",
|
|
442
|
+
"Style: Default,Arial,30,&H00FFFFFF,&H000000FF,&H00000000,&H90000000,0,0,0,0,100,100,0,0,1,2,0,7,0,0,0,1",
|
|
443
|
+
"",
|
|
444
|
+
"[Events]",
|
|
445
|
+
"Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text",
|
|
446
|
+
...lines,
|
|
447
|
+
""
|
|
448
|
+
].join("\n");
|
|
449
|
+
}
|
|
450
|
+
async function videoSize(inputFilePath) {
|
|
451
|
+
const { stdout } = await execFileAsync("ffprobe", [
|
|
452
|
+
"-v",
|
|
453
|
+
"error",
|
|
454
|
+
"-select_streams",
|
|
455
|
+
"v:0",
|
|
456
|
+
"-show_entries",
|
|
457
|
+
"stream=width,height",
|
|
458
|
+
"-of",
|
|
459
|
+
"json",
|
|
460
|
+
inputFilePath
|
|
461
|
+
], { maxBuffer: 1024 * 1024 });
|
|
462
|
+
const parsed = JSON.parse(stdout);
|
|
463
|
+
const stream = parsed.streams?.[0];
|
|
464
|
+
if (!stream?.width || !stream.height) throw new Error("could not read replay video dimensions");
|
|
465
|
+
return { width: stream.width, height: stream.height };
|
|
466
|
+
}
|
|
467
|
+
function ffmpegFilterPath(path) {
|
|
468
|
+
return path.replace(/\\/g, "\\\\").replace(/:/g, "\\:");
|
|
469
|
+
}
|
|
470
|
+
async function annotateReplayVideo(inputFilePath, outputFilePath, options) {
|
|
471
|
+
if (!options.annotations.length) throw new Error("annotations must include at least one item");
|
|
472
|
+
const size = await videoSize(inputFilePath);
|
|
473
|
+
const tmp = await (0, import_promises.mkdtemp)((0, import_node_path.join)((0, import_node_os.tmpdir)(), "mcp-scraper-ass-"));
|
|
474
|
+
const assPath = (0, import_node_path.join)(tmp, "annotations.ass");
|
|
475
|
+
try {
|
|
476
|
+
await (0, import_promises.writeFile)(assPath, buildAssSubtitle(options, size), "utf8");
|
|
477
|
+
await execFileAsync("ffmpeg", [
|
|
478
|
+
"-y",
|
|
479
|
+
"-i",
|
|
480
|
+
inputFilePath,
|
|
481
|
+
"-vf",
|
|
482
|
+
`ass=${ffmpegFilterPath(assPath)}`,
|
|
483
|
+
"-c:v",
|
|
484
|
+
"libx264",
|
|
485
|
+
"-pix_fmt",
|
|
486
|
+
"yuv420p",
|
|
487
|
+
"-movflags",
|
|
488
|
+
"+faststart",
|
|
489
|
+
"-c:a",
|
|
490
|
+
"copy",
|
|
491
|
+
outputFilePath
|
|
492
|
+
], { maxBuffer: 1024 * 1024 * 20 });
|
|
493
|
+
const out = await (0, import_promises.stat)(outputFilePath);
|
|
494
|
+
return {
|
|
495
|
+
filePath: outputFilePath,
|
|
496
|
+
bytes: out.size,
|
|
497
|
+
width: size.width,
|
|
498
|
+
height: size.height,
|
|
499
|
+
annotationCount: options.annotations.length
|
|
500
|
+
};
|
|
501
|
+
} finally {
|
|
502
|
+
await (0, import_promises.rm)(tmp, { recursive: true, force: true });
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
194
506
|
// src/mcp/browser-agent-mcp-server.ts
|
|
195
507
|
function textResult(value, isError = false) {
|
|
196
508
|
return { content: [{ type: "text", text: JSON.stringify(value) }], isError };
|
|
197
509
|
}
|
|
198
510
|
function outputBaseDir() {
|
|
199
|
-
return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0,
|
|
511
|
+
return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path2.join)((0, import_node_os2.homedir)(), "Downloads", "mcp-scraper");
|
|
200
512
|
}
|
|
201
513
|
function safeFilePart(value) {
|
|
202
514
|
return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 120) || "replay";
|
|
@@ -205,7 +517,32 @@ function replayFilePath(sessionId, replayId, filename) {
|
|
|
205
517
|
const requested = filename?.trim();
|
|
206
518
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
207
519
|
const name = requested ? safeFilePart(requested).replace(/\.mp4$/i, "") : `${stamp}-${safeFilePart(sessionId)}-${safeFilePart(replayId)}`;
|
|
208
|
-
return (0,
|
|
520
|
+
return (0, import_node_path2.join)(outputBaseDir(), "browser-replays", `${name}.mp4`);
|
|
521
|
+
}
|
|
522
|
+
function annotatedReplayFilePath(sessionId, replayId, filename) {
|
|
523
|
+
const requested = filename?.trim();
|
|
524
|
+
if (requested) return replayFilePath(sessionId, replayId, requested);
|
|
525
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
526
|
+
return replayFilePath(sessionId, replayId, `${stamp}-${safeFilePart(sessionId)}-${safeFilePart(replayId)}-annotated`);
|
|
527
|
+
}
|
|
528
|
+
function finiteNumber2(value) {
|
|
529
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
530
|
+
}
|
|
531
|
+
function expandElementBounds(element, viewport, padding) {
|
|
532
|
+
const sourceWidth = finiteNumber2(viewport?.width) && viewport.width > 0 ? viewport.width : 1920;
|
|
533
|
+
const sourceHeight = finiteNumber2(viewport?.height) && viewport.height > 0 ? viewport.height : 1080;
|
|
534
|
+
const left = Math.max(0, element.left - padding);
|
|
535
|
+
const top = Math.max(0, element.top - padding);
|
|
536
|
+
const right = Math.min(sourceWidth, element.left + element.width + padding);
|
|
537
|
+
const bottom = Math.min(sourceHeight, element.top + element.height + padding);
|
|
538
|
+
return {
|
|
539
|
+
left,
|
|
540
|
+
top,
|
|
541
|
+
width: Math.max(1, right - left),
|
|
542
|
+
height: Math.max(1, bottom - top),
|
|
543
|
+
sourceWidth,
|
|
544
|
+
sourceHeight
|
|
545
|
+
};
|
|
209
546
|
}
|
|
210
547
|
function registerBrowserAgentMcpTools(server2, opts) {
|
|
211
548
|
const baseUrl2 = opts.baseUrl.replace(/\/$/, "");
|
|
@@ -239,7 +576,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
239
576
|
}
|
|
240
577
|
const bytes = Buffer.from(await res.arrayBuffer());
|
|
241
578
|
const filePath = replayFilePath(sessionId, replayId, filename);
|
|
242
|
-
(0, import_node_fs.mkdirSync)((0,
|
|
579
|
+
(0, import_node_fs.mkdirSync)((0, import_node_path2.join)(outputBaseDir(), "browser-replays"), { recursive: true });
|
|
243
580
|
(0, import_node_fs.writeFileSync)(filePath, bytes);
|
|
244
581
|
return {
|
|
245
582
|
ok: true,
|
|
@@ -323,6 +660,19 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
323
660
|
return textResult(res.data, !res.ok);
|
|
324
661
|
}
|
|
325
662
|
);
|
|
663
|
+
server2.registerTool(
|
|
664
|
+
"browser_locate",
|
|
665
|
+
{
|
|
666
|
+
title: "Locate DOM Targets",
|
|
667
|
+
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.",
|
|
668
|
+
inputSchema: BrowserLocateInputSchema,
|
|
669
|
+
annotations: annotations("Locate DOM Targets", true)
|
|
670
|
+
},
|
|
671
|
+
async (input) => {
|
|
672
|
+
const res = await req("POST", `/agent/sessions/${input.session_id}/locate`, { targets: input.targets });
|
|
673
|
+
return textResult(res.data, !res.ok);
|
|
674
|
+
}
|
|
675
|
+
);
|
|
326
676
|
server2.registerTool(
|
|
327
677
|
"browser_goto",
|
|
328
678
|
{
|
|
@@ -450,6 +800,89 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
450
800
|
return textResult(res.data, !res.ok);
|
|
451
801
|
}
|
|
452
802
|
);
|
|
803
|
+
server2.registerTool(
|
|
804
|
+
"browser_replay_mark",
|
|
805
|
+
{
|
|
806
|
+
title: "Mark Replay Annotation",
|
|
807
|
+
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.",
|
|
808
|
+
inputSchema: BrowserReplayMarkInputSchema,
|
|
809
|
+
annotations: annotations("Mark Replay Annotation", true)
|
|
810
|
+
},
|
|
811
|
+
async (input) => {
|
|
812
|
+
const res = await req("POST", `/agent/sessions/${input.session_id}/locate`, { targets: [input.target] });
|
|
813
|
+
if (!res.ok) return textResult(res.data, true);
|
|
814
|
+
const target = res.data?.targets?.[0];
|
|
815
|
+
const element = target?.element;
|
|
816
|
+
const elapsed = res.data?.replay?.replay_elapsed_seconds;
|
|
817
|
+
if (!target?.found || !element) {
|
|
818
|
+
return textResult({ error: target?.error ?? "target not found in current viewport", target }, true);
|
|
819
|
+
}
|
|
820
|
+
if (!finiteNumber2(elapsed)) {
|
|
821
|
+
return textResult({ error: "no active replay clock found; call browser_replay_start before browser_replay_mark" }, true);
|
|
822
|
+
}
|
|
823
|
+
const padded = expandElementBounds(element, res.data?.viewport, input.padding ?? 8);
|
|
824
|
+
const start = Math.max(0, elapsed + (input.start_offset_seconds ?? -0.25));
|
|
825
|
+
const duration = input.duration_seconds ?? 4;
|
|
826
|
+
const annotation = {
|
|
827
|
+
type: input.type ?? "box",
|
|
828
|
+
start_seconds: Number(start.toFixed(3)),
|
|
829
|
+
end_seconds: Number((start + duration).toFixed(3)),
|
|
830
|
+
left: Math.round(padded.left),
|
|
831
|
+
top: Math.round(padded.top),
|
|
832
|
+
width: Math.round(padded.width),
|
|
833
|
+
height: Math.round(padded.height),
|
|
834
|
+
...input.label ? { label: input.label } : {},
|
|
835
|
+
...input.color ? { color: input.color } : {},
|
|
836
|
+
...input.thickness ? { thickness: input.thickness } : {}
|
|
837
|
+
};
|
|
838
|
+
return textResult({
|
|
839
|
+
annotation,
|
|
840
|
+
source_width: padded.sourceWidth,
|
|
841
|
+
source_height: padded.sourceHeight,
|
|
842
|
+
replay: res.data.replay,
|
|
843
|
+
target,
|
|
844
|
+
hint: "Append annotation to your annotations array. Use source_width/source_height with browser_replay_annotate."
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
);
|
|
848
|
+
server2.registerTool(
|
|
849
|
+
"browser_replay_annotate",
|
|
850
|
+
{
|
|
851
|
+
title: "Annotate Replay MP4",
|
|
852
|
+
description: "Download a browser replay MP4, render visual annotations over it, and save a new annotated MP4 locally. 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.",
|
|
853
|
+
inputSchema: BrowserReplayAnnotateInputSchema,
|
|
854
|
+
annotations: annotations("Annotate Replay MP4", true)
|
|
855
|
+
},
|
|
856
|
+
async (input) => {
|
|
857
|
+
const sourceName = input.filename ? `${input.filename}-source` : void 0;
|
|
858
|
+
const downloaded = await downloadReplay(input.session_id, input.replay_id, sourceName);
|
|
859
|
+
if (!downloaded.ok) return textResult(downloaded.data, true);
|
|
860
|
+
try {
|
|
861
|
+
const sourcePath = String(downloaded.data.file_path);
|
|
862
|
+
const outputPath = annotatedReplayFilePath(input.session_id, input.replay_id, input.filename);
|
|
863
|
+
(0, import_node_fs.mkdirSync)((0, import_node_path2.join)(outputBaseDir(), "browser-replays"), { recursive: true });
|
|
864
|
+
const result = await annotateReplayVideo(sourcePath, outputPath, {
|
|
865
|
+
annotations: input.annotations,
|
|
866
|
+
sourceWidth: input.source_width,
|
|
867
|
+
sourceHeight: input.source_height,
|
|
868
|
+
sourceLeftOffset: input.source_left_offset,
|
|
869
|
+
sourceTopOffset: input.source_top_offset
|
|
870
|
+
});
|
|
871
|
+
return textResult({
|
|
872
|
+
replay_id: input.replay_id,
|
|
873
|
+
source_file_path: sourcePath,
|
|
874
|
+
annotated_file_path: result.filePath,
|
|
875
|
+
bytes: result.bytes,
|
|
876
|
+
width: result.width,
|
|
877
|
+
height: result.height,
|
|
878
|
+
annotation_count: result.annotationCount,
|
|
879
|
+
mime_type: "video/mp4"
|
|
880
|
+
});
|
|
881
|
+
} catch (err) {
|
|
882
|
+
return textResult({ error: err instanceof Error ? err.message : String(err) }, true);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
);
|
|
453
886
|
server2.registerTool(
|
|
454
887
|
"browser_close",
|
|
455
888
|
{
|
|
@@ -483,12 +916,12 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
483
916
|
// src/mcp/paa-mcp-server.ts
|
|
484
917
|
var import_mcp2 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
485
918
|
var import_node_fs3 = require("fs");
|
|
486
|
-
var
|
|
919
|
+
var import_node_path4 = require("path");
|
|
487
920
|
|
|
488
921
|
// src/mcp/mcp-response-formatter.ts
|
|
489
922
|
var import_node_fs2 = require("fs");
|
|
490
|
-
var
|
|
491
|
-
var
|
|
923
|
+
var import_node_os3 = require("os");
|
|
924
|
+
var import_node_path3 = require("path");
|
|
492
925
|
|
|
493
926
|
// src/errors.ts
|
|
494
927
|
function sanitizeVendorName(message) {
|
|
@@ -510,7 +943,7 @@ function reportTitle(full) {
|
|
|
510
943
|
return title?.replace(/^#\s+/, "").trim() || "MCP Scraper Report";
|
|
511
944
|
}
|
|
512
945
|
function outputBaseDir2() {
|
|
513
|
-
return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0,
|
|
946
|
+
return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path3.join)((0, import_node_os3.homedir)(), "Downloads", "mcp-scraper");
|
|
514
947
|
}
|
|
515
948
|
function saveFullReport(full) {
|
|
516
949
|
if (!reportSavingEnabled || process.env.MCP_SCRAPER_SAVE_REPORTS === "false") return null;
|
|
@@ -518,7 +951,7 @@ function saveFullReport(full) {
|
|
|
518
951
|
try {
|
|
519
952
|
(0, import_node_fs2.mkdirSync)(outDir, { recursive: true });
|
|
520
953
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
521
|
-
const file = (0,
|
|
954
|
+
const file = (0, import_node_path3.join)(outDir, `${stamp}-${slugifyReportName(reportTitle(full))}.md`);
|
|
522
955
|
(0, import_node_fs2.writeFileSync)(file, full, "utf8");
|
|
523
956
|
return file;
|
|
524
957
|
} catch {
|
|
@@ -528,11 +961,11 @@ function saveFullReport(full) {
|
|
|
528
961
|
function persistScreenshotLocally(base64, url) {
|
|
529
962
|
if (!reportSavingEnabled || process.env.MCP_SCRAPER_SAVE_REPORTS === "false") return null;
|
|
530
963
|
try {
|
|
531
|
-
const dir = (0,
|
|
964
|
+
const dir = (0, import_node_path3.join)(outputBaseDir2(), "screenshots");
|
|
532
965
|
(0, import_node_fs2.mkdirSync)(dir, { recursive: true });
|
|
533
966
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
534
967
|
const slug = url.replace(/^https?:\/\//, "").replace(/[^a-z0-9]+/gi, "-").replace(/^-+|-+$/g, "").slice(0, 60);
|
|
535
|
-
const filePath = (0,
|
|
968
|
+
const filePath = (0, import_node_path3.join)(dir, `${stamp}-${slug}.png`);
|
|
536
969
|
(0, import_node_fs2.writeFileSync)(filePath, Buffer.from(base64, "base64"));
|
|
537
970
|
return filePath;
|
|
538
971
|
} catch {
|
|
@@ -623,12 +1056,17 @@ function errorAttemptsSection(body) {
|
|
|
623
1056
|
const browser = debug.browser ?? {};
|
|
624
1057
|
const kernel = browser.browserRuntime ?? browser.kernel ?? {};
|
|
625
1058
|
const proxyResolution = kernel.proxyResolution ?? {};
|
|
626
|
-
const network = browser.networkLocation ?? {
|
|
1059
|
+
const network = browser.networkLocation ?? {
|
|
1060
|
+
ip: attempt.observedIp ?? attempt.observed_ip,
|
|
1061
|
+
city: attempt.observedCity ?? attempt.observed_city,
|
|
1062
|
+
region: attempt.observedRegion ?? attempt.observed_region
|
|
1063
|
+
};
|
|
627
1064
|
const nav = browser.serpNavigation ?? {};
|
|
628
1065
|
const geo = [network.ip, network.city, network.region].filter(Boolean).join(" / ") || "geo unknown";
|
|
629
|
-
const sessionId = attempt.browser_session_id ?? attempt.kernel_session_id ?? kernel.sessionId ?? "unknown";
|
|
1066
|
+
const sessionId = attempt.browser_session_id ?? attempt.browserSessionIdSuffix ?? attempt.kernel_session_id ?? kernel.sessionId ?? "unknown";
|
|
630
1067
|
const cleanupSucceeded = attempt.session_cleanup_succeeded ?? attempt.kernel_delete_succeeded;
|
|
631
|
-
|
|
1068
|
+
const proxySource = proxyResolution.source ?? attempt.proxyResolutionSource;
|
|
1069
|
+
return `- Attempt ${attempt.attempt_number ?? attempt.attemptNumber ?? "?"}: ${attempt.outcome ?? attempt.status ?? "unknown"} \xB7 session ${sessionId} \xB7 proxy ${debug.request?.proxyMode ?? kernel.proxyMode ?? attempt.proxyMode ?? "unknown"}${proxySource ? `/${proxySource}` : ""} \xB7 ${geo} \xB7 CAPTCHA ${nav.captchaDetected === true ? "yes" : nav.captchaDetected === false ? "no" : "unknown"} \xB7 cleanup ${cleanupSucceeded === true ? "yes" : cleanupSucceeded === false ? "no" : "unknown"}`;
|
|
632
1070
|
});
|
|
633
1071
|
return `
|
|
634
1072
|
|
|
@@ -1089,6 +1527,29 @@ ${rows}`,
|
|
|
1089
1527
|
}
|
|
1090
1528
|
};
|
|
1091
1529
|
}
|
|
1530
|
+
function normalizeMapsAttempts(value) {
|
|
1531
|
+
const attempts = Array.isArray(value) ? value : [];
|
|
1532
|
+
return attempts.map((attempt, index) => ({
|
|
1533
|
+
attemptNumber: attempt.attemptNumber ?? attempt.attempt_number ?? index + 1,
|
|
1534
|
+
maxAttempts: attempt.maxAttempts ?? attempt.max_attempts ?? attempts.length,
|
|
1535
|
+
status: attempt.status === "ok" ? "ok" : "failed",
|
|
1536
|
+
outcome: attempt.outcome ?? attempt.status ?? "unknown",
|
|
1537
|
+
willRetry: attempt.willRetry ?? attempt.will_retry ?? false,
|
|
1538
|
+
durationMs: attempt.durationMs ?? attempt.duration_ms ?? 0,
|
|
1539
|
+
resultCount: attempt.resultCount ?? attempt.result_count ?? 0,
|
|
1540
|
+
error: attempt.error ? sanitizeVendorText(attempt.error) : null,
|
|
1541
|
+
proxyMode: attempt.proxyMode ?? attempt.proxy_mode ?? "location",
|
|
1542
|
+
proxyResolutionSource: attempt.proxyResolutionSource ?? attempt.proxy_resolution_source ?? null,
|
|
1543
|
+
proxyIdSuffix: attempt.proxyIdSuffix ?? attempt.proxy_id_suffix ?? null,
|
|
1544
|
+
proxyTargetLevel: attempt.proxyTargetLevel ?? attempt.proxy_target_level ?? null,
|
|
1545
|
+
proxyTargetLocation: attempt.proxyTargetLocation ?? attempt.proxy_target_location ?? null,
|
|
1546
|
+
proxyTargetZip: attempt.proxyTargetZip ?? attempt.proxy_target_zip ?? null,
|
|
1547
|
+
browserSessionIdSuffix: attempt.browserSessionIdSuffix ?? attempt.browser_session_id ?? null,
|
|
1548
|
+
observedIp: attempt.observedIp ?? attempt.observed_ip ?? null,
|
|
1549
|
+
observedCity: attempt.observedCity ?? attempt.observed_city ?? null,
|
|
1550
|
+
observedRegion: attempt.observedRegion ?? attempt.observed_region ?? null
|
|
1551
|
+
}));
|
|
1552
|
+
}
|
|
1092
1553
|
function formatCreditsInfo(raw, input) {
|
|
1093
1554
|
const parsed = parseData(raw);
|
|
1094
1555
|
if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
|
|
@@ -1161,6 +1622,8 @@ function formatMapsSearch(raw, input) {
|
|
|
1161
1622
|
const searchQuery = d.searchQuery ?? [input.query, input.location].filter(Boolean).join(" ");
|
|
1162
1623
|
const requestedMax = d.requestedMaxResults ?? input.maxResults ?? 10;
|
|
1163
1624
|
const durationMs = d.durationMs;
|
|
1625
|
+
const attempts = normalizeMapsAttempts(d.attempts);
|
|
1626
|
+
const lastAttempt = attempts.at(-1);
|
|
1164
1627
|
const rows = results.map((r) => {
|
|
1165
1628
|
const rating = [r.rating, r.reviewCount ? `(${r.reviewCount})` : null].filter(Boolean).join(" ");
|
|
1166
1629
|
return `| ${r.position} | ${cell(r.name)} | ${cell(r.category)} | ${cell(rating)} | ${cell(r.address)} | ${r.cidDecimal ? `\`${r.cidDecimal}\`` : "\u2014"} | ${r.websiteUrl ? `[site](${r.websiteUrl})` : "\u2014"} | [maps](${r.placeUrl}) |`;
|
|
@@ -1175,6 +1638,7 @@ ${meta}`;
|
|
|
1175
1638
|
const full = [
|
|
1176
1639
|
`# Google Maps Search: "${searchQuery}"`,
|
|
1177
1640
|
`**Returned:** ${results.length} profile candidate${results.length === 1 ? "" : "s"} \xB7 **Requested max:** ${requestedMax} \xB7 **Limit:** 50`,
|
|
1641
|
+
attempts.length ? `**Attempts:** ${attempts.length}/${lastAttempt?.maxAttempts ?? attempts.length} \xB7 **Proxy:** ${lastAttempt?.proxyMode ?? "unknown"}${lastAttempt?.proxyResolutionSource ? `/${lastAttempt.proxyResolutionSource}` : ""} \xB7 **Observed:** ${[lastAttempt?.observedCity, lastAttempt?.observedRegion].filter(Boolean).join(", ") || "unknown"}` : null,
|
|
1178
1642
|
`
|
|
1179
1643
|
## Results
|
|
1180
1644
|
| # | Name | Category | Rating | Address | CID | Website | Maps |
|
|
@@ -1198,6 +1662,7 @@ ${rows}`,
|
|
|
1198
1662
|
requestedMaxResults: requestedMax,
|
|
1199
1663
|
resultCount: results.length,
|
|
1200
1664
|
results: normalizedResults,
|
|
1665
|
+
attempts,
|
|
1201
1666
|
durationMs: durationMs ?? 0
|
|
1202
1667
|
}
|
|
1203
1668
|
};
|
|
@@ -1208,6 +1673,7 @@ function formatDirectoryWorkflow(raw, input) {
|
|
|
1208
1673
|
const d = parsed.data;
|
|
1209
1674
|
const cities = (d.cities ?? []).map((city) => ({
|
|
1210
1675
|
...city,
|
|
1676
|
+
attempts: normalizeMapsAttempts(city.attempts),
|
|
1211
1677
|
results: city.results.map((result) => ({
|
|
1212
1678
|
...result,
|
|
1213
1679
|
phone: result.phone ?? null,
|
|
@@ -1435,7 +1901,7 @@ var HarvestPaaInputSchema = {
|
|
|
1435
1901
|
gl: import_zod2.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."),
|
|
1436
1902
|
hl: import_zod2.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."),
|
|
1437
1903
|
device: import_zod2.z.enum(["desktop", "mobile"]).default("desktop").describe("SERP device context. Use desktop by default; use mobile only when the user asks for mobile rankings."),
|
|
1438
|
-
proxyMode: import_zod2.z.enum(["location", "configured", "none"]).default("location").describe("Proxy targeting mode. Use location by default for US city/state SERPs; it creates a fresh residential proxy ID per attempt
|
|
1904
|
+
proxyMode: import_zod2.z.enum(["location", "configured", "none"]).default("location").describe("Proxy targeting mode. Use location by default for US city/state SERPs; it creates a fresh residential proxy ID per attempt. If Google shows a CAPTCHA/challenge, browser-service sessions briefly wait for the automatic solver first, then retry with a fresh proxy/session if the challenge does not clear. Also retries proxy tunnel failure and wrong-location evidence before returning. Use configured only for the static configured proxy. Use none only for direct-network debugging."),
|
|
1439
1905
|
proxyZip: import_zod2.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 when city-center targeting needs to be forced. With proxyMode location this ZIP is used for each fresh proxy attempt."),
|
|
1440
1906
|
debug: import_zod2.z.boolean().default(false).describe("Include sanitized browser/session/location diagnostics in the response. Use true when debugging localization, CAPTCHA, or proxy behavior.")
|
|
1441
1907
|
};
|
|
@@ -1494,7 +1960,7 @@ var MapsSearchInputSchema = {
|
|
|
1494
1960
|
gl: import_zod2.z.string().length(2).default("us").describe("Google country code inferred from location."),
|
|
1495
1961
|
hl: import_zod2.z.string().length(2).default("en").describe("Language inferred from user request."),
|
|
1496
1962
|
maxResults: import_zod2.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."),
|
|
1497
|
-
proxyMode: import_zod2.z.enum(["location", "configured", "none"]).default("location").describe("Proxy targeting mode. Use location by default for US city/state Maps searches;
|
|
1963
|
+
proxyMode: import_zod2.z.enum(["location", "configured", "none"]).default("location").describe("Proxy targeting mode. Use location by default for US city/state Maps searches; retryable failures create a new residential proxy ID and new browser session for up to 5 attempts. Use configured for the server proxy ID, and none only for local direct-network debugging."),
|
|
1498
1964
|
proxyZip: import_zod2.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."),
|
|
1499
1965
|
debug: import_zod2.z.boolean().default(false).describe("Include sanitized browser/proxy diagnostics when debugging Maps localization, CAPTCHA, or proxy behavior.")
|
|
1500
1966
|
};
|
|
@@ -1509,7 +1975,7 @@ var DirectoryWorkflowInputSchema = {
|
|
|
1509
1975
|
includeZipGroups: import_zod2.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."),
|
|
1510
1976
|
usZipsCsvPath: import_zod2.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."),
|
|
1511
1977
|
saveCsv: import_zod2.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, category, address, phone, hours_status, website_url, directions_url, place_url, CID fields, population, and ZIP groups."),
|
|
1512
|
-
proxyMode: import_zod2.z.enum(["location", "configured", "none"]).default("location").describe("Proxy targeting mode for every city Maps search. Use location by default for US city/state batches;
|
|
1978
|
+
proxyMode: import_zod2.z.enum(["location", "configured", "none"]).default("location").describe("Proxy targeting mode for every city Maps search. Use location by default for US city/state batches; retryable failures create a new residential proxy ID and new browser session for up to 5 attempts per city. Use configured for the server proxy ID, and none only for local direct-network debugging."),
|
|
1513
1979
|
proxyZip: import_zod2.z.string().regex(/^\d{5}$/).optional().describe("Optional ZIP override for proxy targeting. Normally omit it so each city can use its Lead Magician ZIP group or city/state location."),
|
|
1514
1980
|
debug: import_zod2.z.boolean().default(false).describe("Include sanitized browser/proxy diagnostics in each Maps browser session when supported.")
|
|
1515
1981
|
};
|
|
@@ -1532,6 +1998,26 @@ var RankTrackerBlueprintInputSchema = {
|
|
|
1532
1998
|
notes: import_zod2.z.string().max(4e3).optional().describe("Extra product, client, stack, or hosting requirements to include in the implementation prompt.")
|
|
1533
1999
|
};
|
|
1534
2000
|
var NullableString = import_zod2.z.string().nullable();
|
|
2001
|
+
var MapsSearchAttemptOutput = import_zod2.z.object({
|
|
2002
|
+
attemptNumber: import_zod2.z.number().int().min(1),
|
|
2003
|
+
maxAttempts: import_zod2.z.number().int().min(1),
|
|
2004
|
+
status: import_zod2.z.enum(["ok", "failed"]),
|
|
2005
|
+
outcome: import_zod2.z.string(),
|
|
2006
|
+
willRetry: import_zod2.z.boolean(),
|
|
2007
|
+
durationMs: import_zod2.z.number().int().min(0),
|
|
2008
|
+
resultCount: import_zod2.z.number().int().min(0),
|
|
2009
|
+
error: NullableString,
|
|
2010
|
+
proxyMode: import_zod2.z.enum(["location", "configured", "none"]),
|
|
2011
|
+
proxyResolutionSource: import_zod2.z.enum(["disabled", "location_reused", "location_created", "configured_fallback", "unavailable"]).nullable(),
|
|
2012
|
+
proxyIdSuffix: NullableString,
|
|
2013
|
+
proxyTargetLevel: import_zod2.z.enum(["zip", "city", "state"]).nullable(),
|
|
2014
|
+
proxyTargetLocation: NullableString,
|
|
2015
|
+
proxyTargetZip: NullableString,
|
|
2016
|
+
browserSessionIdSuffix: NullableString,
|
|
2017
|
+
observedIp: NullableString,
|
|
2018
|
+
observedCity: NullableString,
|
|
2019
|
+
observedRegion: NullableString
|
|
2020
|
+
});
|
|
1535
2021
|
var MapsSearchOutputSchema = {
|
|
1536
2022
|
query: import_zod2.z.string(),
|
|
1537
2023
|
location: import_zod2.z.string().nullable(),
|
|
@@ -1556,6 +2042,7 @@ var MapsSearchOutputSchema = {
|
|
|
1556
2042
|
directionsUrl: NullableString,
|
|
1557
2043
|
metadata: import_zod2.z.array(import_zod2.z.string())
|
|
1558
2044
|
})),
|
|
2045
|
+
attempts: import_zod2.z.array(MapsSearchAttemptOutput),
|
|
1559
2046
|
durationMs: import_zod2.z.number().int().min(0)
|
|
1560
2047
|
};
|
|
1561
2048
|
var DirectoryMapsBusinessOutput = import_zod2.z.object({
|
|
@@ -1602,6 +2089,7 @@ var DirectoryWorkflowOutputSchema = {
|
|
|
1602
2089
|
error: NullableString,
|
|
1603
2090
|
resultCount: import_zod2.z.number().int().min(0),
|
|
1604
2091
|
durationMs: import_zod2.z.number().int().min(0),
|
|
2092
|
+
attempts: import_zod2.z.array(MapsSearchAttemptOutput),
|
|
1605
2093
|
results: import_zod2.z.array(DirectoryMapsBusinessOutput)
|
|
1606
2094
|
})),
|
|
1607
2095
|
durationMs: import_zod2.z.number().int().min(0)
|
|
@@ -1818,7 +2306,7 @@ var SearchSerpInputSchema = {
|
|
|
1818
2306
|
gl: import_zod2.z.string().length(2).default("us").describe("Google country code inferred from location or user language."),
|
|
1819
2307
|
hl: import_zod2.z.string().default("en").describe("Google interface/content language inferred from user request."),
|
|
1820
2308
|
device: import_zod2.z.enum(["desktop", "mobile"]).default("desktop").describe("SERP device context. Use desktop by default; use mobile only when the user asks for mobile rankings."),
|
|
1821
|
-
proxyMode: import_zod2.z.enum(["location", "configured", "none"]).default("location").describe("Proxy targeting mode. Use location by default for US city/state SERPs; it creates a fresh residential proxy ID per attempt
|
|
2309
|
+
proxyMode: import_zod2.z.enum(["location", "configured", "none"]).default("location").describe("Proxy targeting mode. Use location by default for US city/state SERPs; it creates a fresh residential proxy ID per attempt. If Google shows a CAPTCHA/challenge, browser-service sessions briefly wait for the automatic solver first, then retry with a fresh proxy/session if the challenge does not clear. Also retries proxy tunnel failure and wrong-location evidence before returning. Use configured only for the static configured proxy. Use none only for direct-network debugging."),
|
|
1822
2310
|
proxyZip: import_zod2.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 when city-center targeting needs to be forced. With proxyMode location this ZIP is used for each fresh proxy attempt."),
|
|
1823
2311
|
debug: import_zod2.z.boolean().default(false).describe("Include sanitized browser/session/location diagnostics in the response. Use true when debugging localization, CAPTCHA, or proxy behavior."),
|
|
1824
2312
|
pages: import_zod2.z.number().int().min(1).max(2).default(1).describe("Number of result pages to fetch (1\u20132)")
|
|
@@ -1829,7 +2317,7 @@ var CaptureSerpSnapshotInputSchema = {
|
|
|
1829
2317
|
gl: import_zod2.z.string().length(2).default("us").describe("Google country code inferred from the requested market, e.g. us, gb, ca, au."),
|
|
1830
2318
|
hl: import_zod2.z.string().default("en").describe("Google interface/content language inferred from the user request."),
|
|
1831
2319
|
device: import_zod2.z.enum(["desktop", "mobile"]).default("desktop").describe("SERP device context. Use mobile only when the user asks for mobile rankings or mobile SERP evidence."),
|
|
1832
|
-
proxyMode: import_zod2.z.enum(["location", "configured", "none"]).default("location").describe("Proxy behavior for capture. Use location for localized US residential evidence; it creates a fresh proxy ID per attempt
|
|
2320
|
+
proxyMode: import_zod2.z.enum(["location", "configured", "none"]).default("location").describe("Proxy behavior for capture. Use location for localized US residential evidence; it creates a fresh proxy ID per attempt. If Google shows a CAPTCHA/challenge, browser-service sessions briefly wait for the automatic solver first, then retry with a fresh proxy/session if the challenge does not clear. Also retries proxy tunnel failure and wrong-location evidence before returning. Use configured only for the static residential proxy, and none only for direct-network debugging."),
|
|
1833
2321
|
proxyZip: import_zod2.z.string().regex(/^\d{5}$/).optional().describe("Optional US ZIP override for residential location proxy targeting when a precise city-center or ZIP proxy is needed. With proxyMode location this ZIP is used for each fresh proxy attempt."),
|
|
1834
2322
|
pages: import_zod2.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."),
|
|
1835
2323
|
debug: import_zod2.z.boolean().default(false).describe("Include sanitized browser, proxy, and location diagnostics. Use true when debugging localization, CAPTCHA, proxy selection, or capture reliability."),
|
|
@@ -2198,7 +2686,7 @@ function localPlanningToolAnnotations(title) {
|
|
|
2198
2686
|
function listSavedReports() {
|
|
2199
2687
|
try {
|
|
2200
2688
|
const dir = outputBaseDir2();
|
|
2201
|
-
return (0, import_node_fs3.readdirSync)(dir).filter((f) => f.endsWith(".md")).map((f) => ({ filename: f, mtimeMs: (0, import_node_fs3.statSync)((0,
|
|
2689
|
+
return (0, import_node_fs3.readdirSync)(dir).filter((f) => f.endsWith(".md")).map((f) => ({ filename: f, mtimeMs: (0, import_node_fs3.statSync)((0, import_node_path4.join)(dir, f)).mtimeMs })).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, 100);
|
|
2202
2690
|
} catch {
|
|
2203
2691
|
return [];
|
|
2204
2692
|
}
|
|
@@ -2222,9 +2710,9 @@ function registerSavedReportResources(server2) {
|
|
|
2222
2710
|
},
|
|
2223
2711
|
async (uri, variables) => {
|
|
2224
2712
|
const requested = Array.isArray(variables.filename) ? variables.filename[0] : variables.filename;
|
|
2225
|
-
const filename = (0,
|
|
2713
|
+
const filename = (0, import_node_path4.basename)(decodeURIComponent(String(requested ?? "")));
|
|
2226
2714
|
if (!filename.endsWith(".md")) throw new Error("Only saved .md reports can be read");
|
|
2227
|
-
const text = (0, import_node_fs3.readFileSync)((0,
|
|
2715
|
+
const text = (0, import_node_fs3.readFileSync)((0, import_node_path4.join)(outputBaseDir2(), filename), "utf8");
|
|
2228
2716
|
return { contents: [{ uri: uri.href, mimeType: "text/markdown", text }] };
|
|
2229
2717
|
}
|
|
2230
2718
|
);
|
|
@@ -2236,14 +2724,14 @@ function registerPaaExtractorMcpTools(server2, executor, options = {}) {
|
|
|
2236
2724
|
if (savesReports) registerSavedReportResources(server2);
|
|
2237
2725
|
server2.registerTool("harvest_paa", {
|
|
2238
2726
|
title: "Google PAA + SERP Harvest",
|
|
2239
|
-
description: withReportNote('Best default tool for Google search research. Extracts People Also Ask questions plus answers/source URLs, organic SERP, local pack when present, entity IDs (CID/GCID/KG MID), and AI Overview. Infer the user language: split topic from location (e.g. "best hvac company in Denver CO" => query "best hvac company", location "Denver, CO", gl "us", hl "en"). For US local SERPs, leave proxyMode as location so the service uses fresh residential proxy IDs across retries
|
|
2727
|
+
description: withReportNote('Best default tool for Google search research. Extracts People Also Ask questions plus answers/source URLs, organic SERP, local pack when present, entity IDs (CID/GCID/KG MID), and AI Overview. Infer the user language: split topic from location (e.g. "best hvac company in Denver CO" => query "best hvac company", location "Denver, CO", gl "us", hl "en"). For US local SERPs, leave proxyMode as location so the service uses fresh residential proxy IDs across retries, briefly waits for browser-service automatic CAPTCHA/challenge solving when Google blocks a page, and rotates to a new proxy/session if the challenge does not clear or the market evidence is wrong. Use maxQuestions 30 normally, 100-200 for "full", "deep", "all", or comprehensive research. Deep harvests above 100 questions can run for several minutes with no interim progress \u2014 warn the user before starting one and keep maxQuestions at or below 100 unless they explicitly want a deep harvest. Credits are charged by extracted question; unused request hold is refunded.'),
|
|
2240
2728
|
inputSchema: HarvestPaaInputSchema,
|
|
2241
2729
|
outputSchema: HarvestPaaOutputSchema,
|
|
2242
2730
|
annotations: liveWebToolAnnotations("Google PAA + SERP Harvest")
|
|
2243
2731
|
}, async (input) => formatHarvestPaa(await executor.harvestPaa(input), input));
|
|
2244
2732
|
server2.registerTool("search_serp", {
|
|
2245
2733
|
title: "Google SERP Lookup",
|
|
2246
|
-
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. For US city/state rankings, keep proxyMode as location and pass proxyZip when a city-center ZIP is known; location mode uses fresh residential proxy IDs
|
|
2734
|
+
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. For US city/state rankings, keep proxyMode as location and pass proxyZip when a city-center ZIP is known; location mode uses fresh residential proxy IDs, briefly waits for browser-service automatic CAPTCHA/challenge solving when Google blocks a page, and rotates to a new proxy/session if the challenge does not clear, the proxy tunnel fails, or location evidence is wrong."),
|
|
2247
2735
|
inputSchema: SearchSerpInputSchema,
|
|
2248
2736
|
outputSchema: SearchSerpOutputSchema,
|
|
2249
2737
|
annotations: liveWebToolAnnotations("Google SERP Lookup")
|
|
@@ -2311,14 +2799,14 @@ function registerPaaExtractorMcpTools(server2, executor, options = {}) {
|
|
|
2311
2799
|
}, async (input) => formatMapsPlaceIntel(await executor.mapsPlaceIntel(input), input));
|
|
2312
2800
|
server2.registerTool("maps_search", {
|
|
2313
2801
|
title: "Google Maps Business Search",
|
|
2314
|
-
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." For US city/state Maps searches, keep proxyMode as location so the browser service
|
|
2802
|
+
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." For US city/state Maps searches, keep proxyMode as location so the browser service creates a fresh residential proxy ID and browser session for the market; retryable failures rotate to a new proxy and new browser session for up to 5 attempts. Pass proxyZip only when a specific ZIP or city-center ZIP is known. 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.'),
|
|
2315
2803
|
inputSchema: MapsSearchInputSchema,
|
|
2316
2804
|
outputSchema: MapsSearchOutputSchema,
|
|
2317
2805
|
annotations: liveWebToolAnnotations("Google Maps Business Search")
|
|
2318
2806
|
}, async (input) => formatMapsSearch(await executor.mapsSearch(input), input));
|
|
2319
2807
|
server2.registerTool("directory_workflow", {
|
|
2320
2808
|
title: "Directory Workflow: Markets + Maps",
|
|
2321
|
-
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. Use concurrency up to 5 for parallel city sessions. Keep proxyMode as location so each city
|
|
2809
|
+
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. Use concurrency up to 5 for parallel city sessions. Keep proxyMode as location so each city search creates fresh residential proxy IDs and browser sessions; retryable city failures rotate to a new proxy and new browser session for up to 5 attempts. Saved CSV rows include source_location, result_position, business_name, review_stars, 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. This workflow captures star ratings from Maps list cards, not profile review counts; use maps_place_intel only when a selected profile needs deeper review details. For local Lead Magician ZIP enrichment, set MCP_SCRAPER_USZIPS_CSV_PATH on the API server or pass usZipsCsvPath only in local/test mode.'),
|
|
2322
2810
|
inputSchema: DirectoryWorkflowInputSchema,
|
|
2323
2811
|
outputSchema: DirectoryWorkflowOutputSchema,
|
|
2324
2812
|
annotations: liveWebToolAnnotations("Directory Workflow: Markets + Maps")
|
|
@@ -2348,7 +2836,7 @@ function registerPaaExtractorMcpTools(server2, executor, options = {}) {
|
|
|
2348
2836
|
// bin/mcp-scraper-combined-stdio-server.ts
|
|
2349
2837
|
function readApiKeyFile() {
|
|
2350
2838
|
const explicitPath = process.env.MCP_SCRAPER_KEY_PATH?.trim();
|
|
2351
|
-
const paths = [explicitPath, (0,
|
|
2839
|
+
const paths = [explicitPath, (0, import_node_path5.join)((0, import_node_os4.homedir)(), ".mcp-scraper-key")].filter(Boolean);
|
|
2352
2840
|
for (const path of paths) {
|
|
2353
2841
|
try {
|
|
2354
2842
|
const value = (0, import_node_fs4.readFileSync)(path, "utf8").trim();
|