mcp-scraper 0.2.19 → 0.2.21
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 +36 -5
- package/dist/bin/api-server.cjs +1072 -349
- package/dist/bin/api-server.cjs.map +1 -1
- package/dist/bin/api-server.js +3 -3
- package/dist/bin/browser-agent-stdio-server.cjs +1274 -75
- package/dist/bin/browser-agent-stdio-server.cjs.map +1 -1
- package/dist/bin/browser-agent-stdio-server.js +4 -2
- package/dist/bin/browser-agent-stdio-server.js.map +1 -1
- package/dist/bin/mcp-scraper-cli.cjs +659 -210
- package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
- package/dist/bin/mcp-scraper-cli.js +108 -9
- package/dist/bin/mcp-scraper-cli.js.map +1 -1
- package/dist/bin/mcp-scraper-combined-stdio-server.cjs +2104 -566
- package/dist/bin/mcp-scraper-combined-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-scraper-combined-stdio-server.js +7 -4
- package/dist/bin/mcp-scraper-combined-stdio-server.js.map +1 -1
- package/dist/bin/mcp-scraper-install.cjs +3 -3
- package/dist/bin/mcp-scraper-install.cjs.map +1 -1
- package/dist/bin/mcp-scraper-install.js +2 -2
- package/dist/bin/mcp-stdio-server.cjs +726 -387
- package/dist/bin/mcp-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-stdio-server.js +3 -2
- package/dist/bin/mcp-stdio-server.js.map +1 -1
- package/dist/bin/paa-harvest.cjs +5 -3
- package/dist/bin/paa-harvest.cjs.map +1 -1
- package/dist/bin/paa-harvest.js +5 -2
- package/dist/bin/paa-harvest.js.map +1 -1
- package/dist/{chunk-TL7YTFLH.js → chunk-DBQDG7EH.js} +99 -27
- package/dist/chunk-DBQDG7EH.js.map +1 -0
- package/dist/chunk-DUEW4EOO.js +214 -0
- package/dist/chunk-DUEW4EOO.js.map +1 -0
- package/dist/{chunk-F44RBOJ5.js → chunk-H2R232HK.js} +216 -108
- package/dist/chunk-H2R232HK.js.map +1 -0
- package/dist/chunk-HS6OKELW.js +1786 -0
- package/dist/chunk-HS6OKELW.js.map +1 -0
- package/dist/{chunk-CQTAKXBN.js → chunk-IPW4LFOT.js} +11 -116
- package/dist/chunk-IPW4LFOT.js.map +1 -0
- package/dist/chunk-LFATOGDF.js +111 -0
- package/dist/chunk-LFATOGDF.js.map +1 -0
- package/dist/{chunk-MCBOZ2JF.js → chunk-MLNCKQ2B.js} +3 -3
- package/dist/chunk-MLNCKQ2B.js.map +1 -0
- package/dist/{chunk-4UEVYYDW.js → chunk-O5J7E4VX.js} +394 -57
- package/dist/chunk-O5J7E4VX.js.map +1 -0
- package/dist/{chunk-BSYPATSM.js → chunk-SXTXMFEQ.js} +2 -2
- package/dist/chunk-WN7PBKMV.js +28 -0
- package/dist/chunk-WN7PBKMV.js.map +1 -0
- package/dist/chunk-XKUDVN2E.js +7 -0
- package/dist/chunk-XKUDVN2E.js.map +1 -0
- package/dist/{db-P5X6UQ3E.js → db-BE4JVB3V.js} +8 -2
- package/dist/index.cjs +5 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/{server-CRUSZA2Q.js → server-24XIQ6W6.js} +248 -40
- package/dist/server-24XIQ6W6.js.map +1 -0
- package/dist/{worker-OZSWIS3F.js → worker-SLQ375UG.js} +8 -5
- package/dist/{worker-OZSWIS3F.js.map → worker-SLQ375UG.js.map} +1 -1
- package/docs/adr/0001-in-page-graphql-interception-for-anti-bot-scraping.md +2 -2
- package/docs/mcp-tool-craft-lint.generated.md +51 -0
- package/docs/mcp-tool-manifest.generated.json +1534 -0
- package/docs/specs/local-competitive-audit-spec.md +1 -2
- package/docs/specs/mcp-tool-definition-quality-audit-spec.md +1602 -0
- package/package.json +4 -1
- package/dist/chunk-4UEVYYDW.js.map +0 -1
- package/dist/chunk-76P4DKWR.js +0 -793
- package/dist/chunk-76P4DKWR.js.map +0 -1
- package/dist/chunk-CQTAKXBN.js.map +0 -1
- package/dist/chunk-F44RBOJ5.js.map +0 -1
- package/dist/chunk-MCBOZ2JF.js.map +0 -1
- package/dist/chunk-RU2HZD55.js +0 -7
- package/dist/chunk-RU2HZD55.js.map +0 -1
- package/dist/chunk-TL7YTFLH.js.map +0 -1
- package/dist/server-CRUSZA2Q.js.map +0 -1
- /package/dist/{chunk-BSYPATSM.js.map → chunk-SXTXMFEQ.js.map} +0 -0
- /package/dist/{db-P5X6UQ3E.js.map → db-BE4JVB3V.js.map} +0 -0
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
// bin/mcp-scraper-combined-stdio-server.ts
|
|
5
|
-
var
|
|
6
|
-
var
|
|
7
|
-
var
|
|
5
|
+
var import_node_fs6 = require("fs");
|
|
6
|
+
var import_node_os6 = require("os");
|
|
7
|
+
var import_node_path8 = 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
|
|
|
@@ -23,6 +23,27 @@ function harvestTimeoutBudget(maxQuestions, serpOnly = false) {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
// src/mcp/http-mcp-tool-executor.ts
|
|
26
|
+
function youtubeVideoIdFromUrl(url) {
|
|
27
|
+
if (!url) return null;
|
|
28
|
+
try {
|
|
29
|
+
const parsed = new URL(url);
|
|
30
|
+
const host = parsed.hostname.replace(/^www\./, "").replace(/^m\./, "");
|
|
31
|
+
if (host === "youtu.be") {
|
|
32
|
+
const id = parsed.pathname.split("/").filter(Boolean)[0];
|
|
33
|
+
return id || null;
|
|
34
|
+
}
|
|
35
|
+
if (host === "youtube.com" || host === "music.youtube.com") {
|
|
36
|
+
const watchId = parsed.searchParams.get("v");
|
|
37
|
+
if (watchId) return watchId;
|
|
38
|
+
const parts = parsed.pathname.split("/").filter(Boolean);
|
|
39
|
+
const markerIndex = parts.findIndex((part) => ["shorts", "embed", "live"].includes(part));
|
|
40
|
+
if (markerIndex >= 0 && parts[markerIndex + 1]) return parts[markerIndex + 1];
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
26
47
|
var HttpMcpToolExecutor = class {
|
|
27
48
|
baseUrl;
|
|
28
49
|
apiKey;
|
|
@@ -148,7 +169,21 @@ var HttpMcpToolExecutor = class {
|
|
|
148
169
|
return this.call("/youtube/harvest", input);
|
|
149
170
|
}
|
|
150
171
|
youtubeTranscribe(input) {
|
|
151
|
-
|
|
172
|
+
const videoId = input.videoId?.trim() || youtubeVideoIdFromUrl(input.url);
|
|
173
|
+
if (!videoId) {
|
|
174
|
+
return Promise.resolve({
|
|
175
|
+
content: [{
|
|
176
|
+
type: "text",
|
|
177
|
+
text: JSON.stringify({
|
|
178
|
+
error_code: "youtube_video_id_required",
|
|
179
|
+
error: "Pass videoId from youtube_harvest or a YouTube url that contains a video id.",
|
|
180
|
+
retryable: false
|
|
181
|
+
})
|
|
182
|
+
}],
|
|
183
|
+
isError: true
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return this.call("/youtube/transcribe", { videoId });
|
|
152
187
|
}
|
|
153
188
|
facebookPageIntel(input) {
|
|
154
189
|
return this.call("/facebook/page-intel", input);
|
|
@@ -185,6 +220,10 @@ var HttpMcpToolExecutor = class {
|
|
|
185
220
|
webhookUrl: input.webhookUrl
|
|
186
221
|
}, Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 9e5);
|
|
187
222
|
}
|
|
223
|
+
workflowStep(input) {
|
|
224
|
+
const timeoutMs = this.httpTimeoutOverrideMs ?? Number(process.env.MCP_SCRAPER_WORKFLOW_TIMEOUT_MS ?? 9e5);
|
|
225
|
+
return this.call(`/workflows/runs/${encodeURIComponent(input.runId)}/step`, {}, Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 9e5);
|
|
226
|
+
}
|
|
188
227
|
workflowStatus(input) {
|
|
189
228
|
return this.getJson(`/workflows/runs/${encodeURIComponent(input.runId)}`);
|
|
190
229
|
}
|
|
@@ -207,23 +246,635 @@ var HttpMcpToolExecutor = class {
|
|
|
207
246
|
|
|
208
247
|
// src/mcp/browser-agent-mcp-server.ts
|
|
209
248
|
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
249
|
+
var import_node_fs3 = require("fs");
|
|
250
|
+
var import_node_os4 = require("os");
|
|
251
|
+
var import_node_path5 = require("path");
|
|
252
|
+
|
|
253
|
+
// src/lib/chrome-profiles.ts
|
|
254
|
+
var import_promises = require("fs/promises");
|
|
255
|
+
var import_node_os = require("os");
|
|
256
|
+
var import_node_path = require("path");
|
|
257
|
+
function defaultChromeUserDataDir() {
|
|
258
|
+
if (process.platform === "darwin") return (0, import_node_path.join)((0, import_node_os.homedir)(), "Library", "Application Support", "Google", "Chrome");
|
|
259
|
+
if (process.platform === "win32") {
|
|
260
|
+
return (0, import_node_path.join)(process.env.LOCALAPPDATA ?? (0, import_node_path.join)((0, import_node_os.homedir)(), "AppData", "Local"), "Google", "Chrome", "User Data");
|
|
261
|
+
}
|
|
262
|
+
return (0, import_node_path.join)(process.env.XDG_CONFIG_HOME ?? (0, import_node_path.join)((0, import_node_os.homedir)(), ".config"), "google-chrome");
|
|
263
|
+
}
|
|
264
|
+
function recommendedKernelProfileName(value) {
|
|
265
|
+
return value.trim().toLowerCase().replace(/@/g, "-").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "browser-profile";
|
|
266
|
+
}
|
|
267
|
+
async function listLocalChromeProfiles(userDataDir = defaultChromeUserDataDir(), emailFilter) {
|
|
268
|
+
const raw = await (0, import_promises.readFile)((0, import_node_path.join)(userDataDir, "Local State"), "utf8");
|
|
269
|
+
const localState = JSON.parse(raw);
|
|
270
|
+
const wanted = emailFilter?.trim().toLowerCase();
|
|
271
|
+
return Object.entries(localState.profile?.info_cache ?? {}).map(([directory, info]) => {
|
|
272
|
+
const email = (info.user_name ?? "").trim();
|
|
273
|
+
const displayName = (info.name ?? info.shortcut_name ?? "").trim();
|
|
274
|
+
const labelSource = email || displayName || directory;
|
|
275
|
+
return {
|
|
276
|
+
email,
|
|
277
|
+
displayName,
|
|
278
|
+
chromeProfileDirectory: directory,
|
|
279
|
+
chromeProfilePath: (0, import_node_path.join)(userDataDir, directory),
|
|
280
|
+
recommendedKernelProfileName: recommendedKernelProfileName(labelSource)
|
|
281
|
+
};
|
|
282
|
+
}).filter((row) => !wanted || row.email.toLowerCase() === wanted || row.displayName.toLowerCase().includes(wanted));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// src/lib/browser-service-env.ts
|
|
286
|
+
function browserServiceProfileName() {
|
|
287
|
+
const value = (process.env.BROWSER_AGENT_PROFILE_NAME ?? process.env.BROWSER_SERVICE_PROFILE_NAME ?? process.env.KERNEL_BROWSER_PROFILE_NAME ?? process.env.KERNEL_PROFILE_NAME)?.trim();
|
|
288
|
+
return value || void 0;
|
|
289
|
+
}
|
|
290
|
+
function browserServiceProfileSaveChanges() {
|
|
291
|
+
const value = (process.env.BROWSER_AGENT_PROFILE_SAVE_CHANGES ?? process.env.BROWSER_SERVICE_PROFILE_SAVE_CHANGES ?? process.env.KERNEL_BROWSER_PROFILE_SAVE_CHANGES ?? process.env.KERNEL_PROFILE_SAVE_CHANGES)?.trim().toLowerCase();
|
|
292
|
+
if (!value) return void 0;
|
|
293
|
+
if (["1", "true", "yes", "on"].includes(value)) return true;
|
|
294
|
+
if (["0", "false", "no", "off"].includes(value)) return false;
|
|
295
|
+
return void 0;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// src/lib/local-browser-profiles.ts
|
|
210
299
|
var import_node_fs = require("fs");
|
|
300
|
+
var import_promises2 = require("fs/promises");
|
|
211
301
|
var import_node_os2 = require("os");
|
|
212
302
|
var import_node_path2 = require("path");
|
|
303
|
+
var EXCLUDED_PROFILE_NAMES = [
|
|
304
|
+
"Cache",
|
|
305
|
+
"Code Cache",
|
|
306
|
+
"Crashpad",
|
|
307
|
+
"DawnCache",
|
|
308
|
+
"DawnGraphiteCache",
|
|
309
|
+
"DawnWebGPUCache",
|
|
310
|
+
"GPUCache",
|
|
311
|
+
"GrShaderCache",
|
|
312
|
+
"ShaderCache",
|
|
313
|
+
"Shared Dictionary"
|
|
314
|
+
];
|
|
315
|
+
function defaultImportedBrowserProfilesDir() {
|
|
316
|
+
return process.env.MCP_SCRAPER_BROWSER_PROFILE_BASE_DIR?.trim() || (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".mcp-scraper", "browser-profiles");
|
|
317
|
+
}
|
|
318
|
+
function defaultChromeExecutablePath() {
|
|
319
|
+
const envPath = process.env.MCP_SCRAPER_BROWSER_EXECUTABLE?.trim() || process.env.CHROME_PATH?.trim();
|
|
320
|
+
if (envPath) return envPath;
|
|
321
|
+
const candidates = process.platform === "darwin" ? ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"] : process.platform === "win32" ? [
|
|
322
|
+
(0, import_node_path2.join)(process.env.PROGRAMFILES ?? "C:\\Program Files", "Google", "Chrome", "Application", "chrome.exe"),
|
|
323
|
+
(0, import_node_path2.join)(process.env["PROGRAMFILES(X86)"] ?? "C:\\Program Files (x86)", "Google", "Chrome", "Application", "chrome.exe")
|
|
324
|
+
] : ["/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/usr/bin/chromium-browser", "/usr/bin/chromium"];
|
|
325
|
+
return candidates.find((candidate) => (0, import_node_fs.existsSync)(candidate));
|
|
326
|
+
}
|
|
327
|
+
function localBrowserModeEnabled() {
|
|
328
|
+
return process.env.MCP_SCRAPER_BROWSER_MODE?.trim().toLowerCase() === "local";
|
|
329
|
+
}
|
|
330
|
+
function importedProfileName(value) {
|
|
331
|
+
return recommendedKernelProfileName(value);
|
|
332
|
+
}
|
|
333
|
+
function importedProfileRoot(name, outputDir = defaultImportedBrowserProfilesDir()) {
|
|
334
|
+
return (0, import_node_path2.join)(outputDir, importedProfileName(name));
|
|
335
|
+
}
|
|
336
|
+
function importedProfileManifestPath(name, outputDir = defaultImportedBrowserProfilesDir()) {
|
|
337
|
+
return (0, import_node_path2.join)(importedProfileRoot(name, outputDir), "manifest.json");
|
|
338
|
+
}
|
|
339
|
+
function importedProfileUserDataDir(name, outputDir = defaultImportedBrowserProfilesDir()) {
|
|
340
|
+
return (0, import_node_path2.join)(importedProfileRoot(name, outputDir), "user-data");
|
|
341
|
+
}
|
|
342
|
+
async function readExistingManifest(path) {
|
|
343
|
+
try {
|
|
344
|
+
return JSON.parse(await (0, import_promises2.readFile)(path, "utf8"));
|
|
345
|
+
} catch {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function isExcludedProfilePath(sourceRoot, sourcePath) {
|
|
350
|
+
const rel = (0, import_node_path2.relative)(sourceRoot, sourcePath);
|
|
351
|
+
if (!rel) return false;
|
|
352
|
+
const parts = rel.split(import_node_path2.sep).filter(Boolean);
|
|
353
|
+
const last = parts[parts.length - 1] ?? "";
|
|
354
|
+
if (last.startsWith("Singleton")) return true;
|
|
355
|
+
if (last === "LOCK" || last.endsWith(".lock")) return true;
|
|
356
|
+
if (parts.some((part) => EXCLUDED_PROFILE_NAMES.includes(part))) return true;
|
|
357
|
+
if (parts.includes("Service Worker") && parts.includes("CacheStorage")) return true;
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
async function copyProfileTree(sourceProfilePath, destinationProfilePath) {
|
|
361
|
+
await (0, import_promises2.cp)(sourceProfilePath, destinationProfilePath, {
|
|
362
|
+
recursive: true,
|
|
363
|
+
force: true,
|
|
364
|
+
errorOnExist: false,
|
|
365
|
+
dereference: false,
|
|
366
|
+
filter: (source) => !isExcludedProfilePath(sourceProfilePath, source)
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
function selectProfile(profiles, options) {
|
|
370
|
+
const requestedDirectory = options.chromeProfileDirectory?.trim();
|
|
371
|
+
const requestedEmail = options.email?.trim().toLowerCase();
|
|
372
|
+
const exactDirectory = requestedDirectory ? profiles.find((profile) => profile.chromeProfileDirectory === requestedDirectory) : void 0;
|
|
373
|
+
if (exactDirectory) return exactDirectory;
|
|
374
|
+
const exactEmail = requestedEmail ? profiles.find((profile) => profile.email.toLowerCase() === requestedEmail) : void 0;
|
|
375
|
+
if (exactEmail) return exactEmail;
|
|
376
|
+
const first = profiles[0];
|
|
377
|
+
if (first) return first;
|
|
378
|
+
const hint = requestedDirectory ? `Chrome profile directory "${requestedDirectory}"` : requestedEmail ? `Chrome account "${requestedEmail}"` : "Chrome profile";
|
|
379
|
+
throw new Error(`${hint} was not found in ${options.sourceUserDataDir ?? defaultChromeUserDataDir()}`);
|
|
380
|
+
}
|
|
381
|
+
async function ensureProfilePathExists(profile) {
|
|
382
|
+
const details = await (0, import_promises2.stat)(profile.chromeProfilePath).catch(() => null);
|
|
383
|
+
if (!details?.isDirectory()) {
|
|
384
|
+
throw new Error(`Chrome profile path does not exist or is not a directory: ${profile.chromeProfilePath}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
async function writeNormalizedLocalState(sourceLocalStatePath, destinationLocalStatePath, sourceProfileDirectory) {
|
|
388
|
+
const raw = await (0, import_promises2.readFile)(sourceLocalStatePath, "utf8");
|
|
389
|
+
const localState = JSON.parse(raw);
|
|
390
|
+
const selectedInfo = localState.profile?.info_cache?.[sourceProfileDirectory] ?? localState.profile?.info_cache?.Default ?? {};
|
|
391
|
+
localState.profile = {
|
|
392
|
+
...localState.profile ?? {},
|
|
393
|
+
info_cache: { Default: selectedInfo },
|
|
394
|
+
last_used: "Default",
|
|
395
|
+
last_active_profiles: ["Default"]
|
|
396
|
+
};
|
|
397
|
+
await (0, import_promises2.writeFile)(destinationLocalStatePath, `${JSON.stringify(localState, null, 2)}
|
|
398
|
+
`);
|
|
399
|
+
}
|
|
400
|
+
async function importChromeProfile(options = {}) {
|
|
401
|
+
const sourceUserDataDir = options.sourceUserDataDir?.trim() || defaultChromeUserDataDir();
|
|
402
|
+
const profiles = await listLocalChromeProfiles(sourceUserDataDir, options.email);
|
|
403
|
+
const sourceProfile = selectProfile(profiles, { ...options, sourceUserDataDir });
|
|
404
|
+
await ensureProfilePathExists(sourceProfile);
|
|
405
|
+
const name = importedProfileName(options.name?.trim() || sourceProfile.recommendedKernelProfileName);
|
|
406
|
+
const outputDir = options.outputDir?.trim() || defaultImportedBrowserProfilesDir();
|
|
407
|
+
const profileRoot = importedProfileRoot(name, outputDir);
|
|
408
|
+
const manifestPath = importedProfileManifestPath(name, outputDir);
|
|
409
|
+
const userDataDir = importedProfileUserDataDir(name, outputDir);
|
|
410
|
+
const existingManifest = await readExistingManifest(manifestPath);
|
|
411
|
+
const existing = (0, import_node_fs.existsSync)(profileRoot);
|
|
412
|
+
if (existing && !options.overwrite) {
|
|
413
|
+
throw new Error(`Imported browser profile "${name}" already exists at ${profileRoot}. Re-run with --overwrite to refresh it.`);
|
|
414
|
+
}
|
|
415
|
+
if (existing) await (0, import_promises2.rm)(profileRoot, { recursive: true, force: true });
|
|
416
|
+
await (0, import_promises2.mkdir)((0, import_node_path2.join)(userDataDir, "Default"), { recursive: true });
|
|
417
|
+
await writeNormalizedLocalState(
|
|
418
|
+
(0, import_node_path2.join)(sourceUserDataDir, "Local State"),
|
|
419
|
+
(0, import_node_path2.join)(userDataDir, "Local State"),
|
|
420
|
+
sourceProfile.chromeProfileDirectory
|
|
421
|
+
);
|
|
422
|
+
await copyProfileTree(sourceProfile.chromeProfilePath, (0, import_node_path2.join)(userDataDir, "Default"));
|
|
423
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
424
|
+
const manifest = {
|
|
425
|
+
layoutVersion: 1,
|
|
426
|
+
name,
|
|
427
|
+
profileRoot,
|
|
428
|
+
userDataDir,
|
|
429
|
+
browserExecutablePath: options.browserExecutablePath?.trim() || defaultChromeExecutablePath() || null,
|
|
430
|
+
sourceUserDataDir,
|
|
431
|
+
sourceProfileDirectory: sourceProfile.chromeProfileDirectory,
|
|
432
|
+
sourceProfilePath: sourceProfile.chromeProfilePath,
|
|
433
|
+
email: sourceProfile.email,
|
|
434
|
+
displayName: sourceProfile.displayName,
|
|
435
|
+
createdAt: existingManifest?.createdAt ?? now,
|
|
436
|
+
updatedAt: now,
|
|
437
|
+
copyMode: "chrome-profile-clone"
|
|
438
|
+
};
|
|
439
|
+
await (0, import_promises2.writeFile)(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
440
|
+
`);
|
|
441
|
+
return {
|
|
442
|
+
profile: manifest,
|
|
443
|
+
sourceProfile,
|
|
444
|
+
excludedNames: EXCLUDED_PROFILE_NAMES
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
async function loadImportedBrowserProfile(name = process.env.MCP_SCRAPER_BROWSER_PROFILE?.trim() || "default", outputDir = defaultImportedBrowserProfilesDir()) {
|
|
448
|
+
const profileName = importedProfileName(name);
|
|
449
|
+
const manifestPath = importedProfileManifestPath(profileName, outputDir);
|
|
450
|
+
const manifest = await readExistingManifest(manifestPath);
|
|
451
|
+
if (!manifest) {
|
|
452
|
+
throw new Error(
|
|
453
|
+
`Imported browser profile "${profileName}" was not found. Run: mcp-scraper-cli browser import-chrome --name ${profileName}`
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
return manifest;
|
|
457
|
+
}
|
|
458
|
+
async function syncImportedChromeProfile(name, options = {}) {
|
|
459
|
+
const outputDir = options.outputDir?.trim() || defaultImportedBrowserProfilesDir();
|
|
460
|
+
const manifest = await loadImportedBrowserProfile(name, outputDir);
|
|
461
|
+
return importChromeProfile({
|
|
462
|
+
name: manifest.name,
|
|
463
|
+
sourceUserDataDir: manifest.sourceUserDataDir,
|
|
464
|
+
chromeProfileDirectory: manifest.sourceProfileDirectory,
|
|
465
|
+
outputDir,
|
|
466
|
+
overwrite: true,
|
|
467
|
+
browserExecutablePath: options.browserExecutablePath ?? manifest.browserExecutablePath ?? void 0
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// src/services/browser-agent/local-browser-agent-service.ts
|
|
472
|
+
var import_node_crypto = require("crypto");
|
|
473
|
+
var import_node_fs2 = require("fs");
|
|
474
|
+
var import_node_path3 = require("path");
|
|
475
|
+
var import_playwright = require("playwright");
|
|
476
|
+
function directProfileDir() {
|
|
477
|
+
return process.env.MCP_SCRAPER_BROWSER_PROFILE_DIR?.trim() || void 0;
|
|
478
|
+
}
|
|
479
|
+
function configuredProfileName(profile) {
|
|
480
|
+
return profile?.trim() || process.env.MCP_SCRAPER_BROWSER_PROFILE?.trim() || (directProfileDir() ? (0, import_node_path3.basename)(directProfileDir()) : "default");
|
|
481
|
+
}
|
|
482
|
+
function launchExecutablePath(profile) {
|
|
483
|
+
const configured = process.env.MCP_SCRAPER_BROWSER_EXECUTABLE?.trim() || profile.browserExecutablePath || defaultChromeExecutablePath();
|
|
484
|
+
return configured && (0, import_node_fs2.existsSync)(configured) ? configured : void 0;
|
|
485
|
+
}
|
|
486
|
+
function ignoreDefaultArgsFor(executablePath) {
|
|
487
|
+
if (!executablePath) return void 0;
|
|
488
|
+
if (process.platform !== "darwin") return void 0;
|
|
489
|
+
if (!/Google Chrome\.app\/Contents\/MacOS\/Google Chrome$/.test(executablePath)) return void 0;
|
|
490
|
+
return ["--password-store=basic", "--use-mock-keychain"];
|
|
491
|
+
}
|
|
492
|
+
function normalizeKeyCombo(value) {
|
|
493
|
+
return value.split("+").map((part) => {
|
|
494
|
+
const trimmed = part.trim();
|
|
495
|
+
const lower = trimmed.toLowerCase();
|
|
496
|
+
if (lower === "ctrl" || lower === "control") return "Control";
|
|
497
|
+
if (lower === "cmd" || lower === "command" || lower === "meta") return "Meta";
|
|
498
|
+
if (lower === "option" || lower === "alt") return "Alt";
|
|
499
|
+
if (lower === "return") return "Enter";
|
|
500
|
+
if (lower === "esc") return "Escape";
|
|
501
|
+
if (trimmed.length === 1) return trimmed.toUpperCase();
|
|
502
|
+
return trimmed;
|
|
503
|
+
}).join("+");
|
|
504
|
+
}
|
|
505
|
+
function sessionSummary(session) {
|
|
506
|
+
return {
|
|
507
|
+
session_id: session.sessionId,
|
|
508
|
+
status: session.status,
|
|
509
|
+
label: session.label,
|
|
510
|
+
profile: session.profileName,
|
|
511
|
+
user_data_dir: session.userDataDir,
|
|
512
|
+
created_at: session.createdAt,
|
|
513
|
+
last_action_at: session.lastActionAt,
|
|
514
|
+
closed_at: session.closedAt,
|
|
515
|
+
watch_url: `local://browser-session/${session.sessionId}`,
|
|
516
|
+
url: session.status === "open" ? session.page.url() : null
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
var LocalBrowserAgentService = class {
|
|
520
|
+
contexts = /* @__PURE__ */ new Map();
|
|
521
|
+
sessions = /* @__PURE__ */ new Map();
|
|
522
|
+
async open(input) {
|
|
523
|
+
const profile = await this.resolveProfile(input.profile);
|
|
524
|
+
const contextState = await this.ensureContext(profile);
|
|
525
|
+
const pages = contextState.context.pages();
|
|
526
|
+
const page = pages.length === 1 && !this.hasOpenSessionForContext(contextState.key) ? pages[0] : await contextState.context.newPage();
|
|
527
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
528
|
+
const sessionId = `lbs_${(0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 20)}`;
|
|
529
|
+
const session = {
|
|
530
|
+
sessionId,
|
|
531
|
+
label: input.label ?? null,
|
|
532
|
+
page,
|
|
533
|
+
contextKey: contextState.key,
|
|
534
|
+
profileName: profile.name,
|
|
535
|
+
userDataDir: profile.userDataDir,
|
|
536
|
+
createdAt: now,
|
|
537
|
+
lastActionAt: now,
|
|
538
|
+
closedAt: null,
|
|
539
|
+
status: "open"
|
|
540
|
+
};
|
|
541
|
+
const timeoutSeconds = input.timeout_seconds ?? 600;
|
|
542
|
+
if (timeoutSeconds > 0) {
|
|
543
|
+
session.timeout = setTimeout(() => {
|
|
544
|
+
void this.close(sessionId);
|
|
545
|
+
}, timeoutSeconds * 1e3);
|
|
546
|
+
session.timeout.unref();
|
|
547
|
+
}
|
|
548
|
+
this.sessions.set(sessionId, session);
|
|
549
|
+
if (input.url) await this.goto(sessionId, input.url);
|
|
550
|
+
return {
|
|
551
|
+
session_id: sessionId,
|
|
552
|
+
watch_url: `local://browser-session/${sessionId}`,
|
|
553
|
+
live_view_url: null,
|
|
554
|
+
profile: profile.name,
|
|
555
|
+
user_data_dir: profile.userDataDir,
|
|
556
|
+
url: input.url ?? null,
|
|
557
|
+
mode: "local"
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
async screenshot(sessionId) {
|
|
561
|
+
const session = this.requireOpenSession(sessionId);
|
|
562
|
+
await this.waitForPage(session.page);
|
|
563
|
+
const image = await session.page.screenshot({ type: "png", fullPage: false });
|
|
564
|
+
const page = await this.read(sessionId);
|
|
565
|
+
return {
|
|
566
|
+
...page,
|
|
567
|
+
image_base64: image.toString("base64"),
|
|
568
|
+
mime_type: "image/png"
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
async read(sessionId) {
|
|
572
|
+
const session = this.requireOpenSession(sessionId);
|
|
573
|
+
await this.waitForPage(session.page);
|
|
574
|
+
this.touch(session);
|
|
575
|
+
const title = await session.page.title().catch(() => "");
|
|
576
|
+
const snapshot = await session.page.evaluate(() => {
|
|
577
|
+
const cssEscape = (value) => {
|
|
578
|
+
const escapeFn = globalThis.CSS?.escape;
|
|
579
|
+
return escapeFn ? escapeFn(value) : value.replace(/["\\#.:>+~[\]()]/g, "\\$&");
|
|
580
|
+
};
|
|
581
|
+
const textFor = (element) => {
|
|
582
|
+
const html = element;
|
|
583
|
+
return (element.getAttribute("aria-label") || element.getAttribute("title") || html.placeholder || html.value || html.innerText || element.textContent || "").replace(/\s+/g, " ").trim();
|
|
584
|
+
};
|
|
585
|
+
const selectorFor = (element) => {
|
|
586
|
+
const id = element.id;
|
|
587
|
+
if (id) return `#${cssEscape(id)}`;
|
|
588
|
+
const name = element.getAttribute("name");
|
|
589
|
+
const tag = element.tagName.toLowerCase();
|
|
590
|
+
if (name) return `${tag}[name="${name.replace(/"/g, '\\"')}"]`;
|
|
591
|
+
const parts = [];
|
|
592
|
+
let current = element;
|
|
593
|
+
while (current && current !== document.body && parts.length < 4) {
|
|
594
|
+
const currentTag = current.tagName.toLowerCase();
|
|
595
|
+
const parent = current.parentElement;
|
|
596
|
+
if (!parent) {
|
|
597
|
+
parts.unshift(currentTag);
|
|
598
|
+
break;
|
|
599
|
+
}
|
|
600
|
+
const children = Array.from(parent.children);
|
|
601
|
+
const siblings = children.filter((child) => child.tagName === current.tagName);
|
|
602
|
+
const index = siblings.indexOf(current) + 1;
|
|
603
|
+
parts.unshift(siblings.length > 1 ? `${currentTag}:nth-of-type(${index})` : currentTag);
|
|
604
|
+
current = parent;
|
|
605
|
+
}
|
|
606
|
+
return parts.join(" > ") || tag;
|
|
607
|
+
};
|
|
608
|
+
const candidates = Array.from(document.querySelectorAll(
|
|
609
|
+
'a, button, input, textarea, select, summary, [role], [contenteditable="true"], [tabindex]:not([tabindex="-1"])'
|
|
610
|
+
));
|
|
611
|
+
const elements = candidates.map((element, index) => {
|
|
612
|
+
const rect = element.getBoundingClientRect();
|
|
613
|
+
const style = window.getComputedStyle(element);
|
|
614
|
+
const visible = rect.width > 0 && rect.height > 0 && style.display !== "none" && style.visibility !== "hidden" && Number(style.opacity || "1") > 0;
|
|
615
|
+
if (!visible) return null;
|
|
616
|
+
const label = textFor(element);
|
|
617
|
+
return {
|
|
618
|
+
index,
|
|
619
|
+
tag: element.tagName.toLowerCase(),
|
|
620
|
+
role: element.getAttribute("role") || null,
|
|
621
|
+
text: label.slice(0, 180),
|
|
622
|
+
selector: selectorFor(element),
|
|
623
|
+
x: Math.round(rect.left + rect.width / 2),
|
|
624
|
+
y: Math.round(rect.top + rect.height / 2),
|
|
625
|
+
left: Math.round(rect.left),
|
|
626
|
+
top: Math.round(rect.top),
|
|
627
|
+
width: Math.round(rect.width),
|
|
628
|
+
height: Math.round(rect.height)
|
|
629
|
+
};
|
|
630
|
+
}).filter(Boolean).slice(0, 120);
|
|
631
|
+
return {
|
|
632
|
+
text: (document.body?.innerText || "").replace(/\n{3,}/g, "\n\n").slice(0, 3e4),
|
|
633
|
+
elements,
|
|
634
|
+
viewport: {
|
|
635
|
+
width: window.innerWidth,
|
|
636
|
+
height: window.innerHeight,
|
|
637
|
+
devicePixelRatio: window.devicePixelRatio,
|
|
638
|
+
scrollX: window.scrollX,
|
|
639
|
+
scrollY: window.scrollY
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
});
|
|
643
|
+
return {
|
|
644
|
+
url: session.page.url(),
|
|
645
|
+
title,
|
|
646
|
+
text: snapshot.text,
|
|
647
|
+
elements: snapshot.elements,
|
|
648
|
+
viewport: snapshot.viewport
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
async locate(sessionId, targets) {
|
|
652
|
+
const session = this.requireOpenSession(sessionId);
|
|
653
|
+
this.touch(session);
|
|
654
|
+
const located = [];
|
|
655
|
+
for (const target of targets) {
|
|
656
|
+
try {
|
|
657
|
+
const locator = target.selector ? session.page.locator(target.selector) : session.page.getByText(target.text ?? "", { exact: target.match === "exact" });
|
|
658
|
+
const selected = locator.nth(target.index ?? 0);
|
|
659
|
+
await selected.waitFor({ state: "visible", timeout: 3e3 }).catch(() => void 0);
|
|
660
|
+
const box = await selected.boundingBox();
|
|
661
|
+
const text = await selected.textContent({ timeout: 1e3 }).catch(() => null);
|
|
662
|
+
located.push({
|
|
663
|
+
input: target,
|
|
664
|
+
found: Boolean(box),
|
|
665
|
+
element: box ? {
|
|
666
|
+
left: Math.round(box.x),
|
|
667
|
+
top: Math.round(box.y),
|
|
668
|
+
width: Math.round(box.width),
|
|
669
|
+
height: Math.round(box.height),
|
|
670
|
+
x: Math.round(box.x + box.width / 2),
|
|
671
|
+
y: Math.round(box.y + box.height / 2),
|
|
672
|
+
text: (text ?? "").replace(/\s+/g, " ").trim().slice(0, 220)
|
|
673
|
+
} : null
|
|
674
|
+
});
|
|
675
|
+
} catch (err) {
|
|
676
|
+
located.push({
|
|
677
|
+
input: target,
|
|
678
|
+
found: false,
|
|
679
|
+
element: null,
|
|
680
|
+
error: err instanceof Error ? err.message : String(err)
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
const read = await this.read(sessionId);
|
|
685
|
+
return {
|
|
686
|
+
url: read.url,
|
|
687
|
+
title: read.title,
|
|
688
|
+
viewport: read.viewport,
|
|
689
|
+
replay: null,
|
|
690
|
+
targets: located
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
async goto(sessionId, url) {
|
|
694
|
+
const session = this.requireOpenSession(sessionId);
|
|
695
|
+
this.touch(session);
|
|
696
|
+
const response = await session.page.goto(url, { waitUntil: "domcontentloaded", timeout: 45e3 }).catch((err) => {
|
|
697
|
+
throw new Error(err instanceof Error ? err.message : String(err));
|
|
698
|
+
});
|
|
699
|
+
await this.waitForPage(session.page);
|
|
700
|
+
return {
|
|
701
|
+
ok: true,
|
|
702
|
+
url: session.page.url(),
|
|
703
|
+
status: response?.status() ?? null,
|
|
704
|
+
mode: "local"
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
async click(sessionId, input) {
|
|
708
|
+
const session = this.requireOpenSession(sessionId);
|
|
709
|
+
this.touch(session);
|
|
710
|
+
await session.page.mouse.click(input.x, input.y, {
|
|
711
|
+
button: input.button ?? "left",
|
|
712
|
+
clickCount: input.num_clicks ?? 1
|
|
713
|
+
});
|
|
714
|
+
await this.waitForPage(session.page);
|
|
715
|
+
return { ok: true, mode: "local", url: session.page.url() };
|
|
716
|
+
}
|
|
717
|
+
async type(sessionId, input) {
|
|
718
|
+
const session = this.requireOpenSession(sessionId);
|
|
719
|
+
this.touch(session);
|
|
720
|
+
await session.page.keyboard.type(input.text, { delay: input.delay });
|
|
721
|
+
return { ok: true, mode: "local", url: session.page.url() };
|
|
722
|
+
}
|
|
723
|
+
async scroll(sessionId, input) {
|
|
724
|
+
const session = this.requireOpenSession(sessionId);
|
|
725
|
+
this.touch(session);
|
|
726
|
+
if (typeof input.x === "number" && typeof input.y === "number") {
|
|
727
|
+
await session.page.mouse.move(input.x, input.y);
|
|
728
|
+
}
|
|
729
|
+
await session.page.mouse.wheel((input.delta_x ?? 0) * 120, (input.delta_y ?? 5) * 120);
|
|
730
|
+
await session.page.waitForTimeout(150).catch(() => void 0);
|
|
731
|
+
return { ok: true, mode: "local", url: session.page.url() };
|
|
732
|
+
}
|
|
733
|
+
async press(sessionId, input) {
|
|
734
|
+
const session = this.requireOpenSession(sessionId);
|
|
735
|
+
this.touch(session);
|
|
736
|
+
for (const key of input.keys) {
|
|
737
|
+
await session.page.keyboard.press(normalizeKeyCombo(key));
|
|
738
|
+
}
|
|
739
|
+
await this.waitForPage(session.page);
|
|
740
|
+
return { ok: true, mode: "local", url: session.page.url() };
|
|
741
|
+
}
|
|
742
|
+
async close(sessionId) {
|
|
743
|
+
const session = this.sessions.get(sessionId);
|
|
744
|
+
if (!session || session.status === "closed") {
|
|
745
|
+
return { ok: true, closed: true, already_closed: true, mode: "local" };
|
|
746
|
+
}
|
|
747
|
+
if (session.timeout) clearTimeout(session.timeout);
|
|
748
|
+
session.status = "closed";
|
|
749
|
+
session.closedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
750
|
+
await session.page.close().catch(() => void 0);
|
|
751
|
+
await this.closeContextIfUnused(session.contextKey);
|
|
752
|
+
return { ok: true, closed: true, mode: "local" };
|
|
753
|
+
}
|
|
754
|
+
list(includeClosed = false) {
|
|
755
|
+
return Array.from(this.sessions.values()).filter((session) => includeClosed || session.status === "open").map(sessionSummary);
|
|
756
|
+
}
|
|
757
|
+
async resolveProfile(profile) {
|
|
758
|
+
const directDir = directProfileDir();
|
|
759
|
+
const requestedName = configuredProfileName(profile);
|
|
760
|
+
if (directDir && (!profile || requestedName === process.env.MCP_SCRAPER_BROWSER_PROFILE?.trim())) {
|
|
761
|
+
return {
|
|
762
|
+
name: requestedName,
|
|
763
|
+
userDataDir: directDir,
|
|
764
|
+
browserExecutablePath: process.env.MCP_SCRAPER_BROWSER_EXECUTABLE?.trim() || defaultChromeExecutablePath() || null,
|
|
765
|
+
manifest: null
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
const manifest = await loadImportedBrowserProfile(requestedName);
|
|
769
|
+
return {
|
|
770
|
+
name: manifest.name,
|
|
771
|
+
userDataDir: manifest.userDataDir,
|
|
772
|
+
browserExecutablePath: manifest.browserExecutablePath,
|
|
773
|
+
manifest
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
async ensureContext(profile) {
|
|
777
|
+
const existing = this.contexts.get(profile.userDataDir);
|
|
778
|
+
if (existing) return existing;
|
|
779
|
+
const executablePath = launchExecutablePath(profile);
|
|
780
|
+
const context = await import_playwright.chromium.launchPersistentContext(profile.userDataDir, {
|
|
781
|
+
headless: false,
|
|
782
|
+
viewport: { width: 1440, height: 900 },
|
|
783
|
+
acceptDownloads: true,
|
|
784
|
+
...executablePath ? { executablePath } : {},
|
|
785
|
+
...ignoreDefaultArgsFor(executablePath) ? { ignoreDefaultArgs: ignoreDefaultArgsFor(executablePath) } : {}
|
|
786
|
+
});
|
|
787
|
+
const state = { key: profile.userDataDir, context, profile };
|
|
788
|
+
context.on("close", () => {
|
|
789
|
+
this.contexts.delete(profile.userDataDir);
|
|
790
|
+
const closedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
791
|
+
for (const session of this.sessions.values()) {
|
|
792
|
+
if (session.contextKey === profile.userDataDir && session.status === "open") {
|
|
793
|
+
session.status = "closed";
|
|
794
|
+
session.closedAt = closedAt;
|
|
795
|
+
if (session.timeout) clearTimeout(session.timeout);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
this.contexts.set(profile.userDataDir, state);
|
|
800
|
+
return state;
|
|
801
|
+
}
|
|
802
|
+
requireOpenSession(sessionId) {
|
|
803
|
+
const session = this.sessions.get(sessionId);
|
|
804
|
+
if (!session || session.status !== "open") throw new Error(`Local browser session not found or closed: ${sessionId}`);
|
|
805
|
+
return session;
|
|
806
|
+
}
|
|
807
|
+
touch(session) {
|
|
808
|
+
session.lastActionAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
809
|
+
}
|
|
810
|
+
hasOpenSessionForContext(contextKey) {
|
|
811
|
+
return Array.from(this.sessions.values()).some((session) => session.contextKey === contextKey && session.status === "open");
|
|
812
|
+
}
|
|
813
|
+
async closeContextIfUnused(contextKey) {
|
|
814
|
+
if (this.hasOpenSessionForContext(contextKey)) return;
|
|
815
|
+
const state = this.contexts.get(contextKey);
|
|
816
|
+
if (!state) return;
|
|
817
|
+
this.contexts.delete(contextKey);
|
|
818
|
+
await state.context.close().catch(() => void 0);
|
|
819
|
+
}
|
|
820
|
+
async waitForPage(page) {
|
|
821
|
+
await page.waitForLoadState("domcontentloaded", { timeout: 5e3 }).catch(() => void 0);
|
|
822
|
+
await page.waitForTimeout(200).catch(() => void 0);
|
|
823
|
+
}
|
|
824
|
+
};
|
|
213
825
|
|
|
214
826
|
// src/version.ts
|
|
215
|
-
var PACKAGE_VERSION = "0.2.
|
|
827
|
+
var PACKAGE_VERSION = "0.2.20";
|
|
216
828
|
|
|
217
829
|
// src/mcp/browser-agent-tool-schemas.ts
|
|
218
830
|
var import_zod = require("zod");
|
|
831
|
+
var NullableString = import_zod.z.string().nullable();
|
|
832
|
+
var BrowserRawObject = import_zod.z.record(import_zod.z.unknown());
|
|
833
|
+
var BrowserElementOutput = import_zod.z.record(import_zod.z.unknown());
|
|
834
|
+
var BrowserBaseOutput = {
|
|
835
|
+
ok: import_zod.z.boolean().describe("Whether the browser-agent action succeeded."),
|
|
836
|
+
tool: import_zod.z.string().describe("Browser Agent MCP tool that produced this response."),
|
|
837
|
+
session_id: NullableString.describe("Browser session id when the response is scoped to a session.")
|
|
838
|
+
};
|
|
839
|
+
var BrowserReplayBaseOutput = {
|
|
840
|
+
...BrowserBaseOutput,
|
|
841
|
+
replay_id: NullableString.describe("Replay id when the response is scoped to a replay.")
|
|
842
|
+
};
|
|
219
843
|
var BrowserOpenInputSchema = {
|
|
220
844
|
label: import_zod.z.string().optional().describe("Optional human label for this session, shown in the watch console."),
|
|
221
845
|
url: import_zod.z.string().url().optional().describe("Optional URL to navigate to immediately after opening."),
|
|
222
846
|
profile: import_zod.z.string().optional().describe("Optional saved profile name to load a logged-in session for a site."),
|
|
847
|
+
save_profile_changes: import_zod.z.boolean().optional().describe("Persist cookies and local storage back to the named profile when the session is closed. Use this for profile setup or intentional auth refreshes; avoid parallel sessions writing to the same profile."),
|
|
223
848
|
timeout_seconds: import_zod.z.number().int().min(60).max(259200).optional().describe("How long the session may live before auto-termination. Defaults to 600. The browser idles into a zero-cost standby between actions, so a longer timeout is cheap.")
|
|
224
849
|
};
|
|
850
|
+
var BrowserProfileListInputSchema = {
|
|
851
|
+
email: import_zod.z.string().optional().describe("Optional Chrome account email or displayed profile name to filter for, for example seo@example.com."),
|
|
852
|
+
user_data_dir: import_zod.z.string().optional().describe("Optional Chrome user data directory. Defaults to the current OS Chrome profile root.")
|
|
853
|
+
};
|
|
854
|
+
var BrowserProfileOnboardInputSchema = {
|
|
855
|
+
email: import_zod.z.string().optional().describe("Optional local Chrome account email to find and convert into a suggested Kernel profile name."),
|
|
856
|
+
profile: import_zod.z.string().optional().describe("Optional Kernel browser profile name to create/load. If omitted, email is used to derive one."),
|
|
857
|
+
user_data_dir: import_zod.z.string().optional().describe("Optional Chrome user data directory for email lookup. Defaults to the current OS Chrome profile root."),
|
|
858
|
+
url: import_zod.z.string().url().optional().describe("Setup URL to open after creating the browser. Defaults to https://accounts.google.com/."),
|
|
859
|
+
label: import_zod.z.string().optional().describe("Optional human label for this setup session."),
|
|
860
|
+
timeout_seconds: import_zod.z.number().int().min(60).max(259200).optional().describe("How long the setup session may live before auto-termination. Defaults to 600.")
|
|
861
|
+
};
|
|
862
|
+
var BrowserProfileImportInputSchema = {
|
|
863
|
+
email: import_zod.z.string().optional().describe("Optional Chrome account email or displayed profile name to select from Chrome Local State."),
|
|
864
|
+
name: import_zod.z.string().optional().describe("Name for the managed MCP Scraper browser profile. Defaults to a slug derived from the selected Chrome account."),
|
|
865
|
+
chrome_profile_directory: import_zod.z.string().optional().describe("Optional Chrome profile directory to import, for example Default or Profile 1."),
|
|
866
|
+
user_data_dir: import_zod.z.string().optional().describe("Optional source Chrome user data directory. Defaults to the current OS Chrome profile root."),
|
|
867
|
+
output_dir: import_zod.z.string().optional().describe("Optional managed profile base directory. Defaults to ~/.mcp-scraper/browser-profiles."),
|
|
868
|
+
browser_executable_path: import_zod.z.string().optional().describe("Optional Chrome executable path for local browser launches. Defaults to the installed Google Chrome path when found."),
|
|
869
|
+
overwrite: import_zod.z.boolean().default(false).describe("Overwrite an existing managed profile clone with the same name.")
|
|
870
|
+
};
|
|
871
|
+
var BrowserProfileSyncInputSchema = {
|
|
872
|
+
name: import_zod.z.string().describe("Managed MCP Scraper browser profile name to refresh from its recorded source Chrome profile."),
|
|
873
|
+
output_dir: import_zod.z.string().optional().describe("Optional managed profile base directory. Defaults to ~/.mcp-scraper/browser-profiles."),
|
|
874
|
+
browser_executable_path: import_zod.z.string().optional().describe("Optional Chrome executable path to store in the refreshed profile manifest.")
|
|
875
|
+
};
|
|
225
876
|
var BrowserSessionInputSchema = {
|
|
226
|
-
session_id: import_zod.z.string().describe("The session id returned by browser_open.")
|
|
877
|
+
session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself.")
|
|
227
878
|
};
|
|
228
879
|
var BrowserLocateTargetSchema = import_zod.z.object({
|
|
229
880
|
name: import_zod.z.string().optional().describe("Optional label for this target, echoed in the result."),
|
|
@@ -235,43 +886,43 @@ var BrowserLocateTargetSchema = import_zod.z.object({
|
|
|
235
886
|
message: "target requires selector or text"
|
|
236
887
|
});
|
|
237
888
|
var BrowserLocateInputSchema = {
|
|
238
|
-
session_id: import_zod.z.string().describe("The session id returned by browser_open."),
|
|
889
|
+
session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
239
890
|
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.")
|
|
240
891
|
};
|
|
241
892
|
var BrowserGotoInputSchema = {
|
|
242
|
-
session_id: import_zod.z.string().describe("The session id returned by browser_open."),
|
|
893
|
+
session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
243
894
|
url: import_zod.z.string().url().describe("URL to navigate the browser to.")
|
|
244
895
|
};
|
|
245
896
|
var BrowserClickInputSchema = {
|
|
246
|
-
session_id: import_zod.z.string().describe("The session id returned by browser_open."),
|
|
247
|
-
x: import_zod.z.number().describe("X coordinate to click, in screenshot pixels. Use the
|
|
248
|
-
y: import_zod.z.number().describe("Y coordinate to click, in screenshot pixels."),
|
|
897
|
+
session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
898
|
+
x: import_zod.z.number().describe("X coordinate to click, in screenshot pixels. Use only coordinates from the latest browser_screenshot, browser_read, or browser_locate result; do not guess."),
|
|
899
|
+
y: import_zod.z.number().describe("Y coordinate to click, in screenshot pixels. Use only coordinates from the latest browser_screenshot, browser_read, or browser_locate result; do not guess."),
|
|
249
900
|
button: import_zod.z.enum(["left", "right", "middle"]).default("left").describe("Mouse button."),
|
|
250
901
|
num_clicks: import_zod.z.number().int().min(1).max(3).optional().describe("Number of clicks, e.g. 2 for double-click.")
|
|
251
902
|
};
|
|
252
903
|
var BrowserTypeInputSchema = {
|
|
253
|
-
session_id: import_zod.z.string().describe("The session id returned by browser_open."),
|
|
904
|
+
session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
254
905
|
text: import_zod.z.string().describe("Text to type at the current focus. Click a field first to focus it."),
|
|
255
906
|
delay: import_zod.z.number().int().min(0).max(500).optional().describe("Optional per-keystroke delay in ms for human-like typing.")
|
|
256
907
|
};
|
|
257
908
|
var BrowserScrollInputSchema = {
|
|
258
|
-
session_id: import_zod.z.string().describe("The session id returned by browser_open."),
|
|
909
|
+
session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
259
910
|
delta_y: import_zod.z.number().default(5).describe("Vertical scroll in wheel units. Positive scrolls down, negative up."),
|
|
260
911
|
delta_x: import_zod.z.number().default(0).describe("Horizontal scroll in wheel units."),
|
|
261
912
|
x: import_zod.z.number().optional().describe("X position to scroll at. Defaults to screen center."),
|
|
262
913
|
y: import_zod.z.number().optional().describe("Y position to scroll at. Defaults to screen center.")
|
|
263
914
|
};
|
|
264
915
|
var BrowserPressInputSchema = {
|
|
265
|
-
session_id: import_zod.z.string().describe("The session id returned by browser_open."),
|
|
916
|
+
session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
266
917
|
keys: import_zod.z.array(import_zod.z.string()).min(1).describe('Keys or combinations to press, e.g. ["Return"], ["Ctrl+a"], ["Ctrl+Shift+Tab"].')
|
|
267
918
|
};
|
|
268
919
|
var BrowserReplayStopInputSchema = {
|
|
269
|
-
session_id: import_zod.z.string().describe("The session id returned by browser_open."),
|
|
270
|
-
replay_id: import_zod.z.string().describe("The replay id returned by browser_replay_start.")
|
|
920
|
+
session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
921
|
+
replay_id: import_zod.z.string().describe("The replay id returned by browser_replay_start or browser_list_replays. Use only a returned replay_id; do not construct one yourself.")
|
|
271
922
|
};
|
|
272
923
|
var BrowserReplayDownloadInputSchema = {
|
|
273
|
-
session_id: import_zod.z.string().describe("The session id returned by browser_open."),
|
|
274
|
-
replay_id: import_zod.z.string().describe("The replay id returned by browser_replay_start or browser_list_replays."),
|
|
924
|
+
session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
925
|
+
replay_id: import_zod.z.string().describe("The replay id returned by browser_replay_start or browser_list_replays. Use only a returned replay_id; do not construct one yourself."),
|
|
275
926
|
filename: import_zod.z.string().optional().describe("Optional local MP4 filename. Defaults to a timestamped replay filename.")
|
|
276
927
|
};
|
|
277
928
|
var BrowserReplayAnnotationSchema = import_zod.z.object({
|
|
@@ -293,7 +944,7 @@ var BrowserReplayAnnotationSchema = import_zod.z.object({
|
|
|
293
944
|
thickness: import_zod.z.number().min(1).max(24).optional().describe("Stroke thickness in pixels. Defaults to 5.")
|
|
294
945
|
});
|
|
295
946
|
var BrowserReplayMarkInputSchema = {
|
|
296
|
-
session_id: import_zod.z.string().describe("The session id returned by browser_open. A replay must already be recording."),
|
|
947
|
+
session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself. A replay must already be recording."),
|
|
297
948
|
target: BrowserLocateTargetSchema.describe("The exact DOM element or text range to mark in the current viewport."),
|
|
298
949
|
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."),
|
|
299
950
|
label: import_zod.z.string().max(120).optional().describe("Optional callout text to render near the target."),
|
|
@@ -304,8 +955,8 @@ var BrowserReplayMarkInputSchema = {
|
|
|
304
955
|
duration_seconds: import_zod.z.number().min(0.5).max(30).default(4).describe("How long the generated annotation should remain visible.")
|
|
305
956
|
};
|
|
306
957
|
var BrowserReplayAnnotateInputSchema = {
|
|
307
|
-
session_id: import_zod.z.string().describe("The session id returned by browser_open."),
|
|
308
|
-
replay_id: import_zod.z.string().describe("The replay id returned by browser_replay_start or browser_list_replays."),
|
|
958
|
+
session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
959
|
+
replay_id: import_zod.z.string().describe("The replay id returned by browser_replay_start or browser_list_replays. Use only a returned replay_id; do not construct one yourself."),
|
|
309
960
|
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."),
|
|
310
961
|
filename: import_zod.z.string().optional().describe("Optional output MP4 filename. Defaults to a timestamped annotated replay filename."),
|
|
311
962
|
source_width: import_zod.z.number().positive().optional().describe("Width of the screenshot coordinate space used for annotations. Defaults to the replay video width."),
|
|
@@ -316,12 +967,156 @@ var BrowserReplayAnnotateInputSchema = {
|
|
|
316
967
|
var BrowserListInputSchema = {
|
|
317
968
|
include_closed: import_zod.z.boolean().default(false).describe("Include closed sessions in the list.")
|
|
318
969
|
};
|
|
970
|
+
var BrowserOpenOutputSchema = {
|
|
971
|
+
ok: import_zod.z.boolean(),
|
|
972
|
+
tool: import_zod.z.literal("browser_open"),
|
|
973
|
+
session_id: import_zod.z.string().describe("Session id returned by browser_open. Use only this exact value in later browser_* calls; do not construct one yourself."),
|
|
974
|
+
watch_url: import_zod.z.string().describe("Human watch/takeover URL for this browser session."),
|
|
975
|
+
live_view_url: NullableString.describe("Raw live-view URL from the browser service when available."),
|
|
976
|
+
url: NullableString.describe("Initial URL requested by the caller, when provided."),
|
|
977
|
+
hint: import_zod.z.string(),
|
|
978
|
+
raw: BrowserRawObject.optional()
|
|
979
|
+
};
|
|
980
|
+
var BrowserProfileListOutputSchema = {
|
|
981
|
+
ok: import_zod.z.boolean(),
|
|
982
|
+
tool: import_zod.z.literal("browser_profile_list"),
|
|
983
|
+
session_id: import_zod.z.null(),
|
|
984
|
+
profiles: import_zod.z.array(BrowserRawObject),
|
|
985
|
+
count: import_zod.z.number().int().min(0),
|
|
986
|
+
hint: import_zod.z.string()
|
|
987
|
+
};
|
|
988
|
+
var BrowserProfileOnboardOutputSchema = {
|
|
989
|
+
ok: import_zod.z.boolean(),
|
|
990
|
+
tool: import_zod.z.literal("browser_profile_onboard"),
|
|
991
|
+
session_id: import_zod.z.string(),
|
|
992
|
+
watch_url: import_zod.z.string(),
|
|
993
|
+
live_view_url: NullableString,
|
|
994
|
+
profile: import_zod.z.string(),
|
|
995
|
+
setup_url: import_zod.z.string(),
|
|
996
|
+
chrome_profile: BrowserRawObject.nullable(),
|
|
997
|
+
next_steps: import_zod.z.array(import_zod.z.string()),
|
|
998
|
+
raw: BrowserRawObject.optional()
|
|
999
|
+
};
|
|
1000
|
+
var BrowserProfileImportOutputSchema = {
|
|
1001
|
+
ok: import_zod.z.boolean(),
|
|
1002
|
+
tool: import_zod.z.literal("browser_profile_import"),
|
|
1003
|
+
session_id: import_zod.z.null(),
|
|
1004
|
+
profile: BrowserRawObject,
|
|
1005
|
+
source_profile: BrowserRawObject,
|
|
1006
|
+
next_steps: import_zod.z.array(import_zod.z.string()),
|
|
1007
|
+
hint: import_zod.z.string()
|
|
1008
|
+
};
|
|
1009
|
+
var BrowserProfileSyncOutputSchema = {
|
|
1010
|
+
ok: import_zod.z.boolean(),
|
|
1011
|
+
tool: import_zod.z.literal("browser_profile_sync"),
|
|
1012
|
+
session_id: import_zod.z.null(),
|
|
1013
|
+
profile: BrowserRawObject,
|
|
1014
|
+
source_profile: BrowserRawObject,
|
|
1015
|
+
hint: import_zod.z.string()
|
|
1016
|
+
};
|
|
1017
|
+
var BrowserScreenshotOutputSchema = {
|
|
1018
|
+
...BrowserBaseOutput,
|
|
1019
|
+
tool: import_zod.z.literal("browser_screenshot"),
|
|
1020
|
+
url: NullableString,
|
|
1021
|
+
title: NullableString,
|
|
1022
|
+
text: import_zod.z.string(),
|
|
1023
|
+
elements: import_zod.z.array(BrowserElementOutput),
|
|
1024
|
+
screenshot: import_zod.z.object({
|
|
1025
|
+
mime_type: import_zod.z.string(),
|
|
1026
|
+
inline: import_zod.z.boolean()
|
|
1027
|
+
}).nullable()
|
|
1028
|
+
};
|
|
1029
|
+
var BrowserReadOutputSchema = {
|
|
1030
|
+
...BrowserBaseOutput,
|
|
1031
|
+
tool: import_zod.z.literal("browser_read"),
|
|
1032
|
+
url: NullableString,
|
|
1033
|
+
title: NullableString,
|
|
1034
|
+
text: import_zod.z.string(),
|
|
1035
|
+
elements: import_zod.z.array(BrowserElementOutput),
|
|
1036
|
+
raw: BrowserRawObject.optional()
|
|
1037
|
+
};
|
|
1038
|
+
var BrowserLocateOutputSchema = {
|
|
1039
|
+
...BrowserBaseOutput,
|
|
1040
|
+
tool: import_zod.z.literal("browser_locate"),
|
|
1041
|
+
url: NullableString,
|
|
1042
|
+
title: NullableString,
|
|
1043
|
+
viewport: BrowserRawObject.nullable(),
|
|
1044
|
+
replay: BrowserRawObject.nullable(),
|
|
1045
|
+
targets: import_zod.z.array(BrowserRawObject),
|
|
1046
|
+
raw: BrowserRawObject.optional()
|
|
1047
|
+
};
|
|
1048
|
+
var BrowserActionOutputSchema = {
|
|
1049
|
+
...BrowserBaseOutput,
|
|
1050
|
+
result: BrowserRawObject.describe("Provider action result. Check ok and follow with browser_screenshot/browser_read when page state matters."),
|
|
1051
|
+
nextRecommendedTool: import_zod.z.string().nullable()
|
|
1052
|
+
};
|
|
1053
|
+
var BrowserReplayStartOutputSchema = {
|
|
1054
|
+
...BrowserReplayBaseOutput,
|
|
1055
|
+
tool: import_zod.z.literal("browser_replay_start"),
|
|
1056
|
+
view_url: NullableString,
|
|
1057
|
+
download_url: NullableString,
|
|
1058
|
+
raw: BrowserRawObject.optional()
|
|
1059
|
+
};
|
|
1060
|
+
var BrowserReplayStopOutputSchema = {
|
|
1061
|
+
...BrowserReplayBaseOutput,
|
|
1062
|
+
tool: import_zod.z.literal("browser_replay_stop"),
|
|
1063
|
+
view_url: NullableString,
|
|
1064
|
+
download_url: NullableString,
|
|
1065
|
+
raw: BrowserRawObject.optional()
|
|
1066
|
+
};
|
|
1067
|
+
var BrowserListReplaysOutputSchema = {
|
|
1068
|
+
...BrowserBaseOutput,
|
|
1069
|
+
tool: import_zod.z.literal("browser_list_replays"),
|
|
1070
|
+
replays: import_zod.z.array(BrowserRawObject),
|
|
1071
|
+
count: import_zod.z.number().int().min(0)
|
|
1072
|
+
};
|
|
1073
|
+
var BrowserReplayDownloadOutputSchema = {
|
|
1074
|
+
...BrowserReplayBaseOutput,
|
|
1075
|
+
tool: import_zod.z.literal("browser_replay_download"),
|
|
1076
|
+
file_path: NullableString,
|
|
1077
|
+
bytes: import_zod.z.number().int().min(0).nullable(),
|
|
1078
|
+
mime_type: NullableString,
|
|
1079
|
+
download_url: NullableString
|
|
1080
|
+
};
|
|
1081
|
+
var BrowserReplayMarkOutputSchema = {
|
|
1082
|
+
...BrowserReplayBaseOutput,
|
|
1083
|
+
tool: import_zod.z.literal("browser_replay_mark"),
|
|
1084
|
+
annotation: BrowserRawObject,
|
|
1085
|
+
source_width: import_zod.z.number().nullable(),
|
|
1086
|
+
source_height: import_zod.z.number().nullable(),
|
|
1087
|
+
target: BrowserRawObject.nullable(),
|
|
1088
|
+
hint: import_zod.z.string()
|
|
1089
|
+
};
|
|
1090
|
+
var BrowserReplayAnnotateOutputSchema = {
|
|
1091
|
+
...BrowserReplayBaseOutput,
|
|
1092
|
+
tool: import_zod.z.literal("browser_replay_annotate"),
|
|
1093
|
+
source_file_path: NullableString,
|
|
1094
|
+
annotated_file_path: NullableString,
|
|
1095
|
+
bytes: import_zod.z.number().int().min(0).nullable(),
|
|
1096
|
+
width: import_zod.z.number().int().min(0).nullable(),
|
|
1097
|
+
height: import_zod.z.number().int().min(0).nullable(),
|
|
1098
|
+
annotation_count: import_zod.z.number().int().min(0).nullable(),
|
|
1099
|
+
mime_type: NullableString
|
|
1100
|
+
};
|
|
1101
|
+
var BrowserCloseOutputSchema = {
|
|
1102
|
+
...BrowserBaseOutput,
|
|
1103
|
+
tool: import_zod.z.literal("browser_close"),
|
|
1104
|
+
closed: import_zod.z.boolean(),
|
|
1105
|
+
raw: BrowserRawObject.optional()
|
|
1106
|
+
};
|
|
1107
|
+
var BrowserListSessionsOutputSchema = {
|
|
1108
|
+
ok: import_zod.z.boolean(),
|
|
1109
|
+
tool: import_zod.z.literal("browser_list_sessions"),
|
|
1110
|
+
session_id: import_zod.z.null(),
|
|
1111
|
+
sessions: import_zod.z.array(BrowserRawObject),
|
|
1112
|
+
count: import_zod.z.number().int().min(0)
|
|
1113
|
+
};
|
|
319
1114
|
|
|
320
1115
|
// src/mcp/replay-annotator.ts
|
|
321
1116
|
var import_node_child_process = require("child_process");
|
|
322
|
-
var
|
|
323
|
-
var
|
|
324
|
-
var
|
|
1117
|
+
var import_promises3 = require("fs/promises");
|
|
1118
|
+
var import_node_os3 = require("os");
|
|
1119
|
+
var import_node_path4 = require("path");
|
|
325
1120
|
var import_node_util = require("util");
|
|
326
1121
|
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
327
1122
|
function finiteNumber(value) {
|
|
@@ -544,10 +1339,10 @@ function ffmpegFilterPath(path) {
|
|
|
544
1339
|
async function annotateReplayVideo(inputFilePath, outputFilePath, options) {
|
|
545
1340
|
if (!options.annotations.length) throw new Error("annotations must include at least one item");
|
|
546
1341
|
const size = await videoSize(inputFilePath);
|
|
547
|
-
const tmp = await (0,
|
|
548
|
-
const assPath = (0,
|
|
1342
|
+
const tmp = await (0, import_promises3.mkdtemp)((0, import_node_path4.join)((0, import_node_os3.tmpdir)(), "mcp-scraper-ass-"));
|
|
1343
|
+
const assPath = (0, import_node_path4.join)(tmp, "annotations.ass");
|
|
549
1344
|
try {
|
|
550
|
-
await (0,
|
|
1345
|
+
await (0, import_promises3.writeFile)(assPath, buildAssSubtitle(options, size), "utf8");
|
|
551
1346
|
await execFileAsync("ffmpeg", [
|
|
552
1347
|
"-y",
|
|
553
1348
|
"-i",
|
|
@@ -564,7 +1359,7 @@ async function annotateReplayVideo(inputFilePath, outputFilePath, options) {
|
|
|
564
1359
|
"copy",
|
|
565
1360
|
outputFilePath
|
|
566
1361
|
], { maxBuffer: 1024 * 1024 * 20 });
|
|
567
|
-
const out = await (0,
|
|
1362
|
+
const out = await (0, import_promises3.stat)(outputFilePath);
|
|
568
1363
|
return {
|
|
569
1364
|
filePath: outputFilePath,
|
|
570
1365
|
bytes: out.size,
|
|
@@ -573,16 +1368,48 @@ async function annotateReplayVideo(inputFilePath, outputFilePath, options) {
|
|
|
573
1368
|
annotationCount: options.annotations.length
|
|
574
1369
|
};
|
|
575
1370
|
} finally {
|
|
576
|
-
await (0,
|
|
1371
|
+
await (0, import_promises3.rm)(tmp, { recursive: true, force: true });
|
|
577
1372
|
}
|
|
578
1373
|
}
|
|
579
1374
|
|
|
580
1375
|
// src/mcp/browser-agent-mcp-server.ts
|
|
581
|
-
function
|
|
582
|
-
return {
|
|
1376
|
+
function structuredResult(value, isError = false) {
|
|
1377
|
+
return {
|
|
1378
|
+
content: [{ type: "text", text: JSON.stringify(value) }],
|
|
1379
|
+
structuredContent: value,
|
|
1380
|
+
isError
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
function errorMessage(value) {
|
|
1384
|
+
if (value && typeof value === "object") {
|
|
1385
|
+
const data = value;
|
|
1386
|
+
if (typeof data.error === "string") return data.error;
|
|
1387
|
+
if (typeof data.message === "string") return data.message;
|
|
1388
|
+
}
|
|
1389
|
+
return typeof value === "string" ? value : "Browser Agent request failed";
|
|
1390
|
+
}
|
|
1391
|
+
function errorResult(tool, value, sessionId = null, replayId = null) {
|
|
1392
|
+
return structuredResult({
|
|
1393
|
+
ok: false,
|
|
1394
|
+
tool,
|
|
1395
|
+
session_id: sessionId,
|
|
1396
|
+
...replayId !== null ? { replay_id: replayId } : {},
|
|
1397
|
+
error: errorMessage(value),
|
|
1398
|
+
raw: value && typeof value === "object" ? value : { value }
|
|
1399
|
+
}, true);
|
|
1400
|
+
}
|
|
1401
|
+
function actionResult(tool, sessionId, ok, data, nextRecommendedTool = "browser_screenshot") {
|
|
1402
|
+
if (!ok) return errorResult(tool, data, sessionId);
|
|
1403
|
+
return structuredResult({
|
|
1404
|
+
ok: true,
|
|
1405
|
+
tool,
|
|
1406
|
+
session_id: sessionId,
|
|
1407
|
+
result: data && typeof data === "object" ? data : { value: data },
|
|
1408
|
+
nextRecommendedTool
|
|
1409
|
+
});
|
|
583
1410
|
}
|
|
584
1411
|
function outputBaseDir() {
|
|
585
|
-
return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0,
|
|
1412
|
+
return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path5.join)((0, import_node_os4.homedir)(), "Downloads", "mcp-scraper");
|
|
586
1413
|
}
|
|
587
1414
|
function safeFilePart(value) {
|
|
588
1415
|
return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 120) || "replay";
|
|
@@ -591,7 +1418,7 @@ function replayFilePath(sessionId, replayId, filename) {
|
|
|
591
1418
|
const requested = filename?.trim();
|
|
592
1419
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
593
1420
|
const name = requested ? safeFilePart(requested).replace(/\.mp4$/i, "") : `${stamp}-${safeFilePart(sessionId)}-${safeFilePart(replayId)}`;
|
|
594
|
-
return (0,
|
|
1421
|
+
return (0, import_node_path5.join)(outputBaseDir(), "browser-replays", `${name}.mp4`);
|
|
595
1422
|
}
|
|
596
1423
|
function annotatedReplayFilePath(sessionId, replayId, filename) {
|
|
597
1424
|
const requested = filename?.trim();
|
|
@@ -622,6 +1449,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
622
1449
|
const baseUrl2 = opts.baseUrl.replace(/\/$/, "");
|
|
623
1450
|
const consoleBase = (opts.consoleBaseUrl ?? opts.baseUrl).replace(/\/$/, "");
|
|
624
1451
|
const timeoutMs = opts.timeoutMs ?? 9e4;
|
|
1452
|
+
const localBrowser = new LocalBrowserAgentService();
|
|
625
1453
|
async function req(method, path, body) {
|
|
626
1454
|
try {
|
|
627
1455
|
const res = await fetch(`${baseUrl2}${path}`, {
|
|
@@ -650,8 +1478,8 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
650
1478
|
}
|
|
651
1479
|
const bytes = Buffer.from(await res.arrayBuffer());
|
|
652
1480
|
const filePath = replayFilePath(sessionId, replayId, filename);
|
|
653
|
-
(0,
|
|
654
|
-
(0,
|
|
1481
|
+
(0, import_node_fs3.mkdirSync)((0, import_node_path5.join)(outputBaseDir(), "browser-replays"), { recursive: true });
|
|
1482
|
+
(0, import_node_fs3.writeFileSync)(filePath, bytes);
|
|
655
1483
|
return {
|
|
656
1484
|
ok: true,
|
|
657
1485
|
data: {
|
|
@@ -673,30 +1501,211 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
673
1501
|
idempotentHint: false,
|
|
674
1502
|
openWorldHint: true
|
|
675
1503
|
});
|
|
1504
|
+
function selectChromeProfile(matches, email) {
|
|
1505
|
+
const wanted = email?.trim().toLowerCase();
|
|
1506
|
+
if (!wanted) return matches[0] ?? null;
|
|
1507
|
+
return matches.find((profile) => profile.email.toLowerCase() === wanted) ?? matches[0] ?? null;
|
|
1508
|
+
}
|
|
1509
|
+
function localReplayUnsupported(tool, sessionId = null, replayId = null) {
|
|
1510
|
+
return errorResult(tool, {
|
|
1511
|
+
error: "Local browser mode does not support replay recording yet. Use hosted mode for browser_replay_* tools.",
|
|
1512
|
+
mode: "local"
|
|
1513
|
+
}, sessionId, replayId);
|
|
1514
|
+
}
|
|
1515
|
+
server2.registerTool(
|
|
1516
|
+
"browser_profile_list",
|
|
1517
|
+
{
|
|
1518
|
+
title: "List Local Chrome Profiles",
|
|
1519
|
+
description: "List local Chrome profile metadata visible to this MCP server: account email, Chrome profile directory, local path, and a suggested managed browser profile name. This reads Chrome Local State only; it does not read cookies, passwords, browsing history, or copy local browser state. Use browser_profile_import when the user wants MCP Scraper to clone a local Chrome profile for local browser mode.",
|
|
1520
|
+
inputSchema: BrowserProfileListInputSchema,
|
|
1521
|
+
outputSchema: BrowserProfileListOutputSchema,
|
|
1522
|
+
annotations: annotations("List Local Chrome Profiles", true)
|
|
1523
|
+
},
|
|
1524
|
+
async (input) => {
|
|
1525
|
+
try {
|
|
1526
|
+
const profiles = await listLocalChromeProfiles(input.user_data_dir, input.email);
|
|
1527
|
+
return structuredResult({
|
|
1528
|
+
ok: true,
|
|
1529
|
+
tool: "browser_profile_list",
|
|
1530
|
+
session_id: null,
|
|
1531
|
+
profiles,
|
|
1532
|
+
count: profiles.length,
|
|
1533
|
+
hint: "Use browser_profile_import to clone one profile for local mode. Use browser_profile_onboard only for hosted Kernel profile setup."
|
|
1534
|
+
});
|
|
1535
|
+
} catch (err) {
|
|
1536
|
+
return errorResult("browser_profile_list", { error: err instanceof Error ? err.message : String(err) });
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
);
|
|
1540
|
+
server2.registerTool(
|
|
1541
|
+
"browser_profile_import",
|
|
1542
|
+
{
|
|
1543
|
+
title: "Import Local Chrome Profile",
|
|
1544
|
+
description: "Clone one local Chrome profile into MCP Scraper managed storage for local browser mode. This copies browser state files such as cookies, history, local storage, session storage, and password database metadata into ~/.mcp-scraper/browser-profiles by default; it skips cache and lock files and never prints cookie or password values. Use this when the user wants browser_open to run locally with their existing logged-in Chrome state.",
|
|
1545
|
+
inputSchema: BrowserProfileImportInputSchema,
|
|
1546
|
+
outputSchema: BrowserProfileImportOutputSchema,
|
|
1547
|
+
annotations: { title: "Import Local Chrome Profile", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
|
|
1548
|
+
},
|
|
1549
|
+
async (input) => {
|
|
1550
|
+
try {
|
|
1551
|
+
const result = await importChromeProfile({
|
|
1552
|
+
email: input.email,
|
|
1553
|
+
name: input.name,
|
|
1554
|
+
chromeProfileDirectory: input.chrome_profile_directory,
|
|
1555
|
+
sourceUserDataDir: input.user_data_dir,
|
|
1556
|
+
outputDir: input.output_dir,
|
|
1557
|
+
overwrite: input.overwrite,
|
|
1558
|
+
browserExecutablePath: input.browser_executable_path
|
|
1559
|
+
});
|
|
1560
|
+
return structuredResult({
|
|
1561
|
+
ok: true,
|
|
1562
|
+
tool: "browser_profile_import",
|
|
1563
|
+
session_id: null,
|
|
1564
|
+
profile: result.profile,
|
|
1565
|
+
source_profile: result.sourceProfile,
|
|
1566
|
+
next_steps: [
|
|
1567
|
+
`Set MCP_SCRAPER_BROWSER_MODE=local in the MCP server config.`,
|
|
1568
|
+
`Set MCP_SCRAPER_BROWSER_PROFILE=${result.profile.name}.`,
|
|
1569
|
+
"Restart the MCP client so it starts the local browser mode process.",
|
|
1570
|
+
"Call browser_open with the same MCP server; it will open a local Chrome window using the cloned profile."
|
|
1571
|
+
],
|
|
1572
|
+
hint: "The clone is local to this machine. Re-run browser_profile_sync after logging into new sites in normal Chrome."
|
|
1573
|
+
});
|
|
1574
|
+
} catch (err) {
|
|
1575
|
+
return errorResult("browser_profile_import", { error: err instanceof Error ? err.message : String(err) });
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
);
|
|
1579
|
+
server2.registerTool(
|
|
1580
|
+
"browser_profile_sync",
|
|
1581
|
+
{
|
|
1582
|
+
title: "Sync Local Chrome Profile",
|
|
1583
|
+
description: "Refresh an existing MCP Scraper managed local browser profile from the source Chrome profile recorded in its manifest. This overwrites the managed clone, copies browser state files again, skips cache and lock files, and never prints cookie or password values.",
|
|
1584
|
+
inputSchema: BrowserProfileSyncInputSchema,
|
|
1585
|
+
outputSchema: BrowserProfileSyncOutputSchema,
|
|
1586
|
+
annotations: { title: "Sync Local Chrome Profile", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
|
|
1587
|
+
},
|
|
1588
|
+
async (input) => {
|
|
1589
|
+
try {
|
|
1590
|
+
const result = await syncImportedChromeProfile(input.name, {
|
|
1591
|
+
outputDir: input.output_dir,
|
|
1592
|
+
browserExecutablePath: input.browser_executable_path
|
|
1593
|
+
});
|
|
1594
|
+
return structuredResult({
|
|
1595
|
+
ok: true,
|
|
1596
|
+
tool: "browser_profile_sync",
|
|
1597
|
+
session_id: null,
|
|
1598
|
+
profile: result.profile,
|
|
1599
|
+
source_profile: result.sourceProfile,
|
|
1600
|
+
hint: `Refreshed ${result.profile.name}. Restart local browser sessions that were using this profile.`
|
|
1601
|
+
});
|
|
1602
|
+
} catch (err) {
|
|
1603
|
+
return errorResult("browser_profile_sync", { error: err instanceof Error ? err.message : String(err) });
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
);
|
|
1607
|
+
server2.registerTool(
|
|
1608
|
+
"browser_profile_onboard",
|
|
1609
|
+
{
|
|
1610
|
+
title: "Onboard Hosted Browser Profile",
|
|
1611
|
+
description: "Create or load a saved hosted Kernel browser profile and open a live setup browser with profile saving enabled. Use this only for hosted browser mode. If email is provided, the tool reads local Chrome profile metadata only to suggest the Kernel profile name; it does not import local cookies. For local browser mode with real Chrome state, use browser_profile_import instead.",
|
|
1612
|
+
inputSchema: BrowserProfileOnboardInputSchema,
|
|
1613
|
+
outputSchema: BrowserProfileOnboardOutputSchema,
|
|
1614
|
+
annotations: annotations("Onboard Hosted Browser Profile")
|
|
1615
|
+
},
|
|
1616
|
+
async (input) => {
|
|
1617
|
+
let chromeProfile = null;
|
|
1618
|
+
if (input.email) {
|
|
1619
|
+
try {
|
|
1620
|
+
chromeProfile = selectChromeProfile(await listLocalChromeProfiles(input.user_data_dir, input.email), input.email);
|
|
1621
|
+
} catch (err) {
|
|
1622
|
+
return errorResult("browser_profile_onboard", { error: err instanceof Error ? err.message : String(err) });
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
const profile = input.profile?.trim() || chromeProfile?.recommendedKernelProfileName || browserServiceProfileName();
|
|
1626
|
+
if (!profile) {
|
|
1627
|
+
return errorResult("browser_profile_onboard", {
|
|
1628
|
+
error: "profile is required when no email match or BROWSER_AGENT_PROFILE_NAME is available"
|
|
1629
|
+
});
|
|
1630
|
+
}
|
|
1631
|
+
const setupUrl = input.url ?? "https://accounts.google.com/";
|
|
1632
|
+
const open = await req("POST", "/agent/sessions", {
|
|
1633
|
+
label: input.label ?? `Profile setup: ${profile}`,
|
|
1634
|
+
profile,
|
|
1635
|
+
save_profile_changes: true,
|
|
1636
|
+
timeout_seconds: input.timeout_seconds
|
|
1637
|
+
});
|
|
1638
|
+
if (!open.ok) return errorResult("browser_profile_onboard", open.data);
|
|
1639
|
+
const session = open.data;
|
|
1640
|
+
const goto = await req("POST", `/agent/sessions/${session.session_id}/goto`, { url: setupUrl });
|
|
1641
|
+
if (!goto.ok) return errorResult("browser_profile_onboard", goto.data, session.session_id);
|
|
1642
|
+
return structuredResult({
|
|
1643
|
+
ok: true,
|
|
1644
|
+
tool: "browser_profile_onboard",
|
|
1645
|
+
session_id: session.session_id,
|
|
1646
|
+
watch_url: `${consoleBase}/console/${session.session_id}`,
|
|
1647
|
+
live_view_url: session.live_view_url ?? null,
|
|
1648
|
+
profile,
|
|
1649
|
+
setup_url: setupUrl,
|
|
1650
|
+
chrome_profile: chromeProfile,
|
|
1651
|
+
next_steps: [
|
|
1652
|
+
"Open the watch_url and complete the login manually.",
|
|
1653
|
+
"After login is complete, call browser_close with this session_id to persist cookies and local storage to the Kernel profile.",
|
|
1654
|
+
"Use BROWSER_AGENT_PROFILE_NAME or browser_open.profile with this profile name for future authenticated sessions."
|
|
1655
|
+
],
|
|
1656
|
+
raw: session
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
);
|
|
676
1660
|
server2.registerTool(
|
|
677
1661
|
"browser_open",
|
|
678
1662
|
{
|
|
679
1663
|
title: "Open Browser Session",
|
|
680
|
-
description: "Open a
|
|
1664
|
+
description: "Open a browser you can drive. By default this creates a hosted cloud browser with a watch_url; when MCP_SCRAPER_BROWSER_MODE=local it opens a local Google Chrome window against an imported MCP Scraper Chrome profile. Returns a session_id used by all other browser_* tools. In hosted mode, anti-bot stealth and automatic CAPTCHA/Cloudflare solving are on by default. Call browser_close when done.",
|
|
681
1665
|
inputSchema: BrowserOpenInputSchema,
|
|
1666
|
+
outputSchema: BrowserOpenOutputSchema,
|
|
682
1667
|
annotations: annotations("Open Browser Session")
|
|
683
1668
|
},
|
|
684
1669
|
async (input) => {
|
|
1670
|
+
if (localBrowserModeEnabled()) {
|
|
1671
|
+
try {
|
|
1672
|
+
const session2 = await localBrowser.open(input);
|
|
1673
|
+
return structuredResult({
|
|
1674
|
+
ok: true,
|
|
1675
|
+
tool: "browser_open",
|
|
1676
|
+
session_id: session2.session_id,
|
|
1677
|
+
watch_url: session2.watch_url,
|
|
1678
|
+
live_view_url: null,
|
|
1679
|
+
url: input.url ?? null,
|
|
1680
|
+
hint: "Local Chrome is open on this machine. Call browser_screenshot to see the page and click by x,y coordinates.",
|
|
1681
|
+
raw: session2
|
|
1682
|
+
});
|
|
1683
|
+
} catch (err) {
|
|
1684
|
+
return errorResult("browser_open", { error: err instanceof Error ? err.message : String(err), mode: "local" });
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
const profile = input.profile ?? browserServiceProfileName();
|
|
1688
|
+
const saveProfileChanges = input.save_profile_changes ?? browserServiceProfileSaveChanges();
|
|
685
1689
|
const open = await req("POST", "/agent/sessions", {
|
|
686
1690
|
label: input.label,
|
|
687
|
-
profile:
|
|
1691
|
+
...profile ? { profile } : {},
|
|
1692
|
+
...profile && typeof saveProfileChanges === "boolean" ? { save_profile_changes: saveProfileChanges } : {},
|
|
688
1693
|
timeout_seconds: input.timeout_seconds
|
|
689
1694
|
});
|
|
690
|
-
if (!open.ok) return
|
|
1695
|
+
if (!open.ok) return errorResult("browser_open", open.data);
|
|
691
1696
|
const session = open.data;
|
|
692
1697
|
if (input.url) {
|
|
693
1698
|
await req("POST", `/agent/sessions/${session.session_id}/goto`, { url: input.url });
|
|
694
1699
|
}
|
|
695
|
-
return
|
|
1700
|
+
return structuredResult({
|
|
1701
|
+
ok: true,
|
|
1702
|
+
tool: "browser_open",
|
|
696
1703
|
session_id: session.session_id,
|
|
697
1704
|
watch_url: `${consoleBase}/console/${session.session_id}`,
|
|
698
1705
|
live_view_url: session.live_view_url ?? null,
|
|
699
|
-
|
|
1706
|
+
url: input.url ?? null,
|
|
1707
|
+
hint: "Call browser_screenshot to see the page. Click by the x,y of an element from the snapshot.",
|
|
1708
|
+
raw: session
|
|
700
1709
|
});
|
|
701
1710
|
}
|
|
702
1711
|
);
|
|
@@ -706,19 +1715,51 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
706
1715
|
title: "See Page (Screenshot + Elements)",
|
|
707
1716
|
description: "Capture what the browser currently shows. Returns a screenshot image PLUS a text snapshot listing interactive elements with their center x,y coordinates, the page url and title, and visible text. This is your primary way to perceive the page. Click elements by their listed x,y. If a Cloudflare/CAPTCHA challenge is visible, wait and screenshot again rather than clicking it.",
|
|
708
1717
|
inputSchema: BrowserSessionInputSchema,
|
|
1718
|
+
outputSchema: BrowserScreenshotOutputSchema,
|
|
709
1719
|
annotations: annotations("See Page", true)
|
|
710
1720
|
},
|
|
711
1721
|
async (input) => {
|
|
1722
|
+
if (localBrowserModeEnabled()) {
|
|
1723
|
+
try {
|
|
1724
|
+
const res2 = await localBrowser.screenshot(input.session_id);
|
|
1725
|
+
const content2 = [];
|
|
1726
|
+
if (res2.image_base64) content2.push({ type: "image", data: res2.image_base64, mimeType: res2.mime_type });
|
|
1727
|
+
const structured2 = {
|
|
1728
|
+
ok: true,
|
|
1729
|
+
tool: "browser_screenshot",
|
|
1730
|
+
session_id: input.session_id,
|
|
1731
|
+
url: res2.url ?? null,
|
|
1732
|
+
title: res2.title ?? null,
|
|
1733
|
+
text: res2.text,
|
|
1734
|
+
elements: res2.elements,
|
|
1735
|
+
screenshot: { mime_type: res2.mime_type, inline: true }
|
|
1736
|
+
};
|
|
1737
|
+
content2.push({ type: "text", text: JSON.stringify(structured2) });
|
|
1738
|
+
return { content: content2, structuredContent: structured2 };
|
|
1739
|
+
} catch (err) {
|
|
1740
|
+
return errorResult("browser_screenshot", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
712
1743
|
const res = await req("POST", `/agent/sessions/${input.session_id}/screenshot`);
|
|
713
|
-
if (!res.ok) return
|
|
1744
|
+
if (!res.ok) return errorResult("browser_screenshot", res.data, input.session_id);
|
|
714
1745
|
const { image_base64, mime_type, url, title, elements, text } = res.data;
|
|
715
1746
|
const content = [];
|
|
716
1747
|
if (image_base64) content.push({ type: "image", data: image_base64, mimeType: mime_type ?? "image/png" });
|
|
1748
|
+
const structured = {
|
|
1749
|
+
ok: true,
|
|
1750
|
+
tool: "browser_screenshot",
|
|
1751
|
+
session_id: input.session_id,
|
|
1752
|
+
url: url ?? null,
|
|
1753
|
+
title: title ?? null,
|
|
1754
|
+
text: typeof text === "string" ? text : "",
|
|
1755
|
+
elements: Array.isArray(elements) ? elements : [],
|
|
1756
|
+
screenshot: image_base64 ? { mime_type: mime_type ?? "image/png", inline: true } : null
|
|
1757
|
+
};
|
|
717
1758
|
content.push({
|
|
718
1759
|
type: "text",
|
|
719
|
-
text: JSON.stringify(
|
|
1760
|
+
text: JSON.stringify(structured)
|
|
720
1761
|
});
|
|
721
|
-
return { content };
|
|
1762
|
+
return { content, structuredContent: structured };
|
|
722
1763
|
}
|
|
723
1764
|
);
|
|
724
1765
|
server2.registerTool(
|
|
@@ -727,11 +1768,39 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
727
1768
|
title: "Read Page Text + Elements",
|
|
728
1769
|
description: "Return the page url, title, visible text, and the list of interactive elements (with x,y) without an image. Cheaper than browser_screenshot when you only need to read content or find a target element to click.",
|
|
729
1770
|
inputSchema: BrowserSessionInputSchema,
|
|
1771
|
+
outputSchema: BrowserReadOutputSchema,
|
|
730
1772
|
annotations: annotations("Read Page", true)
|
|
731
1773
|
},
|
|
732
1774
|
async (input) => {
|
|
1775
|
+
if (localBrowserModeEnabled()) {
|
|
1776
|
+
try {
|
|
1777
|
+
const res2 = await localBrowser.read(input.session_id);
|
|
1778
|
+
return structuredResult({
|
|
1779
|
+
ok: true,
|
|
1780
|
+
tool: "browser_read",
|
|
1781
|
+
session_id: input.session_id,
|
|
1782
|
+
url: res2.url ?? null,
|
|
1783
|
+
title: res2.title ?? null,
|
|
1784
|
+
text: res2.text,
|
|
1785
|
+
elements: res2.elements,
|
|
1786
|
+
raw: res2
|
|
1787
|
+
});
|
|
1788
|
+
} catch (err) {
|
|
1789
|
+
return errorResult("browser_read", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
733
1792
|
const res = await req("POST", `/agent/sessions/${input.session_id}/read`);
|
|
734
|
-
return
|
|
1793
|
+
if (!res.ok) return errorResult("browser_read", res.data, input.session_id);
|
|
1794
|
+
return structuredResult({
|
|
1795
|
+
ok: true,
|
|
1796
|
+
tool: "browser_read",
|
|
1797
|
+
session_id: input.session_id,
|
|
1798
|
+
url: res.data?.url ?? null,
|
|
1799
|
+
title: res.data?.title ?? null,
|
|
1800
|
+
text: typeof res.data?.text === "string" ? res.data.text : "",
|
|
1801
|
+
elements: Array.isArray(res.data?.elements) ? res.data.elements : [],
|
|
1802
|
+
raw: res.data
|
|
1803
|
+
});
|
|
735
1804
|
}
|
|
736
1805
|
);
|
|
737
1806
|
server2.registerTool(
|
|
@@ -740,86 +1809,161 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
740
1809
|
title: "Locate DOM Targets",
|
|
741
1810
|
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.",
|
|
742
1811
|
inputSchema: BrowserLocateInputSchema,
|
|
1812
|
+
outputSchema: BrowserLocateOutputSchema,
|
|
743
1813
|
annotations: annotations("Locate DOM Targets", true)
|
|
744
1814
|
},
|
|
745
1815
|
async (input) => {
|
|
1816
|
+
if (localBrowserModeEnabled()) {
|
|
1817
|
+
try {
|
|
1818
|
+
const res2 = await localBrowser.locate(input.session_id, input.targets);
|
|
1819
|
+
return structuredResult({
|
|
1820
|
+
ok: true,
|
|
1821
|
+
tool: "browser_locate",
|
|
1822
|
+
session_id: input.session_id,
|
|
1823
|
+
url: res2.url ?? null,
|
|
1824
|
+
title: res2.title ?? null,
|
|
1825
|
+
viewport: res2.viewport ?? null,
|
|
1826
|
+
replay: null,
|
|
1827
|
+
targets: Array.isArray(res2.targets) ? res2.targets : [],
|
|
1828
|
+
raw: res2
|
|
1829
|
+
});
|
|
1830
|
+
} catch (err) {
|
|
1831
|
+
return errorResult("browser_locate", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
746
1834
|
const res = await req("POST", `/agent/sessions/${input.session_id}/locate`, { targets: input.targets });
|
|
747
|
-
return
|
|
1835
|
+
if (!res.ok) return errorResult("browser_locate", res.data, input.session_id);
|
|
1836
|
+
return structuredResult({
|
|
1837
|
+
ok: true,
|
|
1838
|
+
tool: "browser_locate",
|
|
1839
|
+
session_id: input.session_id,
|
|
1840
|
+
url: res.data?.url ?? null,
|
|
1841
|
+
title: res.data?.title ?? null,
|
|
1842
|
+
viewport: res.data?.viewport ?? null,
|
|
1843
|
+
replay: res.data?.replay ?? null,
|
|
1844
|
+
targets: Array.isArray(res.data?.targets) ? res.data.targets : [],
|
|
1845
|
+
raw: res.data
|
|
1846
|
+
});
|
|
748
1847
|
}
|
|
749
1848
|
);
|
|
750
1849
|
server2.registerTool(
|
|
751
1850
|
"browser_goto",
|
|
752
1851
|
{
|
|
753
1852
|
title: "Navigate To URL",
|
|
754
|
-
description: "Navigate
|
|
1853
|
+
description: "Navigate an existing browser session to a URL when the user asks you to go to another page or continue browsing in the same session. Use browser_open first if no session exists. Follow with browser_screenshot to see redirects, login walls, CAPTCHA/Cloudflare state, or the loaded page.",
|
|
755
1854
|
inputSchema: BrowserGotoInputSchema,
|
|
1855
|
+
outputSchema: BrowserActionOutputSchema,
|
|
756
1856
|
annotations: annotations("Navigate To URL")
|
|
757
1857
|
},
|
|
758
1858
|
async (input) => {
|
|
1859
|
+
if (localBrowserModeEnabled()) {
|
|
1860
|
+
try {
|
|
1861
|
+
const res2 = await localBrowser.goto(input.session_id, input.url);
|
|
1862
|
+
return actionResult("browser_goto", input.session_id, true, res2, "browser_screenshot");
|
|
1863
|
+
} catch (err) {
|
|
1864
|
+
return errorResult("browser_goto", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
759
1867
|
const res = await req("POST", `/agent/sessions/${input.session_id}/goto`, { url: input.url });
|
|
760
|
-
return
|
|
1868
|
+
return actionResult("browser_goto", input.session_id, res.ok, res.data, "browser_screenshot");
|
|
761
1869
|
}
|
|
762
1870
|
);
|
|
763
1871
|
server2.registerTool(
|
|
764
1872
|
"browser_click",
|
|
765
1873
|
{
|
|
766
1874
|
title: "Click",
|
|
767
|
-
description: "Click
|
|
1875
|
+
description: "Click a visible page target using screenshot pixel coordinates. Use this when the user asks you to press a button, open a menu, choose a result, or activate a visible UI element. Use x/y only from the latest browser_screenshot, browser_read, or browser_locate result; do not guess coordinates. Follow with browser_screenshot when the click may change the page.",
|
|
768
1876
|
inputSchema: BrowserClickInputSchema,
|
|
1877
|
+
outputSchema: BrowserActionOutputSchema,
|
|
769
1878
|
annotations: annotations("Click")
|
|
770
1879
|
},
|
|
771
1880
|
async (input) => {
|
|
1881
|
+
if (localBrowserModeEnabled()) {
|
|
1882
|
+
try {
|
|
1883
|
+
const res2 = await localBrowser.click(input.session_id, input);
|
|
1884
|
+
return actionResult("browser_click", input.session_id, true, res2, "browser_screenshot");
|
|
1885
|
+
} catch (err) {
|
|
1886
|
+
return errorResult("browser_click", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
772
1889
|
const res = await req("POST", `/agent/sessions/${input.session_id}/click`, {
|
|
773
1890
|
x: input.x,
|
|
774
1891
|
y: input.y,
|
|
775
1892
|
button: input.button,
|
|
776
1893
|
num_clicks: input.num_clicks
|
|
777
1894
|
});
|
|
778
|
-
return
|
|
1895
|
+
return actionResult("browser_click", input.session_id, res.ok, res.data, "browser_screenshot");
|
|
779
1896
|
}
|
|
780
1897
|
);
|
|
781
1898
|
server2.registerTool(
|
|
782
1899
|
"browser_type",
|
|
783
1900
|
{
|
|
784
1901
|
title: "Type Text",
|
|
785
|
-
description: 'Type text
|
|
1902
|
+
description: 'Type text into the currently focused browser field. Use this when the user asks you to fill a search box, form field, login field, or editable text area. Click or Tab to the field first if focus is uncertain. Use browser_press with ["Return"] to submit, and browser_screenshot afterward when page state matters.',
|
|
786
1903
|
inputSchema: BrowserTypeInputSchema,
|
|
1904
|
+
outputSchema: BrowserActionOutputSchema,
|
|
787
1905
|
annotations: annotations("Type Text")
|
|
788
1906
|
},
|
|
789
1907
|
async (input) => {
|
|
1908
|
+
if (localBrowserModeEnabled()) {
|
|
1909
|
+
try {
|
|
1910
|
+
const res2 = await localBrowser.type(input.session_id, input);
|
|
1911
|
+
return actionResult("browser_type", input.session_id, true, res2, "browser_screenshot");
|
|
1912
|
+
} catch (err) {
|
|
1913
|
+
return errorResult("browser_type", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
790
1916
|
const res = await req("POST", `/agent/sessions/${input.session_id}/type`, { text: input.text, delay: input.delay });
|
|
791
|
-
return
|
|
1917
|
+
return actionResult("browser_type", input.session_id, res.ok, res.data, "browser_screenshot");
|
|
792
1918
|
}
|
|
793
1919
|
);
|
|
794
1920
|
server2.registerTool(
|
|
795
1921
|
"browser_scroll",
|
|
796
1922
|
{
|
|
797
1923
|
title: "Scroll",
|
|
798
|
-
description: "Scroll the page. Positive delta_y scrolls down. Follow with browser_screenshot to
|
|
1924
|
+
description: "Scroll the page to reveal more content before reading, clicking, or locating elements. Positive delta_y scrolls down; negative delta_y scrolls up. Follow with browser_screenshot or browser_read to inspect newly revealed content.",
|
|
799
1925
|
inputSchema: BrowserScrollInputSchema,
|
|
1926
|
+
outputSchema: BrowserActionOutputSchema,
|
|
800
1927
|
annotations: annotations("Scroll")
|
|
801
1928
|
},
|
|
802
1929
|
async (input) => {
|
|
1930
|
+
if (localBrowserModeEnabled()) {
|
|
1931
|
+
try {
|
|
1932
|
+
const res2 = await localBrowser.scroll(input.session_id, input);
|
|
1933
|
+
return actionResult("browser_scroll", input.session_id, true, res2, "browser_screenshot");
|
|
1934
|
+
} catch (err) {
|
|
1935
|
+
return errorResult("browser_scroll", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
803
1938
|
const res = await req("POST", `/agent/sessions/${input.session_id}/scroll`, {
|
|
804
1939
|
delta_y: input.delta_y,
|
|
805
1940
|
delta_x: input.delta_x,
|
|
806
1941
|
x: input.x,
|
|
807
1942
|
y: input.y
|
|
808
1943
|
});
|
|
809
|
-
return
|
|
1944
|
+
return actionResult("browser_scroll", input.session_id, res.ok, res.data, "browser_screenshot");
|
|
810
1945
|
}
|
|
811
1946
|
);
|
|
812
1947
|
server2.registerTool(
|
|
813
1948
|
"browser_press",
|
|
814
1949
|
{
|
|
815
1950
|
title: "Press Keys",
|
|
816
|
-
description: 'Press keys or combinations,
|
|
1951
|
+
description: 'Press keyboard keys or combinations in the active browser session. Use this for submit, Escape, Tab navigation, select-all, or keyboard shortcuts. Examples: ["Return"], ["Escape"], ["Tab"], ["Ctrl+a"], ["Ctrl+Shift+Tab"]. Use browser_type for text entry and browser_screenshot after keypresses that may change the page.',
|
|
817
1952
|
inputSchema: BrowserPressInputSchema,
|
|
1953
|
+
outputSchema: BrowserActionOutputSchema,
|
|
818
1954
|
annotations: annotations("Press Keys")
|
|
819
1955
|
},
|
|
820
1956
|
async (input) => {
|
|
1957
|
+
if (localBrowserModeEnabled()) {
|
|
1958
|
+
try {
|
|
1959
|
+
const res2 = await localBrowser.press(input.session_id, input);
|
|
1960
|
+
return actionResult("browser_press", input.session_id, true, res2, "browser_screenshot");
|
|
1961
|
+
} catch (err) {
|
|
1962
|
+
return errorResult("browser_press", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
821
1965
|
const res = await req("POST", `/agent/sessions/${input.session_id}/press`, { keys: input.keys });
|
|
822
|
-
return
|
|
1966
|
+
return actionResult("browser_press", input.session_id, res.ok, res.data, "browser_screenshot");
|
|
823
1967
|
}
|
|
824
1968
|
);
|
|
825
1969
|
server2.registerTool(
|
|
@@ -828,11 +1972,22 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
828
1972
|
title: "Start Recording",
|
|
829
1973
|
description: "Start recording an MP4 replay of the session. Returns replay_id, view_url when available, and a download_url. Use to capture a task for later review; stop with browser_replay_stop.",
|
|
830
1974
|
inputSchema: BrowserSessionInputSchema,
|
|
1975
|
+
outputSchema: BrowserReplayStartOutputSchema,
|
|
831
1976
|
annotations: annotations("Start Recording")
|
|
832
1977
|
},
|
|
833
1978
|
async (input) => {
|
|
1979
|
+
if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_start", input.session_id);
|
|
834
1980
|
const res = await req("POST", `/agent/sessions/${input.session_id}/replay/start`);
|
|
835
|
-
return
|
|
1981
|
+
if (!res.ok) return errorResult("browser_replay_start", res.data, input.session_id);
|
|
1982
|
+
return structuredResult({
|
|
1983
|
+
ok: true,
|
|
1984
|
+
tool: "browser_replay_start",
|
|
1985
|
+
session_id: input.session_id,
|
|
1986
|
+
replay_id: res.data?.replay_id ?? res.data?.replayId ?? null,
|
|
1987
|
+
view_url: res.data?.view_url ?? res.data?.viewUrl ?? null,
|
|
1988
|
+
download_url: res.data?.download_url ?? res.data?.downloadUrl ?? null,
|
|
1989
|
+
raw: res.data
|
|
1990
|
+
});
|
|
836
1991
|
}
|
|
837
1992
|
);
|
|
838
1993
|
server2.registerTool(
|
|
@@ -841,11 +1996,22 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
841
1996
|
title: "Stop Recording",
|
|
842
1997
|
description: "Stop a replay recording and expose the final view_url and download_url. Use browser_replay_download to save the MP4 locally.",
|
|
843
1998
|
inputSchema: BrowserReplayStopInputSchema,
|
|
1999
|
+
outputSchema: BrowserReplayStopOutputSchema,
|
|
844
2000
|
annotations: annotations("Stop Recording")
|
|
845
2001
|
},
|
|
846
2002
|
async (input) => {
|
|
2003
|
+
if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_stop", input.session_id, input.replay_id);
|
|
847
2004
|
const res = await req("POST", `/agent/sessions/${input.session_id}/replay/stop`, { replay_id: input.replay_id });
|
|
848
|
-
return
|
|
2005
|
+
if (!res.ok) return errorResult("browser_replay_stop", res.data, input.session_id, input.replay_id);
|
|
2006
|
+
return structuredResult({
|
|
2007
|
+
ok: true,
|
|
2008
|
+
tool: "browser_replay_stop",
|
|
2009
|
+
session_id: input.session_id,
|
|
2010
|
+
replay_id: res.data?.replay_id ?? res.data?.replayId ?? input.replay_id,
|
|
2011
|
+
view_url: res.data?.view_url ?? res.data?.viewUrl ?? null,
|
|
2012
|
+
download_url: res.data?.download_url ?? res.data?.downloadUrl ?? null,
|
|
2013
|
+
raw: res.data
|
|
2014
|
+
});
|
|
849
2015
|
}
|
|
850
2016
|
);
|
|
851
2017
|
server2.registerTool(
|
|
@@ -854,11 +2020,21 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
854
2020
|
title: "List Replay Videos",
|
|
855
2021
|
description: "List replay recordings for a browser session, including final view_url and authenticated download_url values when available.",
|
|
856
2022
|
inputSchema: BrowserSessionInputSchema,
|
|
2023
|
+
outputSchema: BrowserListReplaysOutputSchema,
|
|
857
2024
|
annotations: annotations("List Replay Videos", true)
|
|
858
2025
|
},
|
|
859
2026
|
async (input) => {
|
|
2027
|
+
if (localBrowserModeEnabled()) return localReplayUnsupported("browser_list_replays", input.session_id);
|
|
860
2028
|
const res = await req("GET", `/agent/sessions/${input.session_id}/replays`);
|
|
861
|
-
return
|
|
2029
|
+
if (!res.ok) return errorResult("browser_list_replays", res.data, input.session_id);
|
|
2030
|
+
const replays = Array.isArray(res.data?.replays) ? res.data.replays : [];
|
|
2031
|
+
return structuredResult({
|
|
2032
|
+
ok: true,
|
|
2033
|
+
tool: "browser_list_replays",
|
|
2034
|
+
session_id: input.session_id,
|
|
2035
|
+
replays,
|
|
2036
|
+
count: replays.length
|
|
2037
|
+
});
|
|
862
2038
|
}
|
|
863
2039
|
);
|
|
864
2040
|
server2.registerTool(
|
|
@@ -867,11 +2043,23 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
867
2043
|
title: "Download Replay MP4",
|
|
868
2044
|
description: "Download a replay recording through MCP Scraper and save the MP4 locally under MCP_SCRAPER_OUTPUT_DIR/browser-replays. Use after browser_replay_stop or browser_list_replays.",
|
|
869
2045
|
inputSchema: BrowserReplayDownloadInputSchema,
|
|
870
|
-
|
|
2046
|
+
outputSchema: BrowserReplayDownloadOutputSchema,
|
|
2047
|
+
annotations: annotations("Download Replay MP4")
|
|
871
2048
|
},
|
|
872
2049
|
async (input) => {
|
|
2050
|
+
if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_download", input.session_id, input.replay_id);
|
|
873
2051
|
const res = await downloadReplay(input.session_id, input.replay_id, input.filename);
|
|
874
|
-
return
|
|
2052
|
+
if (!res.ok) return errorResult("browser_replay_download", res.data, input.session_id, input.replay_id);
|
|
2053
|
+
return structuredResult({
|
|
2054
|
+
ok: true,
|
|
2055
|
+
tool: "browser_replay_download",
|
|
2056
|
+
session_id: input.session_id,
|
|
2057
|
+
replay_id: input.replay_id,
|
|
2058
|
+
file_path: res.data?.file_path ?? null,
|
|
2059
|
+
bytes: typeof res.data?.bytes === "number" ? res.data.bytes : null,
|
|
2060
|
+
mime_type: res.data?.mime_type ?? null,
|
|
2061
|
+
download_url: res.data?.download_url ?? null
|
|
2062
|
+
});
|
|
875
2063
|
}
|
|
876
2064
|
);
|
|
877
2065
|
server2.registerTool(
|
|
@@ -880,19 +2068,21 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
880
2068
|
title: "Mark Replay Annotation",
|
|
881
2069
|
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.",
|
|
882
2070
|
inputSchema: BrowserReplayMarkInputSchema,
|
|
2071
|
+
outputSchema: BrowserReplayMarkOutputSchema,
|
|
883
2072
|
annotations: annotations("Mark Replay Annotation", true)
|
|
884
2073
|
},
|
|
885
2074
|
async (input) => {
|
|
2075
|
+
if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_mark", input.session_id);
|
|
886
2076
|
const res = await req("POST", `/agent/sessions/${input.session_id}/locate`, { targets: [input.target] });
|
|
887
|
-
if (!res.ok) return
|
|
2077
|
+
if (!res.ok) return errorResult("browser_replay_mark", res.data, input.session_id);
|
|
888
2078
|
const target = res.data?.targets?.[0];
|
|
889
2079
|
const element = target?.element;
|
|
890
2080
|
const elapsed = res.data?.replay?.replay_elapsed_seconds;
|
|
891
2081
|
if (!target?.found || !element) {
|
|
892
|
-
return
|
|
2082
|
+
return errorResult("browser_replay_mark", { error: target?.error ?? "target not found in current viewport", target }, input.session_id);
|
|
893
2083
|
}
|
|
894
2084
|
if (!finiteNumber2(elapsed)) {
|
|
895
|
-
return
|
|
2085
|
+
return errorResult("browser_replay_mark", { error: "no active replay clock found; call browser_replay_start before browser_replay_mark" }, input.session_id);
|
|
896
2086
|
}
|
|
897
2087
|
const padded = expandElementBounds(element, res.data?.viewport, input.padding ?? 8);
|
|
898
2088
|
const start = Math.max(0, elapsed + (input.start_offset_seconds ?? -0.25));
|
|
@@ -909,7 +2099,11 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
909
2099
|
...input.color ? { color: input.color } : {},
|
|
910
2100
|
...input.thickness ? { thickness: input.thickness } : {}
|
|
911
2101
|
};
|
|
912
|
-
return
|
|
2102
|
+
return structuredResult({
|
|
2103
|
+
ok: true,
|
|
2104
|
+
tool: "browser_replay_mark",
|
|
2105
|
+
session_id: input.session_id,
|
|
2106
|
+
replay_id: res.data?.replay?.replay_id ?? res.data?.replay?.replayId ?? null,
|
|
913
2107
|
annotation,
|
|
914
2108
|
source_width: padded.sourceWidth,
|
|
915
2109
|
source_height: padded.sourceHeight,
|
|
@@ -925,16 +2119,18 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
925
2119
|
title: "Annotate Replay MP4",
|
|
926
2120
|
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.",
|
|
927
2121
|
inputSchema: BrowserReplayAnnotateInputSchema,
|
|
928
|
-
|
|
2122
|
+
outputSchema: BrowserReplayAnnotateOutputSchema,
|
|
2123
|
+
annotations: annotations("Annotate Replay MP4")
|
|
929
2124
|
},
|
|
930
2125
|
async (input) => {
|
|
2126
|
+
if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_annotate", input.session_id, input.replay_id);
|
|
931
2127
|
const sourceName = input.filename ? `${input.filename}-source` : void 0;
|
|
932
2128
|
const downloaded = await downloadReplay(input.session_id, input.replay_id, sourceName);
|
|
933
|
-
if (!downloaded.ok) return
|
|
2129
|
+
if (!downloaded.ok) return errorResult("browser_replay_annotate", downloaded.data, input.session_id, input.replay_id);
|
|
934
2130
|
try {
|
|
935
2131
|
const sourcePath = String(downloaded.data.file_path);
|
|
936
2132
|
const outputPath = annotatedReplayFilePath(input.session_id, input.replay_id, input.filename);
|
|
937
|
-
(0,
|
|
2133
|
+
(0, import_node_fs3.mkdirSync)((0, import_node_path5.join)(outputBaseDir(), "browser-replays"), { recursive: true });
|
|
938
2134
|
const result = await annotateReplayVideo(sourcePath, outputPath, {
|
|
939
2135
|
annotations: input.annotations,
|
|
940
2136
|
sourceWidth: input.source_width,
|
|
@@ -942,7 +2138,10 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
942
2138
|
sourceLeftOffset: input.source_left_offset,
|
|
943
2139
|
sourceTopOffset: input.source_top_offset
|
|
944
2140
|
});
|
|
945
|
-
return
|
|
2141
|
+
return structuredResult({
|
|
2142
|
+
ok: true,
|
|
2143
|
+
tool: "browser_replay_annotate",
|
|
2144
|
+
session_id: input.session_id,
|
|
946
2145
|
replay_id: input.replay_id,
|
|
947
2146
|
source_file_path: sourcePath,
|
|
948
2147
|
annotated_file_path: result.filePath,
|
|
@@ -953,7 +2152,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
953
2152
|
mime_type: "video/mp4"
|
|
954
2153
|
});
|
|
955
2154
|
} catch (err) {
|
|
956
|
-
return
|
|
2155
|
+
return errorResult("browser_replay_annotate", { error: err instanceof Error ? err.message : String(err) }, input.session_id, input.replay_id);
|
|
957
2156
|
}
|
|
958
2157
|
}
|
|
959
2158
|
);
|
|
@@ -961,41 +2160,80 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
961
2160
|
"browser_close",
|
|
962
2161
|
{
|
|
963
2162
|
title: "Close Browser Session",
|
|
964
|
-
description: "Close and release
|
|
2163
|
+
description: "Close and release a browser session when the task is done, when the user asks to stop the browser, or when active browser billing should end. Use browser_list_sessions first if you need to recover the session_id.",
|
|
965
2164
|
inputSchema: BrowserSessionInputSchema,
|
|
2165
|
+
outputSchema: BrowserCloseOutputSchema,
|
|
966
2166
|
annotations: { title: "Close Browser Session", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
|
|
967
2167
|
},
|
|
968
2168
|
async (input) => {
|
|
2169
|
+
if (localBrowserModeEnabled()) {
|
|
2170
|
+
try {
|
|
2171
|
+
const res2 = await localBrowser.close(input.session_id);
|
|
2172
|
+
return structuredResult({
|
|
2173
|
+
ok: true,
|
|
2174
|
+
tool: "browser_close",
|
|
2175
|
+
session_id: input.session_id,
|
|
2176
|
+
closed: true,
|
|
2177
|
+
raw: res2
|
|
2178
|
+
});
|
|
2179
|
+
} catch (err) {
|
|
2180
|
+
return errorResult("browser_close", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
969
2183
|
const res = await req("DELETE", `/agent/sessions/${input.session_id}`);
|
|
970
|
-
return
|
|
2184
|
+
if (!res.ok) return errorResult("browser_close", res.data, input.session_id);
|
|
2185
|
+
return structuredResult({
|
|
2186
|
+
ok: true,
|
|
2187
|
+
tool: "browser_close",
|
|
2188
|
+
session_id: input.session_id,
|
|
2189
|
+
closed: true,
|
|
2190
|
+
raw: res.data
|
|
2191
|
+
});
|
|
971
2192
|
}
|
|
972
2193
|
);
|
|
973
2194
|
server2.registerTool(
|
|
974
2195
|
"browser_list_sessions",
|
|
975
2196
|
{
|
|
976
2197
|
title: "List Browser Sessions",
|
|
977
|
-
description: "List
|
|
2198
|
+
description: "List browser sessions and their status, with a watch_url for each. Use this to recover a session_id, find an active browser, or decide which session to close. Use browser_open to create a new session.",
|
|
978
2199
|
inputSchema: BrowserListInputSchema,
|
|
2200
|
+
outputSchema: BrowserListSessionsOutputSchema,
|
|
979
2201
|
annotations: annotations("List Browser Sessions", true)
|
|
980
2202
|
},
|
|
981
2203
|
async (input) => {
|
|
2204
|
+
if (localBrowserModeEnabled()) {
|
|
2205
|
+
const sessions2 = localBrowser.list(input.include_closed);
|
|
2206
|
+
return structuredResult({
|
|
2207
|
+
ok: true,
|
|
2208
|
+
tool: "browser_list_sessions",
|
|
2209
|
+
session_id: null,
|
|
2210
|
+
sessions: sessions2,
|
|
2211
|
+
count: sessions2.length
|
|
2212
|
+
});
|
|
2213
|
+
}
|
|
982
2214
|
const res = await req("GET", `/agent/sessions${input.include_closed ? "?all=1" : ""}`);
|
|
983
|
-
if (!res.ok) return
|
|
2215
|
+
if (!res.ok) return errorResult("browser_list_sessions", res.data);
|
|
984
2216
|
const sessions = (res.data.sessions ?? []).map((s) => ({ ...s, watch_url: `${consoleBase}/console/${s.session_id}` }));
|
|
985
|
-
return
|
|
2217
|
+
return structuredResult({
|
|
2218
|
+
ok: true,
|
|
2219
|
+
tool: "browser_list_sessions",
|
|
2220
|
+
session_id: null,
|
|
2221
|
+
sessions,
|
|
2222
|
+
count: sessions.length
|
|
2223
|
+
});
|
|
986
2224
|
}
|
|
987
2225
|
);
|
|
988
2226
|
}
|
|
989
2227
|
|
|
990
2228
|
// src/mcp/paa-mcp-server.ts
|
|
991
2229
|
var import_mcp2 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
992
|
-
var
|
|
993
|
-
var
|
|
2230
|
+
var import_node_fs5 = require("fs");
|
|
2231
|
+
var import_node_path7 = require("path");
|
|
994
2232
|
|
|
995
2233
|
// src/mcp/mcp-response-formatter.ts
|
|
996
|
-
var
|
|
997
|
-
var
|
|
998
|
-
var
|
|
2234
|
+
var import_node_fs4 = require("fs");
|
|
2235
|
+
var import_node_os5 = require("os");
|
|
2236
|
+
var import_node_path6 = require("path");
|
|
999
2237
|
|
|
1000
2238
|
// src/errors.ts
|
|
1001
2239
|
function sanitizeVendorName(message) {
|
|
@@ -1132,16 +2370,16 @@ function reportTitle(full) {
|
|
|
1132
2370
|
return title?.replace(/^#\s+/, "").trim() || "MCP Scraper Report";
|
|
1133
2371
|
}
|
|
1134
2372
|
function outputBaseDir2() {
|
|
1135
|
-
return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0,
|
|
2373
|
+
return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path6.join)((0, import_node_os5.homedir)(), "Downloads", "mcp-scraper");
|
|
1136
2374
|
}
|
|
1137
2375
|
function saveFullReport(full) {
|
|
1138
2376
|
if (!reportSavingEnabled || process.env.MCP_SCRAPER_SAVE_REPORTS === "false") return null;
|
|
1139
2377
|
const outDir = outputBaseDir2();
|
|
1140
2378
|
try {
|
|
1141
|
-
(0,
|
|
2379
|
+
(0, import_node_fs4.mkdirSync)(outDir, { recursive: true });
|
|
1142
2380
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
1143
|
-
const file = (0,
|
|
1144
|
-
(0,
|
|
2381
|
+
const file = (0, import_node_path6.join)(outDir, `${stamp}-${slugifyReportName(reportTitle(full))}.md`);
|
|
2382
|
+
(0, import_node_fs4.writeFileSync)(file, full, "utf8");
|
|
1145
2383
|
return file;
|
|
1146
2384
|
} catch {
|
|
1147
2385
|
return null;
|
|
@@ -1150,12 +2388,12 @@ function saveFullReport(full) {
|
|
|
1150
2388
|
function persistScreenshotLocally(base64, url) {
|
|
1151
2389
|
if (!reportSavingEnabled || process.env.MCP_SCRAPER_SAVE_REPORTS === "false") return null;
|
|
1152
2390
|
try {
|
|
1153
|
-
const dir = (0,
|
|
1154
|
-
(0,
|
|
2391
|
+
const dir = (0, import_node_path6.join)(outputBaseDir2(), "screenshots");
|
|
2392
|
+
(0, import_node_fs4.mkdirSync)(dir, { recursive: true });
|
|
1155
2393
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
1156
2394
|
const slug = url.replace(/^https?:\/\//, "").replace(/[^a-z0-9]+/gi, "-").replace(/^-+|-+$/g, "").slice(0, 60);
|
|
1157
|
-
const filePath = (0,
|
|
1158
|
-
(0,
|
|
2395
|
+
const filePath = (0, import_node_path6.join)(dir, `${stamp}-${slug}.png`);
|
|
2396
|
+
(0, import_node_fs4.writeFileSync)(filePath, Buffer.from(base64, "base64"));
|
|
1159
2397
|
return filePath;
|
|
1160
2398
|
} catch {
|
|
1161
2399
|
return null;
|
|
@@ -1462,7 +2700,7 @@ ${bodyMd.slice(0, 3e3)}${bodyMd.length > 3e3 ? "\n\n*(truncated)*" : ""}` : "";
|
|
|
1462
2700
|
const full = `# URL Extract: ${url}
|
|
1463
2701
|
**${title}**
|
|
1464
2702
|
${headingSection}${kpoSection}${brandingSection}${bodySection}${screenshotSection}${mediaSection}${tips}`;
|
|
1465
|
-
const
|
|
2703
|
+
const textResult = oneBlock(full);
|
|
1466
2704
|
const structuredContent = {
|
|
1467
2705
|
url,
|
|
1468
2706
|
title: d.title ?? null,
|
|
@@ -1477,13 +2715,13 @@ ${headingSection}${kpoSection}${brandingSection}${bodySection}${screenshotSectio
|
|
|
1477
2715
|
if (screenshotMeta?.base64) {
|
|
1478
2716
|
return {
|
|
1479
2717
|
content: [
|
|
1480
|
-
...
|
|
2718
|
+
...textResult.content,
|
|
1481
2719
|
{ type: "image", data: screenshotMeta.base64, mimeType: "image/png" }
|
|
1482
2720
|
],
|
|
1483
2721
|
structuredContent
|
|
1484
2722
|
};
|
|
1485
2723
|
}
|
|
1486
|
-
return { ...
|
|
2724
|
+
return { ...textResult, structuredContent };
|
|
1487
2725
|
}
|
|
1488
2726
|
function formatMapSiteUrls(raw, input) {
|
|
1489
2727
|
const parsed = parseData(raw);
|
|
@@ -1612,13 +2850,25 @@ ${videoRows}`,
|
|
|
1612
2850
|
}
|
|
1613
2851
|
};
|
|
1614
2852
|
}
|
|
2853
|
+
function structuredTranscriptChunks(chunks) {
|
|
2854
|
+
return chunks.map((c) => ({
|
|
2855
|
+
startSec: Number.isFinite(c.timestamp?.[0]) ? c.timestamp[0] : 0,
|
|
2856
|
+
endSec: Number.isFinite(c.timestamp?.[1]) ? c.timestamp[1] : 0,
|
|
2857
|
+
text: c.text
|
|
2858
|
+
}));
|
|
2859
|
+
}
|
|
2860
|
+
function wordCount(text) {
|
|
2861
|
+
return text.trim() ? text.trim().split(/\s+/).length : 0;
|
|
2862
|
+
}
|
|
1615
2863
|
function formatYoutubeTranscribe(raw, input) {
|
|
1616
2864
|
const parsed = parseData(raw);
|
|
1617
2865
|
if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
|
|
1618
2866
|
const d = parsed.data;
|
|
1619
2867
|
const text = d.text ?? "";
|
|
1620
2868
|
const chunks = d.chunks ?? [];
|
|
2869
|
+
const videoId = d.videoId ?? input.videoId ?? null;
|
|
1621
2870
|
const durSec = d.durationMs ? (d.durationMs / 1e3).toFixed(0) : "\u2014";
|
|
2871
|
+
const words = wordCount(text);
|
|
1622
2872
|
const chunkRows = chunks.slice(0, 50).map((c) => {
|
|
1623
2873
|
const sec = Number.isFinite(c.timestamp[0]) ? Math.floor(c.timestamp[0]) : 0;
|
|
1624
2874
|
const mm = String(Math.floor(sec / 60)).padStart(2, "0");
|
|
@@ -1626,8 +2876,8 @@ function formatYoutubeTranscribe(raw, input) {
|
|
|
1626
2876
|
return `| ${mm}:${ss} | ${cell(truncate(c.text, 120))} |`;
|
|
1627
2877
|
}).join("\n");
|
|
1628
2878
|
const full = [
|
|
1629
|
-
`# YouTube Transcript: \`${input.
|
|
1630
|
-
`**Duration:** ${durSec}s \xB7 **${
|
|
2879
|
+
`# YouTube Transcript: \`${videoId ?? input.url ?? "video"}\``,
|
|
2880
|
+
`**Duration:** ${durSec}s \xB7 **${words} words**`,
|
|
1631
2881
|
`
|
|
1632
2882
|
## Full Transcript
|
|
1633
2883
|
${text}`,
|
|
@@ -1640,7 +2890,22 @@ ${chunkRows}` : "",
|
|
|
1640
2890
|
---
|
|
1641
2891
|
\u{1F4A1} Harvest more from this channel: use \`youtube_harvest\` with \`mode: "channel"\``
|
|
1642
2892
|
].filter(Boolean).join("\n");
|
|
1643
|
-
return
|
|
2893
|
+
return {
|
|
2894
|
+
...oneBlock(full),
|
|
2895
|
+
structuredContent: {
|
|
2896
|
+
videoId,
|
|
2897
|
+
url: videoId ? `https://www.youtube.com/watch?v=${videoId}` : input.url ?? null,
|
|
2898
|
+
wordCount: words,
|
|
2899
|
+
chunkCount: chunks.length,
|
|
2900
|
+
durationMs: typeof d.durationMs === "number" ? d.durationMs : null,
|
|
2901
|
+
transcriptText: text,
|
|
2902
|
+
chunks: structuredTranscriptChunks(chunks),
|
|
2903
|
+
resolvedInputs: {
|
|
2904
|
+
videoId,
|
|
2905
|
+
url: input.url ?? null
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2908
|
+
};
|
|
1644
2909
|
}
|
|
1645
2910
|
function formatFacebookPageIntel(raw, input) {
|
|
1646
2911
|
const parsed = parseData(raw);
|
|
@@ -1650,12 +2915,13 @@ function formatFacebookPageIntel(raw, input) {
|
|
|
1650
2915
|
const ads = d.ads ?? [];
|
|
1651
2916
|
const s = d.summary ?? { totalAds: 0, activeCount: 0, videoCount: 0, imageCount: 0 };
|
|
1652
2917
|
const adBlocks = ads.map((ad, i) => [
|
|
1653
|
-
`### Ad ${i + 1}${ad.libraryId ? ` \xB7 \`${ad.libraryId}\`` : ""} \u2014 ${ad.status ?? "\u2014"} \xB7 ${ad.creativeType ?? "\u2014"} \xB7 ${ad.startDate ?? "\u2014"}`,
|
|
2918
|
+
`### Ad ${i + 1}${ad.libraryId ? ` \xB7 \`${ad.libraryId}\`` : ""} \u2014 ${ad.status ?? "\u2014"} \xB7 ${ad.creativeType ?? "\u2014"} \xB7 ${ad.startDate ?? ad.started ?? "\u2014"}`,
|
|
1654
2919
|
ad.headline ? `**Headline:** ${ad.headline}` : "",
|
|
1655
2920
|
ad.primaryText ? `**Copy:** ${truncate(ad.primaryText, 200)}` : "",
|
|
1656
2921
|
ad.cta ? `**CTA:** ${ad.cta}` : "",
|
|
1657
|
-
ad.
|
|
1658
|
-
ad.
|
|
2922
|
+
ad.landingUrl ? `**Landing URL:** ${ad.landingUrl}` : "",
|
|
2923
|
+
ad.videoUrl ?? ad.videoSrc ? `**Video URL:** \`${ad.videoUrl ?? ad.videoSrc}\`` : "",
|
|
2924
|
+
ad.variations ?? ad.clusterCount ? `**Variations:** ${ad.variations ?? ad.clusterCount}` : ""
|
|
1659
2925
|
].filter(Boolean).join("\n")).join("\n\n---\n\n");
|
|
1660
2926
|
const full = [
|
|
1661
2927
|
`# Facebook Ad Intel: ${advertiser}`,
|
|
@@ -1681,11 +2947,16 @@ ${adBlocks}`,
|
|
|
1681
2947
|
libraryId: ad.libraryId ?? null,
|
|
1682
2948
|
status: ad.status ?? null,
|
|
1683
2949
|
creativeType: ad.creativeType ?? null,
|
|
2950
|
+
primaryText: ad.primaryText ?? null,
|
|
1684
2951
|
headline: ad.headline ?? null,
|
|
1685
2952
|
cta: ad.cta ?? null,
|
|
1686
|
-
startDate: ad.startDate ?? null,
|
|
1687
|
-
|
|
1688
|
-
|
|
2953
|
+
startDate: ad.startDate ?? ad.started ?? null,
|
|
2954
|
+
landingUrl: ad.landingUrl ?? null,
|
|
2955
|
+
domain: ad.domain ?? null,
|
|
2956
|
+
videoUrl: ad.videoUrl ?? ad.videoSrc ?? null,
|
|
2957
|
+
imageUrl: ad.imageUrl ?? ad.imageSrc ?? null,
|
|
2958
|
+
videoPoster: ad.videoPoster ?? null,
|
|
2959
|
+
variations: typeof ad.variations === "number" ? ad.variations : typeof ad.clusterCount === "number" ? ad.clusterCount : null
|
|
1689
2960
|
}))
|
|
1690
2961
|
}
|
|
1691
2962
|
};
|
|
@@ -1719,8 +2990,11 @@ ${rows}`,
|
|
|
1719
2990
|
advertiserCount: advertisers.length,
|
|
1720
2991
|
advertisers: advertisers.map((a) => ({
|
|
1721
2992
|
name: a.pageName ?? a.name ?? null,
|
|
2993
|
+
pageId: a.pageId ?? null,
|
|
2994
|
+
pageUrl: a.pageUrl ?? null,
|
|
1722
2995
|
adCount: typeof a.adCount === "number" ? a.adCount : null,
|
|
1723
|
-
libraryId: a.sampleLibraryId ?? a.libraryId ?? null
|
|
2996
|
+
libraryId: a.sampleLibraryId ?? a.libraryId ?? null,
|
|
2997
|
+
sampleLibraryId: a.sampleLibraryId ?? null
|
|
1724
2998
|
}))
|
|
1725
2999
|
}
|
|
1726
3000
|
};
|
|
@@ -1818,6 +3092,34 @@ function formatWorkflowSuggest(input) {
|
|
|
1818
3092
|
}
|
|
1819
3093
|
};
|
|
1820
3094
|
}
|
|
3095
|
+
function workflowStepLines(data) {
|
|
3096
|
+
const step = data.step;
|
|
3097
|
+
const nextStep = data.nextStep;
|
|
3098
|
+
const done = data.done === true;
|
|
3099
|
+
const lines = [];
|
|
3100
|
+
if (step) {
|
|
3101
|
+
const index = Number(step.index ?? 0);
|
|
3102
|
+
const totalSteps = step.totalSteps != null ? Number(step.totalSteps) : void 0;
|
|
3103
|
+
const stepLabel = totalSteps ? `${index + 1}/${totalSteps}` : `${index + 1}`;
|
|
3104
|
+
lines.push(`
|
|
3105
|
+
## Step ${stepLabel}: ${step.title ?? step.id ?? ""}`);
|
|
3106
|
+
const output = step.output;
|
|
3107
|
+
if (output && Object.keys(output).length) {
|
|
3108
|
+
lines.push(Object.entries(output).map(([k, v]) => `- ${k}: ${typeof v === "object" ? JSON.stringify(v) : String(v)}`).join("\n"));
|
|
3109
|
+
}
|
|
3110
|
+
const warnings = Array.isArray(step.warnings) ? step.warnings : [];
|
|
3111
|
+
if (warnings.length) lines.push(`
|
|
3112
|
+
**Warnings:**
|
|
3113
|
+
${warnings.map((w) => `- ${w}`).join("\n")}`);
|
|
3114
|
+
}
|
|
3115
|
+
if (done) {
|
|
3116
|
+
lines.push("\n**Done.** All steps complete.");
|
|
3117
|
+
} else if (nextStep && typeof nextStep === "object") {
|
|
3118
|
+
lines.push(`
|
|
3119
|
+
**Next step:** \`${nextStep.id ?? nextStep.index}\` \u2014 call \`workflow_step\` with this run id to continue.`);
|
|
3120
|
+
}
|
|
3121
|
+
return lines;
|
|
3122
|
+
}
|
|
1821
3123
|
function formatWorkflowRun(raw, input) {
|
|
1822
3124
|
const parsed = parseData(raw);
|
|
1823
3125
|
if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
|
|
@@ -1831,6 +3133,7 @@ function formatWorkflowRun(raw, input) {
|
|
|
1831
3133
|
`**Run ID:** \`${runId || "unknown"}\``,
|
|
1832
3134
|
`**Status:** ${status}`,
|
|
1833
3135
|
summary?.title ? `**Title:** ${summary.title}` : "",
|
|
3136
|
+
...workflowStepLines(parsed.data),
|
|
1834
3137
|
summary?.summary ? `
|
|
1835
3138
|
## Summary
|
|
1836
3139
|
${summary.summary}` : "",
|
|
@@ -1846,6 +3149,42 @@ ${workflowArtifactRows(artifacts)}` : "",
|
|
|
1846
3149
|
input: input.input ?? {},
|
|
1847
3150
|
run,
|
|
1848
3151
|
summary,
|
|
3152
|
+
step: parsed.data.step,
|
|
3153
|
+
nextStep: parsed.data.nextStep ?? null,
|
|
3154
|
+
done: parsed.data.done === true,
|
|
3155
|
+
artifacts
|
|
3156
|
+
}
|
|
3157
|
+
};
|
|
3158
|
+
}
|
|
3159
|
+
function formatWorkflowStep(raw, input) {
|
|
3160
|
+
const parsed = parseData(raw);
|
|
3161
|
+
if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
|
|
3162
|
+
const run = parsed.data.run;
|
|
3163
|
+
const summary = parsed.data.summary;
|
|
3164
|
+
const artifacts = workflowArtifactsFrom(run);
|
|
3165
|
+
const done = parsed.data.done === true;
|
|
3166
|
+
const full = [
|
|
3167
|
+
`# Workflow Step`,
|
|
3168
|
+
`**Run ID:** \`${input.runId}\``,
|
|
3169
|
+
`**Status:** ${run?.status ?? (done ? "done" : "running")}`,
|
|
3170
|
+
...workflowStepLines(parsed.data),
|
|
3171
|
+
done && summary?.summary ? `
|
|
3172
|
+
## Summary
|
|
3173
|
+
${summary.summary}` : "",
|
|
3174
|
+
artifacts.length ? `
|
|
3175
|
+
## Artifacts
|
|
3176
|
+
${workflowArtifactRows(artifacts)}` : "",
|
|
3177
|
+
done && artifacts.length ? "\nUse `workflow_artifact_read` with the run id and artifact id to pull CSV, JSON, Markdown, or report content into context." : ""
|
|
3178
|
+
].filter(Boolean).join("\n");
|
|
3179
|
+
return {
|
|
3180
|
+
...oneBlock(full),
|
|
3181
|
+
structuredContent: {
|
|
3182
|
+
runId: input.runId,
|
|
3183
|
+
run,
|
|
3184
|
+
summary: summary ?? null,
|
|
3185
|
+
step: parsed.data.step,
|
|
3186
|
+
nextStep: parsed.data.nextStep ?? null,
|
|
3187
|
+
done,
|
|
1849
3188
|
artifacts
|
|
1850
3189
|
}
|
|
1851
3190
|
};
|
|
@@ -2250,6 +3589,7 @@ function formatFacebookAdTranscribe(raw, input) {
|
|
|
2250
3589
|
const text = d.text ?? "";
|
|
2251
3590
|
const chunks = d.chunks ?? [];
|
|
2252
3591
|
const durSec = d.durationMs ? (d.durationMs / 1e3).toFixed(0) : "\u2014";
|
|
3592
|
+
const words = wordCount(text);
|
|
2253
3593
|
const chunkRows = chunks.slice(0, 50).map((c) => {
|
|
2254
3594
|
const sec = Number.isFinite(c.timestamp[0]) ? Math.floor(c.timestamp[0]) : 0;
|
|
2255
3595
|
const mm = String(Math.floor(sec / 60)).padStart(2, "0");
|
|
@@ -2258,7 +3598,7 @@ function formatFacebookAdTranscribe(raw, input) {
|
|
|
2258
3598
|
}).join("\n");
|
|
2259
3599
|
const full = [
|
|
2260
3600
|
`# Facebook Ad Transcript`,
|
|
2261
|
-
`**Duration:** ${durSec}s \xB7 **${
|
|
3601
|
+
`**Duration:** ${durSec}s \xB7 **${words} words**`,
|
|
2262
3602
|
`
|
|
2263
3603
|
## Full Transcript
|
|
2264
3604
|
${text}`,
|
|
@@ -2271,7 +3611,20 @@ ${chunkRows}` : "",
|
|
|
2271
3611
|
---
|
|
2272
3612
|
\u{1F4A1} Get more ads from this advertiser: use \`facebook_page_intel\`. For public Facebook reel/post URLs, use \`facebook_video_transcribe\`.`
|
|
2273
3613
|
].filter(Boolean).join("\n");
|
|
2274
|
-
return
|
|
3614
|
+
return {
|
|
3615
|
+
...oneBlock(full),
|
|
3616
|
+
structuredContent: {
|
|
3617
|
+
videoUrl: input.videoUrl,
|
|
3618
|
+
wordCount: words,
|
|
3619
|
+
chunkCount: chunks.length,
|
|
3620
|
+
durationMs: typeof d.durationMs === "number" ? d.durationMs : null,
|
|
3621
|
+
transcriptText: text,
|
|
3622
|
+
chunks: structuredTranscriptChunks(chunks),
|
|
3623
|
+
resolvedInputs: {
|
|
3624
|
+
videoUrl: input.videoUrl
|
|
3625
|
+
}
|
|
3626
|
+
}
|
|
3627
|
+
};
|
|
2275
3628
|
}
|
|
2276
3629
|
function formatFacebookVideoTranscribe(raw, input) {
|
|
2277
3630
|
const parsed = parseData(raw);
|
|
@@ -2279,7 +3632,7 @@ function formatFacebookVideoTranscribe(raw, input) {
|
|
|
2279
3632
|
const d = parsed.data;
|
|
2280
3633
|
const text = d.text ?? "";
|
|
2281
3634
|
const chunks = d.chunks ?? [];
|
|
2282
|
-
const
|
|
3635
|
+
const wordCount2 = text.trim() ? text.trim().split(/\s+/).length : 0;
|
|
2283
3636
|
const durSec = d.durationMs ? (d.durationMs / 1e3).toFixed(0) : "\u2014";
|
|
2284
3637
|
const videoDuration = typeof d.videoDurationSec === "number" ? `${Math.round(d.videoDurationSec)}s` : "\u2014";
|
|
2285
3638
|
const label = d.videoId ? `Facebook Organic Video \`${d.videoId}\`` : "Facebook Organic Video";
|
|
@@ -2292,7 +3645,7 @@ function formatFacebookVideoTranscribe(raw, input) {
|
|
|
2292
3645
|
const full = [
|
|
2293
3646
|
`# ${label} Transcript`,
|
|
2294
3647
|
d.ownerName ? `**Owner:** ${d.ownerName}` : "",
|
|
2295
|
-
`**Video duration:** ${videoDuration} \xB7 **Transcribed in:** ${durSec}s \xB7 **${
|
|
3648
|
+
`**Video duration:** ${videoDuration} \xB7 **Transcribed in:** ${durSec}s \xB7 **${wordCount2} words**`,
|
|
2296
3649
|
d.pageUrl ? `**Page URL:** ${d.pageUrl}` : `**Page URL:** ${input.url}`,
|
|
2297
3650
|
d.videoUrl ? `**Extracted MP4:** \`${d.videoUrl}\`` : "",
|
|
2298
3651
|
`
|
|
@@ -2318,7 +3671,7 @@ ${chunkRows}` : "",
|
|
|
2318
3671
|
bitrate: typeof d.bitrate === "number" ? d.bitrate : null,
|
|
2319
3672
|
videoDurationSec: typeof d.videoDurationSec === "number" ? d.videoDurationSec : null,
|
|
2320
3673
|
videoUrl: d.videoUrl ?? "",
|
|
2321
|
-
wordCount,
|
|
3674
|
+
wordCount: wordCount2,
|
|
2322
3675
|
chunkCount: chunks.length,
|
|
2323
3676
|
transcriptText: text,
|
|
2324
3677
|
chunks: chunks.map((c) => ({
|
|
@@ -2331,451 +3684,612 @@ ${chunkRows}` : "",
|
|
|
2331
3684
|
}
|
|
2332
3685
|
|
|
2333
3686
|
// src/mcp/mcp-tool-schemas.ts
|
|
3687
|
+
var import_zod3 = require("zod");
|
|
3688
|
+
|
|
3689
|
+
// src/schemas.ts
|
|
2334
3690
|
var import_zod2 = require("zod");
|
|
3691
|
+
var DEFAULT_PROXY_MODE = "configured";
|
|
3692
|
+
var DEFAULT_MAPS_PROXY_MODE = "location";
|
|
3693
|
+
var HarvestOptionsSchema = import_zod2.z.object({
|
|
3694
|
+
query: import_zod2.z.string().min(1),
|
|
3695
|
+
location: import_zod2.z.string().optional(),
|
|
3696
|
+
gl: import_zod2.z.string().length(2).default("us"),
|
|
3697
|
+
hl: import_zod2.z.string().length(2).default("en"),
|
|
3698
|
+
device: import_zod2.z.enum(["desktop", "mobile"]).default("desktop"),
|
|
3699
|
+
proxyMode: import_zod2.z.enum(["location", "configured", "none"]).default(DEFAULT_PROXY_MODE),
|
|
3700
|
+
proxyZip: import_zod2.z.string().regex(/^\d{5}$/).optional(),
|
|
3701
|
+
debug: import_zod2.z.boolean().default(false),
|
|
3702
|
+
depth: import_zod2.z.number().int().min(1).max(30).default(3),
|
|
3703
|
+
maxQuestions: import_zod2.z.number().int().min(1).max(1e3).default(100),
|
|
3704
|
+
headless: import_zod2.z.boolean().default(false),
|
|
3705
|
+
profileDir: import_zod2.z.string().optional(),
|
|
3706
|
+
proxy: import_zod2.z.string().url().optional(),
|
|
3707
|
+
kernelApiKey: import_zod2.z.string().optional(),
|
|
3708
|
+
kernelProxyId: import_zod2.z.string().optional(),
|
|
3709
|
+
kernelProxyResolution: import_zod2.z.unknown().optional(),
|
|
3710
|
+
outputDir: import_zod2.z.string().default("./paa-output"),
|
|
3711
|
+
format: import_zod2.z.enum(["json", "csv", "both"]).default("both"),
|
|
3712
|
+
serpOnly: import_zod2.z.boolean().default(false),
|
|
3713
|
+
pages: import_zod2.z.number().int().min(1).max(2).default(1)
|
|
3714
|
+
});
|
|
3715
|
+
var MapsPlaceOptionsSchema = import_zod2.z.object({
|
|
3716
|
+
businessName: import_zod2.z.string().min(1),
|
|
3717
|
+
location: import_zod2.z.string().min(1),
|
|
3718
|
+
gl: import_zod2.z.string().length(2).default("us"),
|
|
3719
|
+
hl: import_zod2.z.string().length(2).default("en"),
|
|
3720
|
+
includeReviews: import_zod2.z.boolean().default(false),
|
|
3721
|
+
maxReviews: import_zod2.z.number().int().min(1).max(500).default(50),
|
|
3722
|
+
kernelApiKey: import_zod2.z.string().optional(),
|
|
3723
|
+
kernelProxyId: import_zod2.z.string().optional(),
|
|
3724
|
+
headless: import_zod2.z.boolean().default(true)
|
|
3725
|
+
});
|
|
3726
|
+
var MapsSearchOptionsSchema = import_zod2.z.object({
|
|
3727
|
+
query: import_zod2.z.string().min(1),
|
|
3728
|
+
location: import_zod2.z.string().optional(),
|
|
3729
|
+
gl: import_zod2.z.string().length(2).default("us"),
|
|
3730
|
+
hl: import_zod2.z.string().length(2).default("en"),
|
|
3731
|
+
maxResults: import_zod2.z.number().int().min(1).max(50).default(10),
|
|
3732
|
+
proxyMode: import_zod2.z.enum(["location", "configured", "none"]).default(DEFAULT_MAPS_PROXY_MODE),
|
|
3733
|
+
proxyZip: import_zod2.z.string().regex(/^\d{5}$/).optional(),
|
|
3734
|
+
debug: import_zod2.z.boolean().default(false),
|
|
3735
|
+
kernelApiKey: import_zod2.z.string().optional(),
|
|
3736
|
+
kernelProxyId: import_zod2.z.string().optional(),
|
|
3737
|
+
kernelProxyResolution: import_zod2.z.unknown().optional(),
|
|
3738
|
+
headless: import_zod2.z.boolean().default(true)
|
|
3739
|
+
});
|
|
3740
|
+
var RawPAAItemSchema = import_zod2.z.object({
|
|
3741
|
+
question: import_zod2.z.string().min(1),
|
|
3742
|
+
answer: import_zod2.z.string().optional(),
|
|
3743
|
+
sourceTitle: import_zod2.z.string().optional(),
|
|
3744
|
+
sourceSite: import_zod2.z.string().optional(),
|
|
3745
|
+
sourceCite: import_zod2.z.string().optional()
|
|
3746
|
+
});
|
|
3747
|
+
var RawMapsOverviewSchema = import_zod2.z.object({
|
|
3748
|
+
name: import_zod2.z.string().nullable(),
|
|
3749
|
+
rating: import_zod2.z.string().nullable(),
|
|
3750
|
+
reviewCount: import_zod2.z.string().nullable(),
|
|
3751
|
+
category: import_zod2.z.string().nullable(),
|
|
3752
|
+
address: import_zod2.z.string().nullable(),
|
|
3753
|
+
hoursSummary: import_zod2.z.string().nullable(),
|
|
3754
|
+
phone: import_zod2.z.string().nullable(),
|
|
3755
|
+
phoneDisplay: import_zod2.z.string().nullable(),
|
|
3756
|
+
website: import_zod2.z.string().nullable(),
|
|
3757
|
+
plusCode: import_zod2.z.string().nullable(),
|
|
3758
|
+
bookingUrl: import_zod2.z.string().nullable()
|
|
3759
|
+
});
|
|
3760
|
+
var RawMapsHoursRowSchema = import_zod2.z.object({
|
|
3761
|
+
day: import_zod2.z.string(),
|
|
3762
|
+
hours: import_zod2.z.string()
|
|
3763
|
+
});
|
|
3764
|
+
var RawMapsReviewStatsSchema = import_zod2.z.object({
|
|
3765
|
+
reviewHistogram: import_zod2.z.array(import_zod2.z.object({
|
|
3766
|
+
stars: import_zod2.z.number(),
|
|
3767
|
+
count: import_zod2.z.string()
|
|
3768
|
+
})),
|
|
3769
|
+
reviewTopics: import_zod2.z.array(import_zod2.z.object({
|
|
3770
|
+
label: import_zod2.z.string(),
|
|
3771
|
+
count: import_zod2.z.string()
|
|
3772
|
+
}))
|
|
3773
|
+
});
|
|
3774
|
+
var RawMapsReviewCardSchema = import_zod2.z.object({
|
|
3775
|
+
reviewId: import_zod2.z.string(),
|
|
3776
|
+
author: import_zod2.z.string().nullable(),
|
|
3777
|
+
stars: import_zod2.z.string().nullable(),
|
|
3778
|
+
date: import_zod2.z.string().nullable(),
|
|
3779
|
+
text: import_zod2.z.string().nullable(),
|
|
3780
|
+
ownerResponse: import_zod2.z.string().nullable()
|
|
3781
|
+
});
|
|
3782
|
+
var RawMapsAboutAttributeSchema = import_zod2.z.object({
|
|
3783
|
+
section: import_zod2.z.string(),
|
|
3784
|
+
attribute: import_zod2.z.string()
|
|
3785
|
+
});
|
|
3786
|
+
|
|
3787
|
+
// src/mcp/mcp-tool-schemas.ts
|
|
2335
3788
|
var HarvestPaaInputSchema = {
|
|
2336
|
-
query:
|
|
2337
|
-
location:
|
|
2338
|
-
maxQuestions:
|
|
2339
|
-
gl:
|
|
2340
|
-
hl:
|
|
2341
|
-
device:
|
|
2342
|
-
proxyMode:
|
|
2343
|
-
proxyZip:
|
|
2344
|
-
debug:
|
|
3789
|
+
query: import_zod3.z.string().min(1).describe('Core search topic only. If the user says "best hvac company in Denver CO", use query="best hvac company" and location="Denver, CO". Do not include the location in query when it can be separated.'),
|
|
3790
|
+
location: import_zod3.z.string().optional().describe('City, region, or country for geo-targeted results, inferred from the user request when present, e.g. "Denver, CO", "Tokyo, Japan", "London, UK".'),
|
|
3791
|
+
maxQuestions: import_zod3.z.number().int().min(1).max(200).default(30).describe("Number of PAA questions to extract. Default 30. Maximum 200. Use 10 for quick probes, 30 for normal research, 100-200 when the user asks for everything/full/deep research. Larger harvests get a longer server time budget (151-200 questions \u2192 up to 280s). Credits are charged by extracted question; unused request hold is refunded."),
|
|
3792
|
+
gl: import_zod3.z.string().length(2).default("us").describe("Google country code inferred from location or user language. Examples: United States us, United Kingdom gb, Japan jp, Canada ca, Australia au."),
|
|
3793
|
+
hl: import_zod3.z.string().default("en").describe("Google interface/content language inferred from the user request. Use en unless the user asks for another language or locale."),
|
|
3794
|
+
device: import_zod3.z.enum(["desktop", "mobile"]).default("desktop").describe("SERP device context. Use desktop by default; use mobile only when the user asks for mobile rankings."),
|
|
3795
|
+
proxyMode: import_zod3.z.enum(["location", "configured", "none"]).default(DEFAULT_PROXY_MODE).describe("Proxy targeting mode. Default configured uses the service proxy without city/ZIP targeting for the highest general success rate. Use location only when the user explicitly needs city/ZIP-targeted residential proxy evidence. Use none only for direct-network debugging."),
|
|
3796
|
+
proxyZip: import_zod3.z.string().regex(/^\d{5}$/).optional().describe("Optional US ZIP override for residential location proxy targeting. Use only with proxyMode location when the user gives a specific ZIP or city-center targeting needs to be forced."),
|
|
3797
|
+
debug: import_zod3.z.boolean().default(false).describe("Include sanitized browser/session/location diagnostics in the response. Use true when debugging localization, CAPTCHA, or proxy behavior.")
|
|
2345
3798
|
};
|
|
2346
3799
|
var ExtractUrlInputSchema = {
|
|
2347
|
-
url:
|
|
2348
|
-
screenshot:
|
|
2349
|
-
screenshotDevice:
|
|
2350
|
-
extractBranding:
|
|
2351
|
-
downloadMedia:
|
|
2352
|
-
mediaTypes:
|
|
2353
|
-
allowLocal:
|
|
3800
|
+
url: import_zod3.z.string().url().describe("Public http/https URL to extract. Use this when the user provides one specific page URL."),
|
|
3801
|
+
screenshot: import_zod3.z.boolean().default(false).describe("Also capture a full-page screenshot of the URL. Saved to ~/Downloads/mcp-scraper/screenshots/ and returned inline. Use when the user asks to see or capture the page visually."),
|
|
3802
|
+
screenshotDevice: import_zod3.z.enum(["desktop", "mobile"]).default("desktop").describe("Viewport for screenshot. desktop = 1440\xD7900. mobile = 390\xD7844. Default desktop."),
|
|
3803
|
+
extractBranding: import_zod3.z.boolean().default(false).describe("Extract brand colors, fonts, logo, and favicon using a rendered browser session. Returns colorScheme (light/dark), colors (primary/accent/background/text/heading as hex), fonts (heading/body family names), and assets (logo URL, favicon URL). Use when the user asks about brand colors, site theme, or brand assets."),
|
|
3804
|
+
downloadMedia: import_zod3.z.boolean().default(false).describe("Extract and download all page media (images, video, audio) to ~/Downloads/mcp-scraper/media/. Ad networks, tracking pixels, and noise URLs are filtered automatically. Use when the user asks to download or harvest assets from a page."),
|
|
3805
|
+
mediaTypes: import_zod3.z.array(import_zod3.z.enum(["image", "video", "audio"])).default(["image", "video", "audio"]).describe("Which media types to download. Default all three."),
|
|
3806
|
+
allowLocal: import_zod3.z.boolean().default(false).describe("Allow localhost and private-network URLs. For local development only.")
|
|
2354
3807
|
};
|
|
2355
3808
|
var MapSiteUrlsInputSchema = {
|
|
2356
|
-
url:
|
|
2357
|
-
maxUrls:
|
|
3809
|
+
url: import_zod3.z.string().url().describe("Public website URL or domain to crawl for internal URLs. Use before extract_site when the user asks to audit/map/crawl a site."),
|
|
3810
|
+
maxUrls: import_zod3.z.number().int().min(1).max(500).optional().describe("Maximum URLs to discover. Use 100 for normal maps, higher when the user asks for a full inventory.")
|
|
2358
3811
|
};
|
|
2359
3812
|
var ExtractSiteInputSchema = {
|
|
2360
|
-
url:
|
|
2361
|
-
maxPages:
|
|
3813
|
+
url: import_zod3.z.string().url().describe("Public website URL or domain to extract across multiple pages. Use when the user asks for a site audit, website crawl, or full-site content/schema extraction."),
|
|
3814
|
+
maxPages: import_zod3.z.number().int().min(1).max(50).optional().describe("Maximum pages to extract. Use 50 when the user asks for full results or a complete crawl within MCP limits.")
|
|
2362
3815
|
};
|
|
2363
3816
|
var YoutubeHarvestInputSchema = {
|
|
2364
|
-
mode:
|
|
2365
|
-
query:
|
|
2366
|
-
channelHandle:
|
|
2367
|
-
maxVideos:
|
|
3817
|
+
mode: import_zod3.z.enum(["search", "channel"]).describe("Use search for topic/keyword requests. Use channel when the user provides @handle, channel ID, or channel URL."),
|
|
3818
|
+
query: import_zod3.z.string().optional().describe("Required when mode is search. The YouTube search topic in the user\u2019s words."),
|
|
3819
|
+
channelHandle: import_zod3.z.string().optional().describe("YouTube channel handle, channel ID, or URL. Examples: @mkbhd, UC..., https://youtube.com/@mkbhd."),
|
|
3820
|
+
maxVideos: import_zod3.z.number().int().min(1).max(500).default(50).describe("Number of videos to return. Default 50, maximum 500. Use 10-25 for quick topic discovery, 50 for normal channel/search harvests, and larger values only when the user asks for full channel/history because large responses consume more context.")
|
|
2368
3821
|
};
|
|
2369
3822
|
var YoutubeTranscribeInputSchema = {
|
|
2370
|
-
videoId:
|
|
3823
|
+
videoId: import_zod3.z.string().min(1).optional().describe("YouTube video ID, e.g. dQw4w9WgXcQ. Use only an ID returned by youtube_harvest or visible in a YouTube URL; do not invent one."),
|
|
3824
|
+
url: import_zod3.z.string().url().optional().describe("Full YouTube URL, e.g. https://www.youtube.com/watch?v=dQw4w9WgXcQ or https://youtu.be/dQw4w9WgXcQ. Use this when the user pasted a URL instead of an ID. Provide videoId or url.")
|
|
2371
3825
|
};
|
|
2372
3826
|
var FacebookPageIntelInputSchema = {
|
|
2373
|
-
pageId:
|
|
2374
|
-
libraryId:
|
|
2375
|
-
query:
|
|
2376
|
-
maxAds:
|
|
2377
|
-
country:
|
|
3827
|
+
pageId: import_zod3.z.string().optional().describe("Facebook advertiser/page ID. Use only a pageId returned by facebook_ad_search/facebook_page_intel or copied from Facebook Ad Library; do not construct one yourself."),
|
|
3828
|
+
libraryId: import_zod3.z.string().optional().describe("Facebook Ad Library archive ID for a known ad or advertiser sample. Use a libraryId returned by facebook_ad_search, or a libraryId/adArchiveId visible in Ad Library."),
|
|
3829
|
+
query: import_zod3.z.string().optional().describe("Advertiser or brand name when pageId/libraryId is not known. One of pageId, libraryId, or query is required."),
|
|
3830
|
+
maxAds: import_zod3.z.number().int().min(1).max(200).default(50).describe("Maximum ads to inspect. Default 50, maximum 200. Prefer 25-50 for focused advertiser scans; use 100-200 only when the user asks for a broad ad archive sweep."),
|
|
3831
|
+
country: import_zod3.z.string().length(2).default("US").describe("Two-letter Ad Library country code. Default US. Examples: US, CA, GB, AU. Infer from the user request when they name a country.")
|
|
2378
3832
|
};
|
|
2379
3833
|
var FacebookAdSearchInputSchema = {
|
|
2380
|
-
query:
|
|
2381
|
-
country:
|
|
2382
|
-
maxResults:
|
|
3834
|
+
query: import_zod3.z.string().min(1).describe("Advertiser, brand, competitor, niche, or keyword to search in Facebook Ad Library."),
|
|
3835
|
+
country: import_zod3.z.string().length(2).default("US").describe("Two-letter Ad Library country code. Default US. Examples: US, CA, GB, AU."),
|
|
3836
|
+
maxResults: import_zod3.z.number().int().min(1).max(20).default(10).describe("Maximum advertisers to return. Default 10, maximum 20. Prefer tighter search terms over maxing this out.")
|
|
2383
3837
|
};
|
|
2384
3838
|
var FacebookAdTranscribeInputSchema = {
|
|
2385
|
-
videoUrl:
|
|
3839
|
+
videoUrl: import_zod3.z.string().url().describe("Direct Facebook CDN video URL from a facebook_page_intel ad result. Do not pass a public Facebook reel/post/share URL here; use facebook_video_transcribe for organic Facebook URLs.")
|
|
2386
3840
|
};
|
|
2387
3841
|
var FacebookVideoTranscribeInputSchema = {
|
|
2388
|
-
url:
|
|
2389
|
-
quality:
|
|
3842
|
+
url: import_zod3.z.string().url().describe("Organic Facebook reel, video, watch, post, or share URL from facebook.com, m.facebook.com, or fb.watch. The tool renders the page, extracts the best matching public Facebook CDN MP4 URL, then transcribes it. Use this when the user pastes a normal Facebook video page URL and asks for the transcript or downloadable MP4."),
|
|
3843
|
+
quality: import_zod3.z.enum(["best", "hd", "sd"]).default("best").describe("Preferred progressive MP4 quality. Use best by default; hd prefers the highest HD progressive URL; sd forces the SD URL.")
|
|
2390
3844
|
};
|
|
2391
3845
|
var MapsPlaceIntelInputSchema = {
|
|
2392
|
-
businessName:
|
|
2393
|
-
location:
|
|
2394
|
-
gl:
|
|
2395
|
-
hl:
|
|
2396
|
-
includeReviews:
|
|
2397
|
-
maxReviews:
|
|
3846
|
+
businessName: import_zod3.z.string().min(1).describe('Business name only. If user says "Elite Roofing Denver CO", use businessName="Elite Roofing" and location="Denver, CO".'),
|
|
3847
|
+
location: import_zod3.z.string().min(1).describe('City/region/country where the business should be searched, e.g. "Denver, CO". Infer from the user request when possible.'),
|
|
3848
|
+
gl: import_zod3.z.string().length(2).default("us").describe("Google country code inferred from location."),
|
|
3849
|
+
hl: import_zod3.z.string().length(2).default("en").describe("Language inferred from user request."),
|
|
3850
|
+
includeReviews: import_zod3.z.boolean().default(false).describe("Whether to fetch individual review cards. Use true when the user asks for reviews, customer pain, complaints, praise, themes, or review evidence."),
|
|
3851
|
+
maxReviews: import_zod3.z.number().int().min(1).max(500).default(50).describe("Max review cards to return when includeReviews is true. Default 50, maximum 500. Use 50 for normal review analysis and larger values only for deep review mining.")
|
|
2398
3852
|
};
|
|
2399
3853
|
var MapsSearchInputSchema = {
|
|
2400
|
-
query:
|
|
2401
|
-
location:
|
|
2402
|
-
gl:
|
|
2403
|
-
hl:
|
|
2404
|
-
maxResults:
|
|
2405
|
-
proxyMode:
|
|
2406
|
-
proxyZip:
|
|
2407
|
-
debug:
|
|
3854
|
+
query: import_zod3.z.string().min(1).describe('Business category, niche, keyword, or search term. If the user says "roofers in Denver CO", use query="roofers" and location="Denver, CO". Do not put the location here when it can be separated.'),
|
|
3855
|
+
location: import_zod3.z.string().optional().describe('City, region, country, or service area for the Maps search, e.g. "Denver, CO". Infer from the user request when present.'),
|
|
3856
|
+
gl: import_zod3.z.string().length(2).default("us").describe("Google country code inferred from location."),
|
|
3857
|
+
hl: import_zod3.z.string().length(2).default("en").describe("Language inferred from user request."),
|
|
3858
|
+
maxResults: import_zod3.z.number().int().min(1).max(50).default(10).describe("Number of Google Maps business/profile candidates to return. Default 10. Maximum 50. Use 10 unless the user asks for more."),
|
|
3859
|
+
proxyMode: import_zod3.z.enum(["location", "configured", "none"]).default(DEFAULT_MAPS_PROXY_MODE).describe("Proxy targeting mode. Maps defaults to location so local market searches get city/state residential proxy targeting and rotation. Use configured to force the service proxy without city/ZIP targeting, and none only for local direct-network debugging."),
|
|
3860
|
+
proxyZip: import_zod3.z.string().regex(/^\d{5}$/).optional().describe("Optional US ZIP override for residential location proxy targeting. Use when the user gives a specific ZIP or city-center ZIP."),
|
|
3861
|
+
debug: import_zod3.z.boolean().default(false).describe("Include sanitized browser/proxy diagnostics when debugging Maps localization, CAPTCHA, or proxy behavior.")
|
|
2408
3862
|
};
|
|
2409
3863
|
var DirectoryWorkflowInputSchema = {
|
|
2410
|
-
query:
|
|
2411
|
-
state:
|
|
2412
|
-
minPopulation:
|
|
2413
|
-
populationYear:
|
|
2414
|
-
maxCities:
|
|
2415
|
-
maxResultsPerCity:
|
|
2416
|
-
concurrency:
|
|
2417
|
-
includeZipGroups:
|
|
2418
|
-
usZipsCsvPath:
|
|
2419
|
-
saveCsv:
|
|
2420
|
-
proxyMode:
|
|
2421
|
-
proxyZip:
|
|
2422
|
-
debug:
|
|
2423
|
-
};
|
|
2424
|
-
var RankTrackerModeSchema =
|
|
3864
|
+
query: import_zod3.z.string().min(1).describe("Business category, niche, or keyword to search on Google Maps for every selected market, e.g. roofers, dentists, med spas. Do not include the city here."),
|
|
3865
|
+
state: import_zod3.z.string().min(2).default("TN").describe("US state abbreviation or state name used to select Census places, e.g. TN or Tennessee."),
|
|
3866
|
+
minPopulation: import_zod3.z.number().int().min(0).default(1e5).describe('Minimum Census place population for market selection. Use 100000 for "cities above 100k population".'),
|
|
3867
|
+
populationYear: import_zod3.z.number().int().min(2020).max(2025).default(2025).describe("Census population estimate year from the 2020-2025 Population Estimates Program city/place dataset."),
|
|
3868
|
+
maxCities: import_zod3.z.number().int().min(1).max(100).default(25).describe("Maximum number of markets to process after sorting by population descending."),
|
|
3869
|
+
maxResultsPerCity: import_zod3.z.number().int().min(1).max(50).default(50).describe("Google Maps business/profile candidates to collect for each city. Maximum 50."),
|
|
3870
|
+
concurrency: import_zod3.z.number().int().min(1).max(5).default(5).describe("How many city Maps searches to run in parallel. Use 5 for broad directory batches unless debugging."),
|
|
3871
|
+
includeZipGroups: import_zod3.z.boolean().default(true).describe("Attach ZIP groups from a configured US ZIPS CSV when available. Set MCP_SCRAPER_USZIPS_CSV_PATH on the API server or pass usZipsCsvPath in local/test mode."),
|
|
3872
|
+
usZipsCsvPath: import_zod3.z.string().optional().describe("Local/test-only path to a US ZIPS CSV with state_abbr, zipcode, county, city columns, such as Lead Magician tools/analytics/data/uszips.csv. Deployed APIs should use MCP_SCRAPER_USZIPS_CSV_PATH instead."),
|
|
3873
|
+
saveCsv: import_zod3.z.boolean().default(true).describe("Save a directory-ready CSV to the MCP Scraper output directory and return its path. CSV rows include source_location, result_position, business_name, review_stars, review_count, category, address, phone, hours_status, website_url, directions_url, place_url, CID fields, population, and ZIP groups."),
|
|
3874
|
+
proxyMode: import_zod3.z.enum(["location", "configured", "none"]).default(DEFAULT_MAPS_PROXY_MODE).describe("Proxy targeting mode for every city Maps search. Maps workflows default to location so each city can use city/state or ZIP-group residential proxy targeting. Use configured to force the service proxy without city/ZIP targeting, and none only for local direct-network debugging."),
|
|
3875
|
+
proxyZip: import_zod3.z.string().regex(/^\d{5}$/).optional().describe("Optional ZIP override for proxy targeting. Normally omit it so each city can use its ZIP group or city/state location."),
|
|
3876
|
+
debug: import_zod3.z.boolean().default(false).describe("Include sanitized browser/proxy diagnostics in each Maps browser session when supported.")
|
|
3877
|
+
};
|
|
3878
|
+
var RankTrackerModeSchema = import_zod3.z.enum(["maps", "organic", "ai_overview", "paa"]);
|
|
2425
3879
|
var RankTrackerBlueprintInputSchema = {
|
|
2426
|
-
projectName:
|
|
2427
|
-
targetDomain:
|
|
2428
|
-
targetBusinessName:
|
|
2429
|
-
trackingModes:
|
|
2430
|
-
keywords:
|
|
2431
|
-
locations:
|
|
2432
|
-
competitors:
|
|
2433
|
-
database:
|
|
2434
|
-
scheduleCadence:
|
|
2435
|
-
customCron:
|
|
2436
|
-
timezone:
|
|
2437
|
-
includeCron:
|
|
2438
|
-
includeDashboard:
|
|
2439
|
-
includeAlerts:
|
|
2440
|
-
notes:
|
|
2441
|
-
};
|
|
2442
|
-
var
|
|
2443
|
-
var MapsSearchAttemptOutput =
|
|
2444
|
-
attemptNumber:
|
|
2445
|
-
maxAttempts:
|
|
2446
|
-
status:
|
|
2447
|
-
outcome:
|
|
2448
|
-
willRetry:
|
|
2449
|
-
durationMs:
|
|
2450
|
-
resultCount:
|
|
2451
|
-
error:
|
|
2452
|
-
proxyMode:
|
|
2453
|
-
proxyResolutionSource:
|
|
2454
|
-
proxyIdSuffix:
|
|
2455
|
-
proxyTargetLevel:
|
|
2456
|
-
proxyTargetLocation:
|
|
2457
|
-
proxyTargetZip:
|
|
2458
|
-
browserSessionIdSuffix:
|
|
2459
|
-
observedIp:
|
|
2460
|
-
observedCity:
|
|
2461
|
-
observedRegion:
|
|
3880
|
+
projectName: import_zod3.z.string().min(1).optional().describe("Optional name for the rank tracker project, client, or campaign."),
|
|
3881
|
+
targetDomain: import_zod3.z.string().min(1).optional().describe("Primary domain to track in organic results, AI Overview citations, and PAA sources, e.g. example.com."),
|
|
3882
|
+
targetBusinessName: import_zod3.z.string().min(1).optional().describe("Primary Google Business Profile or brand/business name to match in Maps results. Required for reliable Maps rank tracking."),
|
|
3883
|
+
trackingModes: import_zod3.z.array(RankTrackerModeSchema).min(1).max(4).default(["maps", "organic", "ai_overview", "paa"]).describe("Rank tracker surfaces to build. maps uses directory_workflow/maps_search. organic uses search_serp. ai_overview uses search_serp/harvest_paa. paa uses harvest_paa source presence."),
|
|
3884
|
+
keywords: import_zod3.z.array(import_zod3.z.string().min(1)).max(200).default([]).describe("Seed keywords or service queries to track. Leave empty when the downstream AI should create the keyword table from user input."),
|
|
3885
|
+
locations: import_zod3.z.array(import_zod3.z.string().min(1)).max(100).default([]).describe("Markets, cities, ZIPs, or service areas to track. Use city/state strings like Denver, CO for localized SERP and Maps checks."),
|
|
3886
|
+
competitors: import_zod3.z.array(import_zod3.z.string().min(1)).max(100).default([]).describe("Optional competitor domains or business names to persist as comparison targets."),
|
|
3887
|
+
database: import_zod3.z.enum(["postgres", "neon", "supabase", "sqlite", "mysql"]).default("postgres").describe("Database family the downstream AI should target when generating migrations."),
|
|
3888
|
+
scheduleCadence: import_zod3.z.enum(["daily", "weekly", "monthly", "custom"]).default("weekly").describe("Default recurring rank check cadence for the generated cron/heartbeat plan."),
|
|
3889
|
+
customCron: import_zod3.z.string().min(1).optional().describe("Cron expression to use when scheduleCadence is custom."),
|
|
3890
|
+
timezone: import_zod3.z.string().min(1).default("UTC").describe("IANA timezone for scheduled rank checks, e.g. America/Denver."),
|
|
3891
|
+
includeCron: import_zod3.z.boolean().default(true).describe("Include a cron or heartbeat worker plan. Keep true for production rank trackers."),
|
|
3892
|
+
includeDashboard: import_zod3.z.boolean().default(true).describe("Include dashboard/reporting requirements in the generated prompt."),
|
|
3893
|
+
includeAlerts: import_zod3.z.boolean().default(true).describe("Include alert rules for rank movement and SERP feature gains/losses."),
|
|
3894
|
+
notes: import_zod3.z.string().max(4e3).optional().describe("Extra product, client, stack, or hosting requirements to include in the implementation prompt.")
|
|
3895
|
+
};
|
|
3896
|
+
var NullableString2 = import_zod3.z.string().nullable();
|
|
3897
|
+
var MapsSearchAttemptOutput = import_zod3.z.object({
|
|
3898
|
+
attemptNumber: import_zod3.z.number().int().min(1),
|
|
3899
|
+
maxAttempts: import_zod3.z.number().int().min(1),
|
|
3900
|
+
status: import_zod3.z.enum(["ok", "failed"]),
|
|
3901
|
+
outcome: import_zod3.z.string(),
|
|
3902
|
+
willRetry: import_zod3.z.boolean(),
|
|
3903
|
+
durationMs: import_zod3.z.number().int().min(0),
|
|
3904
|
+
resultCount: import_zod3.z.number().int().min(0),
|
|
3905
|
+
error: NullableString2,
|
|
3906
|
+
proxyMode: import_zod3.z.enum(["location", "configured", "none"]),
|
|
3907
|
+
proxyResolutionSource: import_zod3.z.enum(["disabled", "location_reused", "location_created", "configured_fallback", "unavailable"]).nullable(),
|
|
3908
|
+
proxyIdSuffix: NullableString2,
|
|
3909
|
+
proxyTargetLevel: import_zod3.z.enum(["zip", "city", "state"]).nullable(),
|
|
3910
|
+
proxyTargetLocation: NullableString2,
|
|
3911
|
+
proxyTargetZip: NullableString2,
|
|
3912
|
+
browserSessionIdSuffix: NullableString2,
|
|
3913
|
+
observedIp: NullableString2,
|
|
3914
|
+
observedCity: NullableString2,
|
|
3915
|
+
observedRegion: NullableString2
|
|
2462
3916
|
});
|
|
2463
3917
|
var MapsSearchOutputSchema = {
|
|
2464
|
-
query:
|
|
2465
|
-
location:
|
|
2466
|
-
searchQuery:
|
|
2467
|
-
searchUrl:
|
|
2468
|
-
extractedAt:
|
|
2469
|
-
requestedMaxResults:
|
|
2470
|
-
resultCount:
|
|
2471
|
-
results:
|
|
2472
|
-
position:
|
|
2473
|
-
name:
|
|
2474
|
-
placeUrl:
|
|
2475
|
-
cid:
|
|
2476
|
-
cidDecimal:
|
|
2477
|
-
rating:
|
|
2478
|
-
reviewCount:
|
|
2479
|
-
category:
|
|
2480
|
-
address:
|
|
2481
|
-
phone:
|
|
2482
|
-
hoursStatus:
|
|
2483
|
-
websiteUrl:
|
|
2484
|
-
directionsUrl:
|
|
2485
|
-
metadata:
|
|
3918
|
+
query: import_zod3.z.string(),
|
|
3919
|
+
location: import_zod3.z.string().nullable(),
|
|
3920
|
+
searchQuery: import_zod3.z.string(),
|
|
3921
|
+
searchUrl: import_zod3.z.string().url(),
|
|
3922
|
+
extractedAt: import_zod3.z.string(),
|
|
3923
|
+
requestedMaxResults: import_zod3.z.number().int().min(1).max(50),
|
|
3924
|
+
resultCount: import_zod3.z.number().int().min(0).max(50),
|
|
3925
|
+
results: import_zod3.z.array(import_zod3.z.object({
|
|
3926
|
+
position: import_zod3.z.number().int().min(1),
|
|
3927
|
+
name: import_zod3.z.string(),
|
|
3928
|
+
placeUrl: import_zod3.z.string().url(),
|
|
3929
|
+
cid: NullableString2,
|
|
3930
|
+
cidDecimal: NullableString2,
|
|
3931
|
+
rating: NullableString2,
|
|
3932
|
+
reviewCount: NullableString2,
|
|
3933
|
+
category: NullableString2,
|
|
3934
|
+
address: NullableString2,
|
|
3935
|
+
phone: NullableString2,
|
|
3936
|
+
hoursStatus: NullableString2,
|
|
3937
|
+
websiteUrl: NullableString2,
|
|
3938
|
+
directionsUrl: NullableString2,
|
|
3939
|
+
metadata: import_zod3.z.array(import_zod3.z.string())
|
|
2486
3940
|
})),
|
|
2487
|
-
attempts:
|
|
2488
|
-
durationMs:
|
|
2489
|
-
};
|
|
2490
|
-
var DirectoryMapsBusinessOutput =
|
|
2491
|
-
position:
|
|
2492
|
-
name:
|
|
2493
|
-
placeUrl:
|
|
2494
|
-
cid:
|
|
2495
|
-
cidDecimal:
|
|
2496
|
-
rating:
|
|
2497
|
-
reviewCount:
|
|
2498
|
-
category:
|
|
2499
|
-
address:
|
|
2500
|
-
phone:
|
|
2501
|
-
hoursStatus:
|
|
2502
|
-
websiteUrl:
|
|
2503
|
-
directionsUrl:
|
|
2504
|
-
metadata:
|
|
3941
|
+
attempts: import_zod3.z.array(MapsSearchAttemptOutput),
|
|
3942
|
+
durationMs: import_zod3.z.number().int().min(0)
|
|
3943
|
+
};
|
|
3944
|
+
var DirectoryMapsBusinessOutput = import_zod3.z.object({
|
|
3945
|
+
position: import_zod3.z.number().int().min(1),
|
|
3946
|
+
name: import_zod3.z.string(),
|
|
3947
|
+
placeUrl: import_zod3.z.string().url(),
|
|
3948
|
+
cid: NullableString2,
|
|
3949
|
+
cidDecimal: NullableString2,
|
|
3950
|
+
rating: NullableString2,
|
|
3951
|
+
reviewCount: NullableString2,
|
|
3952
|
+
category: NullableString2,
|
|
3953
|
+
address: NullableString2,
|
|
3954
|
+
phone: NullableString2,
|
|
3955
|
+
hoursStatus: NullableString2,
|
|
3956
|
+
websiteUrl: NullableString2,
|
|
3957
|
+
directionsUrl: NullableString2,
|
|
3958
|
+
metadata: import_zod3.z.array(import_zod3.z.string())
|
|
2505
3959
|
});
|
|
2506
3960
|
var DirectoryWorkflowOutputSchema = {
|
|
2507
|
-
query:
|
|
2508
|
-
state:
|
|
2509
|
-
minPopulation:
|
|
2510
|
-
populationYear:
|
|
2511
|
-
maxResultsPerCity:
|
|
2512
|
-
concurrency:
|
|
2513
|
-
censusSourceUrl:
|
|
2514
|
-
usZipsSourcePath:
|
|
2515
|
-
warnings:
|
|
2516
|
-
extractedAt:
|
|
2517
|
-
selectedCityCount:
|
|
2518
|
-
totalResultCount:
|
|
2519
|
-
csvPath:
|
|
2520
|
-
cities:
|
|
2521
|
-
city:
|
|
2522
|
-
state:
|
|
2523
|
-
location:
|
|
2524
|
-
cityKey:
|
|
2525
|
-
censusName:
|
|
2526
|
-
population:
|
|
2527
|
-
populationYear:
|
|
2528
|
-
zips:
|
|
2529
|
-
counties:
|
|
2530
|
-
status:
|
|
2531
|
-
error:
|
|
2532
|
-
resultCount:
|
|
2533
|
-
durationMs:
|
|
2534
|
-
attempts:
|
|
2535
|
-
results:
|
|
3961
|
+
query: import_zod3.z.string(),
|
|
3962
|
+
state: import_zod3.z.string(),
|
|
3963
|
+
minPopulation: import_zod3.z.number().int().min(0),
|
|
3964
|
+
populationYear: import_zod3.z.number().int().min(2020).max(2025),
|
|
3965
|
+
maxResultsPerCity: import_zod3.z.number().int().min(1).max(50),
|
|
3966
|
+
concurrency: import_zod3.z.number().int().min(1).max(5),
|
|
3967
|
+
censusSourceUrl: import_zod3.z.string().url(),
|
|
3968
|
+
usZipsSourcePath: NullableString2,
|
|
3969
|
+
warnings: import_zod3.z.array(import_zod3.z.string()),
|
|
3970
|
+
extractedAt: import_zod3.z.string(),
|
|
3971
|
+
selectedCityCount: import_zod3.z.number().int().min(0),
|
|
3972
|
+
totalResultCount: import_zod3.z.number().int().min(0),
|
|
3973
|
+
csvPath: NullableString2,
|
|
3974
|
+
cities: import_zod3.z.array(import_zod3.z.object({
|
|
3975
|
+
city: import_zod3.z.string(),
|
|
3976
|
+
state: import_zod3.z.string(),
|
|
3977
|
+
location: import_zod3.z.string(),
|
|
3978
|
+
cityKey: import_zod3.z.string(),
|
|
3979
|
+
censusName: import_zod3.z.string(),
|
|
3980
|
+
population: import_zod3.z.number().int().min(0),
|
|
3981
|
+
populationYear: import_zod3.z.number().int().min(2020).max(2025),
|
|
3982
|
+
zips: import_zod3.z.array(import_zod3.z.string()),
|
|
3983
|
+
counties: import_zod3.z.array(import_zod3.z.string()),
|
|
3984
|
+
status: import_zod3.z.enum(["ok", "empty", "failed"]),
|
|
3985
|
+
error: NullableString2,
|
|
3986
|
+
resultCount: import_zod3.z.number().int().min(0),
|
|
3987
|
+
durationMs: import_zod3.z.number().int().min(0),
|
|
3988
|
+
attempts: import_zod3.z.array(MapsSearchAttemptOutput),
|
|
3989
|
+
results: import_zod3.z.array(DirectoryMapsBusinessOutput)
|
|
2536
3990
|
})),
|
|
2537
|
-
durationMs:
|
|
3991
|
+
durationMs: import_zod3.z.number().int().min(0)
|
|
2538
3992
|
};
|
|
2539
|
-
var RankTrackerToolPlanOutput =
|
|
2540
|
-
tool:
|
|
2541
|
-
purpose:
|
|
3993
|
+
var RankTrackerToolPlanOutput = import_zod3.z.object({
|
|
3994
|
+
tool: import_zod3.z.string(),
|
|
3995
|
+
purpose: import_zod3.z.string()
|
|
2542
3996
|
});
|
|
2543
|
-
var RankTrackerTableOutput =
|
|
2544
|
-
name:
|
|
2545
|
-
purpose:
|
|
2546
|
-
keyColumns:
|
|
3997
|
+
var RankTrackerTableOutput = import_zod3.z.object({
|
|
3998
|
+
name: import_zod3.z.string(),
|
|
3999
|
+
purpose: import_zod3.z.string(),
|
|
4000
|
+
keyColumns: import_zod3.z.array(import_zod3.z.string())
|
|
2547
4001
|
});
|
|
2548
|
-
var RankTrackerCronJobOutput =
|
|
2549
|
-
name:
|
|
2550
|
-
purpose:
|
|
2551
|
-
modes:
|
|
2552
|
-
recommendedTools:
|
|
4002
|
+
var RankTrackerCronJobOutput = import_zod3.z.object({
|
|
4003
|
+
name: import_zod3.z.string(),
|
|
4004
|
+
purpose: import_zod3.z.string(),
|
|
4005
|
+
modes: import_zod3.z.array(RankTrackerModeSchema),
|
|
4006
|
+
recommendedTools: import_zod3.z.array(import_zod3.z.string())
|
|
2553
4007
|
});
|
|
2554
4008
|
var RankTrackerBlueprintOutputSchema = {
|
|
2555
|
-
projectName:
|
|
2556
|
-
targetDomain:
|
|
2557
|
-
targetBusinessName:
|
|
2558
|
-
trackingModes:
|
|
2559
|
-
database:
|
|
2560
|
-
recommendedTools:
|
|
2561
|
-
tables:
|
|
2562
|
-
cron:
|
|
2563
|
-
enabled:
|
|
2564
|
-
cadence:
|
|
2565
|
-
expression:
|
|
2566
|
-
timezone:
|
|
2567
|
-
jobs:
|
|
4009
|
+
projectName: import_zod3.z.string(),
|
|
4010
|
+
targetDomain: NullableString2,
|
|
4011
|
+
targetBusinessName: NullableString2,
|
|
4012
|
+
trackingModes: import_zod3.z.array(RankTrackerModeSchema),
|
|
4013
|
+
database: import_zod3.z.string(),
|
|
4014
|
+
recommendedTools: import_zod3.z.array(RankTrackerToolPlanOutput),
|
|
4015
|
+
tables: import_zod3.z.array(RankTrackerTableOutput),
|
|
4016
|
+
cron: import_zod3.z.object({
|
|
4017
|
+
enabled: import_zod3.z.boolean(),
|
|
4018
|
+
cadence: import_zod3.z.string(),
|
|
4019
|
+
expression: import_zod3.z.string(),
|
|
4020
|
+
timezone: import_zod3.z.string(),
|
|
4021
|
+
jobs: import_zod3.z.array(RankTrackerCronJobOutput)
|
|
2568
4022
|
}),
|
|
2569
|
-
metrics:
|
|
2570
|
-
implementationPrompt:
|
|
2571
|
-
};
|
|
2572
|
-
var OrganicResultOutput =
|
|
2573
|
-
position:
|
|
2574
|
-
title:
|
|
2575
|
-
url:
|
|
2576
|
-
domain:
|
|
2577
|
-
snippet:
|
|
4023
|
+
metrics: import_zod3.z.array(import_zod3.z.string()),
|
|
4024
|
+
implementationPrompt: import_zod3.z.string()
|
|
4025
|
+
};
|
|
4026
|
+
var OrganicResultOutput = import_zod3.z.object({
|
|
4027
|
+
position: import_zod3.z.number().int(),
|
|
4028
|
+
title: import_zod3.z.string(),
|
|
4029
|
+
url: import_zod3.z.string(),
|
|
4030
|
+
domain: import_zod3.z.string(),
|
|
4031
|
+
snippet: NullableString2
|
|
2578
4032
|
});
|
|
2579
|
-
var AiOverviewOutput =
|
|
2580
|
-
detected:
|
|
2581
|
-
text:
|
|
4033
|
+
var AiOverviewOutput = import_zod3.z.object({
|
|
4034
|
+
detected: import_zod3.z.boolean(),
|
|
4035
|
+
text: NullableString2
|
|
2582
4036
|
}).nullable();
|
|
2583
|
-
var EntityIdsOutput =
|
|
2584
|
-
kgIds:
|
|
2585
|
-
cids:
|
|
2586
|
-
gcids:
|
|
4037
|
+
var EntityIdsOutput = import_zod3.z.object({
|
|
4038
|
+
kgIds: import_zod3.z.array(import_zod3.z.string()),
|
|
4039
|
+
cids: import_zod3.z.array(import_zod3.z.string()),
|
|
4040
|
+
gcids: import_zod3.z.array(import_zod3.z.string())
|
|
2587
4041
|
}).nullable();
|
|
2588
4042
|
var HarvestPaaOutputSchema = {
|
|
2589
|
-
query:
|
|
2590
|
-
location:
|
|
2591
|
-
questionCount:
|
|
2592
|
-
completionStatus:
|
|
2593
|
-
questions:
|
|
2594
|
-
question:
|
|
2595
|
-
answer:
|
|
2596
|
-
sourceTitle:
|
|
2597
|
-
sourceSite:
|
|
4043
|
+
query: import_zod3.z.string(),
|
|
4044
|
+
location: NullableString2,
|
|
4045
|
+
questionCount: import_zod3.z.number().int().min(0),
|
|
4046
|
+
completionStatus: NullableString2,
|
|
4047
|
+
questions: import_zod3.z.array(import_zod3.z.object({
|
|
4048
|
+
question: import_zod3.z.string(),
|
|
4049
|
+
answer: NullableString2,
|
|
4050
|
+
sourceTitle: NullableString2,
|
|
4051
|
+
sourceSite: NullableString2
|
|
2598
4052
|
})),
|
|
2599
|
-
organicResults:
|
|
4053
|
+
organicResults: import_zod3.z.array(OrganicResultOutput),
|
|
2600
4054
|
aiOverview: AiOverviewOutput,
|
|
2601
4055
|
entityIds: EntityIdsOutput,
|
|
2602
|
-
durationMs:
|
|
4056
|
+
durationMs: import_zod3.z.number().min(0).nullable()
|
|
2603
4057
|
};
|
|
2604
4058
|
var SearchSerpOutputSchema = {
|
|
2605
|
-
query:
|
|
2606
|
-
location:
|
|
2607
|
-
organicResults:
|
|
2608
|
-
localPack:
|
|
2609
|
-
position:
|
|
2610
|
-
name:
|
|
2611
|
-
rating:
|
|
2612
|
-
reviewCount:
|
|
2613
|
-
websiteUrl:
|
|
4059
|
+
query: import_zod3.z.string(),
|
|
4060
|
+
location: NullableString2,
|
|
4061
|
+
organicResults: import_zod3.z.array(OrganicResultOutput),
|
|
4062
|
+
localPack: import_zod3.z.array(import_zod3.z.object({
|
|
4063
|
+
position: import_zod3.z.number().int(),
|
|
4064
|
+
name: import_zod3.z.string(),
|
|
4065
|
+
rating: NullableString2,
|
|
4066
|
+
reviewCount: NullableString2,
|
|
4067
|
+
websiteUrl: NullableString2
|
|
2614
4068
|
})),
|
|
2615
4069
|
aiOverview: AiOverviewOutput,
|
|
2616
4070
|
entityIds: EntityIdsOutput
|
|
2617
4071
|
};
|
|
2618
4072
|
var ExtractUrlOutputSchema = {
|
|
2619
|
-
url:
|
|
2620
|
-
title:
|
|
2621
|
-
headings:
|
|
2622
|
-
level:
|
|
2623
|
-
text:
|
|
4073
|
+
url: import_zod3.z.string(),
|
|
4074
|
+
title: NullableString2,
|
|
4075
|
+
headings: import_zod3.z.array(import_zod3.z.object({
|
|
4076
|
+
level: import_zod3.z.number().int(),
|
|
4077
|
+
text: import_zod3.z.string()
|
|
2624
4078
|
})),
|
|
2625
|
-
schemaBlockCount:
|
|
2626
|
-
entityName:
|
|
2627
|
-
entityTypes:
|
|
2628
|
-
napScore:
|
|
2629
|
-
missingSchemaFields:
|
|
2630
|
-
screenshotSaved:
|
|
4079
|
+
schemaBlockCount: import_zod3.z.number().int().min(0),
|
|
4080
|
+
entityName: NullableString2,
|
|
4081
|
+
entityTypes: import_zod3.z.array(import_zod3.z.string()),
|
|
4082
|
+
napScore: import_zod3.z.number().nullable(),
|
|
4083
|
+
missingSchemaFields: import_zod3.z.array(import_zod3.z.string()),
|
|
4084
|
+
screenshotSaved: NullableString2
|
|
2631
4085
|
};
|
|
2632
4086
|
var ExtractSiteOutputSchema = {
|
|
2633
|
-
url:
|
|
2634
|
-
pageCount:
|
|
2635
|
-
pages:
|
|
2636
|
-
url:
|
|
2637
|
-
title:
|
|
2638
|
-
schemaTypes:
|
|
4087
|
+
url: import_zod3.z.string(),
|
|
4088
|
+
pageCount: import_zod3.z.number().int().min(0),
|
|
4089
|
+
pages: import_zod3.z.array(import_zod3.z.object({
|
|
4090
|
+
url: import_zod3.z.string(),
|
|
4091
|
+
title: NullableString2,
|
|
4092
|
+
schemaTypes: import_zod3.z.array(import_zod3.z.string())
|
|
2639
4093
|
})),
|
|
2640
|
-
durationMs:
|
|
4094
|
+
durationMs: import_zod3.z.number().min(0)
|
|
2641
4095
|
};
|
|
2642
4096
|
var MapsPlaceIntelOutputSchema = {
|
|
2643
|
-
name:
|
|
2644
|
-
rating:
|
|
2645
|
-
reviewCount:
|
|
2646
|
-
category:
|
|
2647
|
-
address:
|
|
2648
|
-
phone:
|
|
2649
|
-
website:
|
|
2650
|
-
hoursSummary:
|
|
2651
|
-
bookingUrl:
|
|
2652
|
-
kgmid:
|
|
2653
|
-
cidDecimal:
|
|
2654
|
-
cidUrl:
|
|
2655
|
-
lat:
|
|
2656
|
-
lng:
|
|
2657
|
-
reviewsStatus:
|
|
2658
|
-
reviewsCollected:
|
|
2659
|
-
reviewTopics:
|
|
2660
|
-
label:
|
|
2661
|
-
count:
|
|
4097
|
+
name: import_zod3.z.string(),
|
|
4098
|
+
rating: NullableString2,
|
|
4099
|
+
reviewCount: NullableString2,
|
|
4100
|
+
category: NullableString2,
|
|
4101
|
+
address: NullableString2,
|
|
4102
|
+
phone: NullableString2,
|
|
4103
|
+
website: NullableString2,
|
|
4104
|
+
hoursSummary: NullableString2,
|
|
4105
|
+
bookingUrl: NullableString2,
|
|
4106
|
+
kgmid: NullableString2,
|
|
4107
|
+
cidDecimal: NullableString2,
|
|
4108
|
+
cidUrl: NullableString2,
|
|
4109
|
+
lat: import_zod3.z.number().nullable(),
|
|
4110
|
+
lng: import_zod3.z.number().nullable(),
|
|
4111
|
+
reviewsStatus: import_zod3.z.string(),
|
|
4112
|
+
reviewsCollected: import_zod3.z.number().int().min(0),
|
|
4113
|
+
reviewTopics: import_zod3.z.array(import_zod3.z.object({
|
|
4114
|
+
label: import_zod3.z.string(),
|
|
4115
|
+
count: import_zod3.z.string()
|
|
2662
4116
|
}))
|
|
2663
4117
|
};
|
|
2664
4118
|
var CreditsInfoOutputSchema = {
|
|
2665
|
-
balanceCredits:
|
|
2666
|
-
matchedCost:
|
|
2667
|
-
label:
|
|
2668
|
-
credits:
|
|
2669
|
-
unit:
|
|
2670
|
-
notes:
|
|
4119
|
+
balanceCredits: import_zod3.z.number().nullable(),
|
|
4120
|
+
matchedCost: import_zod3.z.object({
|
|
4121
|
+
label: import_zod3.z.string(),
|
|
4122
|
+
credits: import_zod3.z.number(),
|
|
4123
|
+
unit: import_zod3.z.string(),
|
|
4124
|
+
notes: NullableString2
|
|
2671
4125
|
}).nullable(),
|
|
2672
|
-
costs:
|
|
2673
|
-
key:
|
|
2674
|
-
label:
|
|
2675
|
-
credits:
|
|
2676
|
-
unit:
|
|
2677
|
-
notes:
|
|
4126
|
+
costs: import_zod3.z.array(import_zod3.z.object({
|
|
4127
|
+
key: import_zod3.z.string(),
|
|
4128
|
+
label: import_zod3.z.string(),
|
|
4129
|
+
credits: import_zod3.z.number(),
|
|
4130
|
+
unit: import_zod3.z.string(),
|
|
4131
|
+
notes: NullableString2
|
|
2678
4132
|
})),
|
|
2679
|
-
ledger:
|
|
2680
|
-
createdAt:
|
|
2681
|
-
operation:
|
|
2682
|
-
credits:
|
|
2683
|
-
description:
|
|
4133
|
+
ledger: import_zod3.z.array(import_zod3.z.object({
|
|
4134
|
+
createdAt: import_zod3.z.string(),
|
|
4135
|
+
operation: import_zod3.z.string(),
|
|
4136
|
+
credits: import_zod3.z.number(),
|
|
4137
|
+
description: NullableString2
|
|
2684
4138
|
})),
|
|
2685
|
-
concurrency:
|
|
2686
|
-
currentExtraSlots:
|
|
2687
|
-
currentLimit:
|
|
2688
|
-
hasSubscription:
|
|
2689
|
-
upgrade:
|
|
2690
|
-
product:
|
|
2691
|
-
priceLabel:
|
|
2692
|
-
unitAmountUsd:
|
|
2693
|
-
currency:
|
|
2694
|
-
interval:
|
|
2695
|
-
billingUrl:
|
|
2696
|
-
terminalCommand:
|
|
2697
|
-
terminalCommandWithApiKeyEnv:
|
|
4139
|
+
concurrency: import_zod3.z.object({
|
|
4140
|
+
currentExtraSlots: import_zod3.z.number().int().min(0),
|
|
4141
|
+
currentLimit: import_zod3.z.number().int().min(1),
|
|
4142
|
+
hasSubscription: import_zod3.z.boolean(),
|
|
4143
|
+
upgrade: import_zod3.z.object({
|
|
4144
|
+
product: import_zod3.z.string(),
|
|
4145
|
+
priceLabel: import_zod3.z.string(),
|
|
4146
|
+
unitAmountUsd: import_zod3.z.number(),
|
|
4147
|
+
currency: import_zod3.z.string(),
|
|
4148
|
+
interval: import_zod3.z.string(),
|
|
4149
|
+
billingUrl: import_zod3.z.string().url(),
|
|
4150
|
+
terminalCommand: import_zod3.z.string(),
|
|
4151
|
+
terminalCommandWithApiKeyEnv: import_zod3.z.string()
|
|
2698
4152
|
})
|
|
2699
4153
|
}).nullable()
|
|
2700
4154
|
};
|
|
2701
4155
|
var MapSiteUrlsOutputSchema = {
|
|
2702
|
-
startUrl:
|
|
2703
|
-
totalFound:
|
|
2704
|
-
truncated:
|
|
2705
|
-
okCount:
|
|
2706
|
-
redirectCount:
|
|
2707
|
-
brokenCount:
|
|
2708
|
-
urls:
|
|
2709
|
-
url:
|
|
2710
|
-
status:
|
|
4156
|
+
startUrl: import_zod3.z.string(),
|
|
4157
|
+
totalFound: import_zod3.z.number().int().min(0),
|
|
4158
|
+
truncated: import_zod3.z.boolean(),
|
|
4159
|
+
okCount: import_zod3.z.number().int().min(0),
|
|
4160
|
+
redirectCount: import_zod3.z.number().int().min(0),
|
|
4161
|
+
brokenCount: import_zod3.z.number().int().min(0),
|
|
4162
|
+
urls: import_zod3.z.array(import_zod3.z.object({
|
|
4163
|
+
url: import_zod3.z.string(),
|
|
4164
|
+
status: import_zod3.z.number().int().nullable()
|
|
2711
4165
|
})),
|
|
2712
|
-
durationMs:
|
|
4166
|
+
durationMs: import_zod3.z.number().min(0)
|
|
2713
4167
|
};
|
|
2714
4168
|
var YoutubeHarvestOutputSchema = {
|
|
2715
|
-
mode:
|
|
2716
|
-
videoCount:
|
|
2717
|
-
channel:
|
|
2718
|
-
title:
|
|
2719
|
-
subscriberCount:
|
|
4169
|
+
mode: import_zod3.z.string(),
|
|
4170
|
+
videoCount: import_zod3.z.number().int().min(0),
|
|
4171
|
+
channel: import_zod3.z.object({
|
|
4172
|
+
title: NullableString2,
|
|
4173
|
+
subscriberCount: NullableString2
|
|
2720
4174
|
}).nullable(),
|
|
2721
|
-
videos:
|
|
2722
|
-
videoId:
|
|
2723
|
-
title:
|
|
2724
|
-
channelName:
|
|
2725
|
-
views:
|
|
2726
|
-
duration:
|
|
2727
|
-
url:
|
|
4175
|
+
videos: import_zod3.z.array(import_zod3.z.object({
|
|
4176
|
+
videoId: import_zod3.z.string(),
|
|
4177
|
+
title: import_zod3.z.string(),
|
|
4178
|
+
channelName: NullableString2,
|
|
4179
|
+
views: NullableString2,
|
|
4180
|
+
duration: NullableString2,
|
|
4181
|
+
url: NullableString2
|
|
2728
4182
|
}))
|
|
2729
4183
|
};
|
|
2730
4184
|
var FacebookAdSearchOutputSchema = {
|
|
2731
|
-
query:
|
|
2732
|
-
advertiserCount:
|
|
2733
|
-
advertisers:
|
|
2734
|
-
name:
|
|
2735
|
-
|
|
2736
|
-
|
|
4185
|
+
query: import_zod3.z.string(),
|
|
4186
|
+
advertiserCount: import_zod3.z.number().int().min(0),
|
|
4187
|
+
advertisers: import_zod3.z.array(import_zod3.z.object({
|
|
4188
|
+
name: NullableString2,
|
|
4189
|
+
pageId: NullableString2,
|
|
4190
|
+
pageUrl: NullableString2,
|
|
4191
|
+
adCount: import_zod3.z.number().int().nullable(),
|
|
4192
|
+
libraryId: NullableString2,
|
|
4193
|
+
sampleLibraryId: NullableString2
|
|
2737
4194
|
}))
|
|
2738
4195
|
};
|
|
2739
4196
|
var FacebookPageIntelOutputSchema = {
|
|
2740
|
-
advertiserName:
|
|
2741
|
-
totalAds:
|
|
2742
|
-
activeCount:
|
|
2743
|
-
videoCount:
|
|
2744
|
-
imageCount:
|
|
2745
|
-
ads:
|
|
2746
|
-
libraryId:
|
|
2747
|
-
status:
|
|
2748
|
-
creativeType:
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
4197
|
+
advertiserName: NullableString2,
|
|
4198
|
+
totalAds: import_zod3.z.number().int().min(0),
|
|
4199
|
+
activeCount: import_zod3.z.number().int().min(0),
|
|
4200
|
+
videoCount: import_zod3.z.number().int().min(0),
|
|
4201
|
+
imageCount: import_zod3.z.number().int().min(0),
|
|
4202
|
+
ads: import_zod3.z.array(import_zod3.z.object({
|
|
4203
|
+
libraryId: NullableString2,
|
|
4204
|
+
status: NullableString2,
|
|
4205
|
+
creativeType: NullableString2,
|
|
4206
|
+
primaryText: NullableString2,
|
|
4207
|
+
headline: NullableString2,
|
|
4208
|
+
cta: NullableString2,
|
|
4209
|
+
startDate: NullableString2,
|
|
4210
|
+
landingUrl: NullableString2,
|
|
4211
|
+
domain: NullableString2,
|
|
4212
|
+
videoUrl: NullableString2,
|
|
4213
|
+
imageUrl: NullableString2,
|
|
4214
|
+
videoPoster: NullableString2,
|
|
4215
|
+
variations: import_zod3.z.number().int().nullable()
|
|
2754
4216
|
}))
|
|
2755
4217
|
};
|
|
2756
4218
|
var FacebookVideoTranscribeOutputSchema = {
|
|
2757
|
-
sourceUrl:
|
|
2758
|
-
pageUrl:
|
|
2759
|
-
videoId:
|
|
2760
|
-
ownerName:
|
|
2761
|
-
selectedQuality:
|
|
2762
|
-
bitrate:
|
|
2763
|
-
videoDurationSec:
|
|
2764
|
-
videoUrl:
|
|
2765
|
-
wordCount:
|
|
2766
|
-
chunkCount:
|
|
2767
|
-
transcriptText:
|
|
2768
|
-
chunks:
|
|
2769
|
-
startSec:
|
|
2770
|
-
endSec:
|
|
2771
|
-
text:
|
|
4219
|
+
sourceUrl: import_zod3.z.string().url(),
|
|
4220
|
+
pageUrl: import_zod3.z.string().url(),
|
|
4221
|
+
videoId: NullableString2,
|
|
4222
|
+
ownerName: NullableString2,
|
|
4223
|
+
selectedQuality: import_zod3.z.string(),
|
|
4224
|
+
bitrate: import_zod3.z.number().int().nullable(),
|
|
4225
|
+
videoDurationSec: import_zod3.z.number().nullable(),
|
|
4226
|
+
videoUrl: import_zod3.z.string().url(),
|
|
4227
|
+
wordCount: import_zod3.z.number().int().min(0),
|
|
4228
|
+
chunkCount: import_zod3.z.number().int().min(0),
|
|
4229
|
+
transcriptText: import_zod3.z.string(),
|
|
4230
|
+
chunks: import_zod3.z.array(import_zod3.z.object({
|
|
4231
|
+
startSec: import_zod3.z.number(),
|
|
4232
|
+
endSec: import_zod3.z.number(),
|
|
4233
|
+
text: import_zod3.z.string()
|
|
2772
4234
|
}))
|
|
2773
4235
|
};
|
|
4236
|
+
var TranscriptChunkOutput = import_zod3.z.object({
|
|
4237
|
+
startSec: import_zod3.z.number(),
|
|
4238
|
+
endSec: import_zod3.z.number(),
|
|
4239
|
+
text: import_zod3.z.string()
|
|
4240
|
+
});
|
|
4241
|
+
var YoutubeTranscribeOutputSchema = {
|
|
4242
|
+
videoId: NullableString2,
|
|
4243
|
+
url: NullableString2,
|
|
4244
|
+
wordCount: import_zod3.z.number().int().min(0),
|
|
4245
|
+
chunkCount: import_zod3.z.number().int().min(0),
|
|
4246
|
+
durationMs: import_zod3.z.number().nullable(),
|
|
4247
|
+
transcriptText: import_zod3.z.string(),
|
|
4248
|
+
chunks: import_zod3.z.array(TranscriptChunkOutput),
|
|
4249
|
+
resolvedInputs: import_zod3.z.object({
|
|
4250
|
+
videoId: NullableString2,
|
|
4251
|
+
url: NullableString2
|
|
4252
|
+
})
|
|
4253
|
+
};
|
|
4254
|
+
var FacebookAdTranscribeOutputSchema = {
|
|
4255
|
+
videoUrl: import_zod3.z.string().url(),
|
|
4256
|
+
wordCount: import_zod3.z.number().int().min(0),
|
|
4257
|
+
chunkCount: import_zod3.z.number().int().min(0),
|
|
4258
|
+
durationMs: import_zod3.z.number().nullable(),
|
|
4259
|
+
transcriptText: import_zod3.z.string(),
|
|
4260
|
+
chunks: import_zod3.z.array(TranscriptChunkOutput),
|
|
4261
|
+
resolvedInputs: import_zod3.z.object({
|
|
4262
|
+
videoUrl: import_zod3.z.string().url()
|
|
4263
|
+
})
|
|
4264
|
+
};
|
|
4265
|
+
var CaptureSerpSnapshotOutputSchema = {
|
|
4266
|
+
schemaVersion: import_zod3.z.literal("serp-intelligence.capture.v1"),
|
|
4267
|
+
status: import_zod3.z.string(),
|
|
4268
|
+
query: NullableString2,
|
|
4269
|
+
location: NullableString2,
|
|
4270
|
+
capturedAt: NullableString2,
|
|
4271
|
+
resultCount: import_zod3.z.number().int().min(0).nullable(),
|
|
4272
|
+
snapshotId: NullableString2,
|
|
4273
|
+
resolvedInputs: import_zod3.z.record(import_zod3.z.unknown()),
|
|
4274
|
+
artifacts: import_zod3.z.array(import_zod3.z.record(import_zod3.z.unknown())),
|
|
4275
|
+
diagnostics: import_zod3.z.record(import_zod3.z.unknown()).nullable(),
|
|
4276
|
+
providerPayload: import_zod3.z.record(import_zod3.z.unknown())
|
|
4277
|
+
};
|
|
4278
|
+
var CaptureSerpPageSnapshotsOutputSchema = {
|
|
4279
|
+
schemaVersion: import_zod3.z.literal("serp-intelligence.page-snapshots.v1"),
|
|
4280
|
+
status: import_zod3.z.string(),
|
|
4281
|
+
count: import_zod3.z.number().int().min(0),
|
|
4282
|
+
failedCount: import_zod3.z.number().int().min(0),
|
|
4283
|
+
captures: import_zod3.z.array(import_zod3.z.record(import_zod3.z.unknown())),
|
|
4284
|
+
resolvedInputs: import_zod3.z.record(import_zod3.z.unknown()),
|
|
4285
|
+
diagnostics: import_zod3.z.record(import_zod3.z.unknown()).nullable(),
|
|
4286
|
+
providerPayload: import_zod3.z.record(import_zod3.z.unknown())
|
|
4287
|
+
};
|
|
2774
4288
|
var CreditsInfoInputSchema = {
|
|
2775
|
-
item:
|
|
2776
|
-
includeLedger:
|
|
4289
|
+
item: import_zod3.z.string().optional().describe('Optional tool, action, or feature to look up, e.g. "maps reviews", "extract_url", "YouTube transcription", or "concurrency"'),
|
|
4290
|
+
includeLedger: import_zod3.z.boolean().default(false).describe("Whether to include recent credit ledger entries")
|
|
2777
4291
|
};
|
|
2778
|
-
var WorkflowIdSchema =
|
|
4292
|
+
var WorkflowIdSchema = import_zod3.z.enum([
|
|
2779
4293
|
"directory",
|
|
2780
4294
|
"agent-packet",
|
|
2781
4295
|
"local-competitive-audit",
|
|
@@ -2785,114 +4299,129 @@ var WorkflowIdSchema = import_zod2.z.enum([
|
|
|
2785
4299
|
"ai-overview-language"
|
|
2786
4300
|
]);
|
|
2787
4301
|
var WorkflowListInputSchema = {
|
|
2788
|
-
includeRecipes:
|
|
4302
|
+
includeRecipes: import_zod3.z.boolean().default(true).describe("Include high-level AI-facing recipes such as market analysis, ICP research, forum/review research, brand design briefings, CRO audits, positioning briefs, content gaps, and AI search visibility audits.")
|
|
2789
4303
|
};
|
|
2790
4304
|
var WorkflowSuggestInputSchema = {
|
|
2791
|
-
goal:
|
|
2792
|
-
query:
|
|
2793
|
-
keyword:
|
|
2794
|
-
domain:
|
|
2795
|
-
url:
|
|
2796
|
-
location:
|
|
2797
|
-
state:
|
|
2798
|
-
maxSuggestions:
|
|
4305
|
+
goal: import_zod3.z.string().min(1).describe('The user goal or job to route, e.g. "market analysis for roofers in Tennessee", "ICP research for med spas", "CRO audit for this URL", or "brand design briefing".'),
|
|
4306
|
+
query: import_zod3.z.string().optional().describe("Business category, niche, or Maps query when known."),
|
|
4307
|
+
keyword: import_zod3.z.string().optional().describe("Search keyword, audience problem, or content topic when known."),
|
|
4308
|
+
domain: import_zod3.z.string().optional().describe("Target domain or brand domain when known."),
|
|
4309
|
+
url: import_zod3.z.string().url().optional().describe("Target URL when the workflow should inspect a specific page."),
|
|
4310
|
+
location: import_zod3.z.string().optional().describe("City/region/country for localized research, e.g. Denver, CO."),
|
|
4311
|
+
state: import_zod3.z.string().optional().describe("US state abbreviation or name for state-wide market research."),
|
|
4312
|
+
maxSuggestions: import_zod3.z.number().int().min(1).max(8).default(3).describe("Number of matching workflow recipes to return.")
|
|
2799
4313
|
};
|
|
2800
4314
|
var WorkflowRunInputSchema = {
|
|
2801
|
-
workflowId: WorkflowIdSchema.describe("Workflow to run. Use workflow_list or workflow_suggest first when unsure."),
|
|
2802
|
-
input:
|
|
2803
|
-
webhookUrl:
|
|
4315
|
+
workflowId: WorkflowIdSchema.describe("Workflow to run: directory, agent-packet, local-competitive-audit, map-comparison, serp-comparison, paa-expansion-brief, ai-overview-language. Use only these values; call workflow_list or workflow_suggest first when unsure."),
|
|
4316
|
+
input: import_zod3.z.record(import_zod3.z.unknown()).default({}).describe("Workflow-specific input object. Examples: agent-packet uses {keyword, domain?, location?, maxQuestions?}; local-competitive-audit uses {query, state, minPopulation?, maxCities?, maxResultsPerCity?, hydrateTop?, maxReviews?}; serp-comparison uses {keyword, domain?, url?, location?, extractTop?}."),
|
|
4317
|
+
webhookUrl: import_zod3.z.string().url().optional().describe("Optional HTTPS webhook to receive the completed hosted workflow run event.")
|
|
4318
|
+
};
|
|
4319
|
+
var WorkflowStepInputSchema = {
|
|
4320
|
+
runId: import_zod3.z.string().min(1).describe("Workflow run id returned by workflow_run, workflow_step, or workflow_status. Use only a returned runId; do not construct one yourself. Advances the run by exactly one step (one logical leg, e.g. one live harvest).")
|
|
2804
4321
|
};
|
|
2805
4322
|
var WorkflowStatusInputSchema = {
|
|
2806
|
-
runId:
|
|
4323
|
+
runId: import_zod3.z.string().min(1).describe("Workflow run id returned by workflow_run, workflow_step, or workflow_status. Use only a returned runId; do not construct one yourself.")
|
|
2807
4324
|
};
|
|
2808
4325
|
var WorkflowArtifactReadInputSchema = {
|
|
2809
|
-
runId:
|
|
2810
|
-
artifactId:
|
|
2811
|
-
maxBytes:
|
|
2812
|
-
};
|
|
2813
|
-
var WorkflowRecipeOutput =
|
|
2814
|
-
id:
|
|
2815
|
-
title:
|
|
2816
|
-
description:
|
|
2817
|
-
primaryWorkflowId:
|
|
2818
|
-
recommendedTools:
|
|
2819
|
-
requiredInputs:
|
|
2820
|
-
optionalInputs:
|
|
2821
|
-
produces:
|
|
2822
|
-
runHint:
|
|
4326
|
+
runId: import_zod3.z.string().min(1).describe("Workflow run id returned by workflow_run, workflow_step, or workflow_status. Use only a returned runId; do not construct one yourself."),
|
|
4327
|
+
artifactId: import_zod3.z.string().min(1).describe("Artifact id from the run artifact list returned by workflow_run, workflow_step, or workflow_status. Use only a returned artifactId; do not construct one yourself."),
|
|
4328
|
+
maxBytes: import_zod3.z.number().int().min(1e3).max(1e6).default(2e5).describe("Maximum bytes of artifact text to return inline. Use lower values for large CSV/JSON artifacts; call again with the downloadUrl if needed outside MCP.")
|
|
4329
|
+
};
|
|
4330
|
+
var WorkflowRecipeOutput = import_zod3.z.object({
|
|
4331
|
+
id: import_zod3.z.string(),
|
|
4332
|
+
title: import_zod3.z.string(),
|
|
4333
|
+
description: import_zod3.z.string(),
|
|
4334
|
+
primaryWorkflowId: import_zod3.z.string().nullable(),
|
|
4335
|
+
recommendedTools: import_zod3.z.array(import_zod3.z.string()),
|
|
4336
|
+
requiredInputs: import_zod3.z.array(import_zod3.z.string()),
|
|
4337
|
+
optionalInputs: import_zod3.z.array(import_zod3.z.string()),
|
|
4338
|
+
produces: import_zod3.z.array(import_zod3.z.string()),
|
|
4339
|
+
runHint: import_zod3.z.string()
|
|
2823
4340
|
});
|
|
2824
|
-
var WorkflowDefinitionOutput =
|
|
2825
|
-
id:
|
|
2826
|
-
title:
|
|
2827
|
-
description:
|
|
4341
|
+
var WorkflowDefinitionOutput = import_zod3.z.object({
|
|
4342
|
+
id: import_zod3.z.string(),
|
|
4343
|
+
title: import_zod3.z.string(),
|
|
4344
|
+
description: import_zod3.z.string()
|
|
2828
4345
|
});
|
|
2829
|
-
var WorkflowArtifactOutput =
|
|
4346
|
+
var WorkflowArtifactOutput = import_zod3.z.record(import_zod3.z.unknown());
|
|
2830
4347
|
var WorkflowListOutputSchema = {
|
|
2831
|
-
workflows:
|
|
2832
|
-
recipes:
|
|
4348
|
+
workflows: import_zod3.z.array(WorkflowDefinitionOutput),
|
|
4349
|
+
recipes: import_zod3.z.array(WorkflowRecipeOutput)
|
|
2833
4350
|
};
|
|
2834
4351
|
var WorkflowSuggestOutputSchema = {
|
|
2835
|
-
goal:
|
|
2836
|
-
suggestions:
|
|
4352
|
+
goal: import_zod3.z.string(),
|
|
4353
|
+
suggestions: import_zod3.z.array(WorkflowRecipeOutput)
|
|
2837
4354
|
};
|
|
2838
4355
|
var WorkflowRunOutputSchema = {
|
|
2839
|
-
workflowId:
|
|
2840
|
-
input:
|
|
2841
|
-
run:
|
|
2842
|
-
summary:
|
|
2843
|
-
|
|
4356
|
+
workflowId: import_zod3.z.string(),
|
|
4357
|
+
input: import_zod3.z.record(import_zod3.z.unknown()),
|
|
4358
|
+
run: import_zod3.z.record(import_zod3.z.unknown()).optional(),
|
|
4359
|
+
summary: import_zod3.z.record(import_zod3.z.unknown()).optional(),
|
|
4360
|
+
step: import_zod3.z.record(import_zod3.z.unknown()).optional(),
|
|
4361
|
+
nextStep: import_zod3.z.record(import_zod3.z.unknown()).nullable().optional(),
|
|
4362
|
+
done: import_zod3.z.boolean().optional(),
|
|
4363
|
+
artifacts: import_zod3.z.array(WorkflowArtifactOutput)
|
|
4364
|
+
};
|
|
4365
|
+
var WorkflowStepOutputSchema = {
|
|
4366
|
+
runId: import_zod3.z.string(),
|
|
4367
|
+
run: import_zod3.z.record(import_zod3.z.unknown()).optional(),
|
|
4368
|
+
summary: import_zod3.z.record(import_zod3.z.unknown()).nullable().optional(),
|
|
4369
|
+
step: import_zod3.z.record(import_zod3.z.unknown()).optional(),
|
|
4370
|
+
nextStep: import_zod3.z.record(import_zod3.z.unknown()).nullable().optional(),
|
|
4371
|
+
done: import_zod3.z.boolean(),
|
|
4372
|
+
artifacts: import_zod3.z.array(WorkflowArtifactOutput)
|
|
2844
4373
|
};
|
|
2845
4374
|
var WorkflowStatusOutputSchema = {
|
|
2846
|
-
run:
|
|
2847
|
-
artifacts:
|
|
4375
|
+
run: import_zod3.z.record(import_zod3.z.unknown()).optional(),
|
|
4376
|
+
artifacts: import_zod3.z.array(WorkflowArtifactOutput)
|
|
2848
4377
|
};
|
|
2849
4378
|
var WorkflowArtifactReadOutputSchema = {
|
|
2850
|
-
runId:
|
|
2851
|
-
artifactId:
|
|
2852
|
-
contentType:
|
|
2853
|
-
bytes:
|
|
2854
|
-
truncated:
|
|
2855
|
-
text:
|
|
4379
|
+
runId: import_zod3.z.string(),
|
|
4380
|
+
artifactId: import_zod3.z.string(),
|
|
4381
|
+
contentType: import_zod3.z.string(),
|
|
4382
|
+
bytes: import_zod3.z.number().int().min(0),
|
|
4383
|
+
truncated: import_zod3.z.boolean(),
|
|
4384
|
+
text: import_zod3.z.string()
|
|
2856
4385
|
};
|
|
2857
4386
|
var SearchSerpInputSchema = {
|
|
2858
|
-
query:
|
|
2859
|
-
location:
|
|
2860
|
-
gl:
|
|
2861
|
-
hl:
|
|
2862
|
-
device:
|
|
2863
|
-
proxyMode:
|
|
2864
|
-
proxyZip:
|
|
2865
|
-
debug:
|
|
2866
|
-
pages:
|
|
4387
|
+
query: import_zod3.z.string().min(1).describe('Core search topic only. Separate location when possible. If user says "best dentist in Brooklyn NY serp", use query="best dentist" and location="Brooklyn, NY".'),
|
|
4388
|
+
location: import_zod3.z.string().optional().describe("City, region, or country for geo-targeted results, inferred from user request when present."),
|
|
4389
|
+
gl: import_zod3.z.string().length(2).default("us").describe("Google country code inferred from location or user language."),
|
|
4390
|
+
hl: import_zod3.z.string().default("en").describe("Google interface/content language inferred from user request."),
|
|
4391
|
+
device: import_zod3.z.enum(["desktop", "mobile"]).default("desktop").describe("SERP device context. Use desktop by default; use mobile only when the user asks for mobile rankings."),
|
|
4392
|
+
proxyMode: import_zod3.z.enum(["location", "configured", "none"]).default(DEFAULT_PROXY_MODE).describe("Proxy targeting mode. Default configured uses the service proxy without city/ZIP targeting for the highest general SERP success rate. Use location only when the user explicitly needs city/ZIP-targeted residential proxy evidence. Use none only for direct-network debugging."),
|
|
4393
|
+
proxyZip: import_zod3.z.string().regex(/^\d{5}$/).optional().describe("Optional US ZIP override for residential location proxy targeting. Use only with proxyMode location when the user gives a specific ZIP or city-center targeting needs to be forced."),
|
|
4394
|
+
debug: import_zod3.z.boolean().default(false).describe("Include sanitized browser/session/location diagnostics in the response. Use true when debugging localization, CAPTCHA, or proxy behavior."),
|
|
4395
|
+
pages: import_zod3.z.number().int().min(1).max(2).default(1).describe("Number of result pages to fetch (1\u20132)")
|
|
2867
4396
|
};
|
|
2868
4397
|
var CaptureSerpSnapshotInputSchema = {
|
|
2869
|
-
query:
|
|
2870
|
-
location:
|
|
2871
|
-
gl:
|
|
2872
|
-
hl:
|
|
2873
|
-
device:
|
|
2874
|
-
proxyMode:
|
|
2875
|
-
proxyZip:
|
|
2876
|
-
pages:
|
|
2877
|
-
debug:
|
|
2878
|
-
includePageSnapshots:
|
|
2879
|
-
pageSnapshotLimit:
|
|
4398
|
+
query: import_zod3.z.string().min(1).describe("Core search query to capture as a structured SERP Intelligence snapshot. Separate the place into location when the user gives a city, region, country, or ZIP."),
|
|
4399
|
+
location: import_zod3.z.string().optional().describe("City, region, country, or service area used for localized Google results. MCP Scraper records location evidence; UULE alone is not proof of localization."),
|
|
4400
|
+
gl: import_zod3.z.string().length(2).default("us").describe("Google country code inferred from the requested market, e.g. us, gb, ca, au."),
|
|
4401
|
+
hl: import_zod3.z.string().default("en").describe("Google interface/content language inferred from the user request."),
|
|
4402
|
+
device: import_zod3.z.enum(["desktop", "mobile"]).default("desktop").describe("SERP device context. Use mobile only when the user asks for mobile rankings or mobile SERP evidence."),
|
|
4403
|
+
proxyMode: import_zod3.z.enum(["location", "configured", "none"]).default(DEFAULT_PROXY_MODE).describe("Proxy behavior for capture. Default configured uses the service proxy without city/ZIP targeting for the highest general capture success rate. Use location only when the user explicitly needs city/ZIP-targeted residential proxy evidence. Use none only for direct-network debugging."),
|
|
4404
|
+
proxyZip: import_zod3.z.string().regex(/^\d{5}$/).optional().describe("Optional US ZIP override for residential location proxy targeting. Use only with proxyMode location when a precise city-center or ZIP proxy is needed."),
|
|
4405
|
+
pages: import_zod3.z.number().int().min(1).max(2).default(1).describe("Number of Google result pages to capture. Use 1 normally and 2 only when the user needs deeper ranking evidence."),
|
|
4406
|
+
debug: import_zod3.z.boolean().default(false).describe("Include sanitized browser, proxy, and location diagnostics. Use true when debugging localization, CAPTCHA, proxy selection, or capture reliability."),
|
|
4407
|
+
includePageSnapshots: import_zod3.z.boolean().default(false).describe("Also capture ranking-page snapshots for selected SERP URLs through the same product capture path."),
|
|
4408
|
+
pageSnapshotLimit: import_zod3.z.number().int().min(0).max(10).default(0).describe("Maximum ranking-page snapshots to capture when includePageSnapshots is true. Use 0 when only SERP evidence is needed.")
|
|
2880
4409
|
};
|
|
2881
4410
|
var ScreenshotInputSchema = {
|
|
2882
|
-
url:
|
|
2883
|
-
device:
|
|
2884
|
-
allowLocal:
|
|
4411
|
+
url: import_zod3.z.string().url().describe("URL to capture as a full-page screenshot. Use http or https. Pass allowLocal: true to capture localhost or private-network URLs during development."),
|
|
4412
|
+
device: import_zod3.z.enum(["desktop", "mobile"]).default("desktop").describe("Viewport profile. desktop = 1440\xD7900. mobile = 390\xD7844. Use desktop by default; use mobile when the user asks for a mobile view."),
|
|
4413
|
+
allowLocal: import_zod3.z.boolean().default(false).describe("Allow localhost and private-network URLs (127.x, 192.168.x, 10.x, etc.). For local development only \u2014 not for production use.")
|
|
2885
4414
|
};
|
|
2886
4415
|
var CaptureSerpPageSnapshotsInputSchema = {
|
|
2887
|
-
urls:
|
|
2888
|
-
targets:
|
|
2889
|
-
url:
|
|
2890
|
-
sourceKind:
|
|
2891
|
-
sourcePosition:
|
|
4416
|
+
urls: import_zod3.z.array(import_zod3.z.string().url()).min(1).max(25).describe("Public HTTP/HTTPS URLs to capture as SERP Intelligence page snapshots. Do not pass localhost, private IPs, file URLs, or internal admin URLs."),
|
|
4417
|
+
targets: import_zod3.z.array(import_zod3.z.object({
|
|
4418
|
+
url: import_zod3.z.string().url().describe("Public HTTP/HTTPS URL to capture."),
|
|
4419
|
+
sourceKind: import_zod3.z.enum(["organic", "ai_citation", "local_pack_website", "configured_target", "site_subject"]).default("configured_target").describe("Why this page is being captured for SERP Intelligence evidence."),
|
|
4420
|
+
sourcePosition: import_zod3.z.number().int().min(1).optional().describe("Ranking or citation position when the page came from SERP evidence.")
|
|
2892
4421
|
}).strict()).min(1).max(25).optional().describe("Structured page snapshot targets. Use this instead of urls when source kind or position should be preserved."),
|
|
2893
|
-
maxConcurrency:
|
|
2894
|
-
timeoutMs:
|
|
2895
|
-
debug:
|
|
4422
|
+
maxConcurrency: import_zod3.z.number().int().min(1).max(5).default(2).describe("Parallel page captures. Use 2 normally; higher values can increase proxy/browser pressure."),
|
|
4423
|
+
timeoutMs: import_zod3.z.number().int().min(1e3).max(6e4).default(15e3).describe("Per-page capture timeout in milliseconds. Increase for slow pages; timeout artifacts are returned as structured capture failures."),
|
|
4424
|
+
debug: import_zod3.z.boolean().default(false).describe("Include sanitized browser/proxy diagnostics for page snapshot debugging. Use true for capture, network, or proxy troubleshooting.")
|
|
2896
4425
|
};
|
|
2897
4426
|
|
|
2898
4427
|
// src/mcp/rank-tracker-blueprint.ts
|
|
@@ -3240,7 +4769,7 @@ function localPlanningToolAnnotations(title) {
|
|
|
3240
4769
|
function listSavedReports() {
|
|
3241
4770
|
try {
|
|
3242
4771
|
const dir = outputBaseDir2();
|
|
3243
|
-
return (0,
|
|
4772
|
+
return (0, import_node_fs5.readdirSync)(dir).filter((f) => f.endsWith(".md")).map((f) => ({ filename: f, mtimeMs: (0, import_node_fs5.statSync)((0, import_node_path7.join)(dir, f)).mtimeMs })).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, 100);
|
|
3244
4773
|
} catch {
|
|
3245
4774
|
return [];
|
|
3246
4775
|
}
|
|
@@ -3264,9 +4793,9 @@ function registerSavedReportResources(server2) {
|
|
|
3264
4793
|
},
|
|
3265
4794
|
async (uri, variables) => {
|
|
3266
4795
|
const requested = Array.isArray(variables.filename) ? variables.filename[0] : variables.filename;
|
|
3267
|
-
const filename = (0,
|
|
4796
|
+
const filename = (0, import_node_path7.basename)(decodeURIComponent(String(requested ?? "")));
|
|
3268
4797
|
if (!filename.endsWith(".md")) throw new Error("Only saved .md reports can be read");
|
|
3269
|
-
const text = (0,
|
|
4798
|
+
const text = (0, import_node_fs5.readFileSync)((0, import_node_path7.join)(outputBaseDir2(), filename), "utf8");
|
|
3270
4799
|
return { contents: [{ uri: uri.href, mimeType: "text/markdown", text }] };
|
|
3271
4800
|
}
|
|
3272
4801
|
);
|
|
@@ -3278,70 +4807,72 @@ function registerPaaExtractorMcpTools(server2, executor, options = {}) {
|
|
|
3278
4807
|
if (savesReports) registerSavedReportResources(server2);
|
|
3279
4808
|
server2.registerTool("harvest_paa", {
|
|
3280
4809
|
title: "Google PAA + SERP Harvest",
|
|
3281
|
-
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").
|
|
4810
|
+
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"). Omit proxyMode for normal use; the service defaults to the configured browser-service proxy without city/ZIP targeting for the highest general success rate. Use proxyMode location only when the user explicitly needs city/ZIP-targeted residential proxy evidence. 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.'),
|
|
3282
4811
|
inputSchema: HarvestPaaInputSchema,
|
|
3283
4812
|
outputSchema: HarvestPaaOutputSchema,
|
|
3284
4813
|
annotations: liveWebToolAnnotations("Google PAA + SERP Harvest")
|
|
3285
4814
|
}, async (input) => formatHarvestPaa(await executor.harvestPaa(input), input));
|
|
3286
4815
|
server2.registerTool("search_serp", {
|
|
3287
4816
|
title: "Google SERP Lookup",
|
|
3288
|
-
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.
|
|
4817
|
+
description: withReportNote("Fast Google SERP lookup without PAA expansion. Use when the user asks for rankings, organic results, local pack, quick SERP, or positions. Split topic from location and infer gl/hl from the user request. Omit proxyMode for normal use; the service defaults to the configured browser-service proxy without city/ZIP targeting. Use proxyMode location and proxyZip only when the user explicitly needs city/ZIP-targeted residential proxy evidence."),
|
|
3289
4818
|
inputSchema: SearchSerpInputSchema,
|
|
3290
4819
|
outputSchema: SearchSerpOutputSchema,
|
|
3291
4820
|
annotations: liveWebToolAnnotations("Google SERP Lookup")
|
|
3292
4821
|
}, async (input) => formatSearchSerp(await executor.searchSerp(input), input));
|
|
3293
4822
|
server2.registerTool("extract_url", {
|
|
3294
4823
|
title: "Single URL Extract",
|
|
3295
|
-
description: withReportNote("Extract structured data from one public URL
|
|
4824
|
+
description: withReportNote("Extract structured data from one public URL when the user provides one page, asks to inspect/scrape a page, or needs page content, schema, headings, metadata, screenshots, branding, or media assets. Returns structured page fields plus artifact handles for saved reports/screenshots/media when requested. Use map_site_urls before extracting a site inventory; use extract_site for multi-page crawling."),
|
|
3296
4825
|
inputSchema: ExtractUrlInputSchema,
|
|
3297
4826
|
outputSchema: ExtractUrlOutputSchema,
|
|
3298
4827
|
annotations: liveWebToolAnnotations("Single URL Extract")
|
|
3299
4828
|
}, async (input) => formatExtractUrl(await executor.extractUrl(input), input));
|
|
3300
4829
|
server2.registerTool("map_site_urls", {
|
|
3301
4830
|
title: "Site URL Map",
|
|
3302
|
-
description: withReportNote("Map/crawl a public website
|
|
4831
|
+
description: withReportNote("Map/crawl a public website when the user asks for a sitemap, URL inventory, broken-link scan, redirect scan, or crawl planning. Returns internal URLs with HTTP status and truncation metadata. Use this before extract_site when choosing pages for an audit; use extract_url for one known page."),
|
|
3303
4832
|
inputSchema: MapSiteUrlsInputSchema,
|
|
3304
4833
|
outputSchema: MapSiteUrlsOutputSchema,
|
|
3305
4834
|
annotations: liveWebToolAnnotations("Site URL Map")
|
|
3306
4835
|
}, async (input) => formatMapSiteUrls(await executor.mapSiteUrls(input), input));
|
|
3307
4836
|
server2.registerTool("extract_site", {
|
|
3308
4837
|
title: "Multi-Page Site Extract",
|
|
3309
|
-
description: withReportNote("Run multi-page extraction across a public website. Returns per-page titles, H1s, metadata, headings, schema/entity data, canonical URLs, and content. Use
|
|
4838
|
+
description: withReportNote("Run multi-page extraction across a public website when the user asks for a website audit, competitor audit, full-site content crawl, schema inventory, or page metadata review. Returns per-page titles, H1s, metadata, headings, schema/entity data, canonical URLs, and content. Use map_site_urls first when URL selection matters; use extract_url for one page."),
|
|
3310
4839
|
inputSchema: ExtractSiteInputSchema,
|
|
3311
4840
|
outputSchema: ExtractSiteOutputSchema,
|
|
3312
4841
|
annotations: liveWebToolAnnotations("Multi-Page Site Extract")
|
|
3313
4842
|
}, async (input) => formatExtractSite(await executor.extractSite(input), input));
|
|
3314
4843
|
server2.registerTool("youtube_harvest", {
|
|
3315
4844
|
title: "YouTube Video Harvest",
|
|
3316
|
-
description: withReportNote('Harvest YouTube video metadata by
|
|
4845
|
+
description: withReportNote('Harvest YouTube video metadata when the user wants to find videos by topic, inspect a channel library, compare video angles, or get videoIds for later transcription. Use mode "search" for keyword/topic requests and mode "channel" for @handles, channel IDs, or channel URLs. Returns titles, views, durations, URLs, and videoIds for follow-up transcription. Use youtube_transcribe after selecting one video.'),
|
|
3317
4846
|
inputSchema: YoutubeHarvestInputSchema,
|
|
3318
4847
|
outputSchema: YoutubeHarvestOutputSchema,
|
|
3319
4848
|
annotations: liveWebToolAnnotations("YouTube Video Harvest")
|
|
3320
4849
|
}, async (input) => formatYoutubeHarvest(await executor.youtubeHarvest(input), input));
|
|
3321
4850
|
server2.registerTool("youtube_transcribe", {
|
|
3322
4851
|
title: "YouTube Transcription",
|
|
3323
|
-
description: withReportNote("Fetch and transcribe captions from a YouTube video. Returns full transcript, timestamped chunks,
|
|
4852
|
+
description: withReportNote("Fetch and transcribe captions from a YouTube video. Use this when the user asks what was said in a YouTube video, wants claims/offers/lessons extracted from a video, or provides a YouTube URL for transcript work. Returns full transcript, timestamped chunks, word count, and resolvedInputs. Pass videoId from youtube_harvest results, or pass url when the user pasted a YouTube URL. Use youtube_harvest first when you need to discover videos by topic/channel."),
|
|
3324
4853
|
inputSchema: YoutubeTranscribeInputSchema,
|
|
4854
|
+
outputSchema: YoutubeTranscribeOutputSchema,
|
|
3325
4855
|
annotations: liveWebToolAnnotations("YouTube Transcription")
|
|
3326
4856
|
}, async (input) => formatYoutubeTranscribe(await executor.youtubeTranscribe(input), input));
|
|
3327
4857
|
server2.registerTool("facebook_page_intel", {
|
|
3328
4858
|
title: "Facebook Advertiser Ad Intel",
|
|
3329
|
-
description: withReportNote("Harvest ads from a Facebook advertiser. Returns ad copy, headlines, CTAs, creative type, status, landing URLs, and direct ad video URLs ready for facebook_ad_transcribe. Accepts pageId, libraryId, or a brand/advertiser name as query. Use
|
|
4859
|
+
description: withReportNote("Harvest ads from a Facebook advertiser when the user wants current ad copy, creative angles, CTAs, video URLs, or competitive ad intelligence for one brand/page. Returns ad copy, headlines, CTAs, creative type, status, landing URLs, and direct ad video URLs ready for facebook_ad_transcribe. Accepts pageId, libraryId, or a brand/advertiser name as query. Use facebook_ad_search first when the advertiser handle is unknown. For normal public Facebook reels/posts/watch/share URLs, use facebook_video_transcribe instead."),
|
|
3330
4860
|
inputSchema: FacebookPageIntelInputSchema,
|
|
3331
4861
|
outputSchema: FacebookPageIntelOutputSchema,
|
|
3332
4862
|
annotations: liveWebToolAnnotations("Facebook Advertiser Ad Intel")
|
|
3333
4863
|
}, async (input) => formatFacebookPageIntel(await executor.facebookPageIntel(input), input));
|
|
3334
4864
|
server2.registerTool("facebook_ad_search", {
|
|
3335
4865
|
title: "Facebook Ad Library Search",
|
|
3336
|
-
description: withReportNote("Search Facebook Ad Library by brand,
|
|
4866
|
+
description: withReportNote("Search Facebook Ad Library when the user wants to find advertisers by brand, competitor, niche, or keyword. Returns advertisers with ad counts and library IDs. Use this to discover competitor/page handles, then pass libraryId or pageId to facebook_page_intel for ad details."),
|
|
3337
4867
|
inputSchema: FacebookAdSearchInputSchema,
|
|
3338
4868
|
outputSchema: FacebookAdSearchOutputSchema,
|
|
3339
4869
|
annotations: liveWebToolAnnotations("Facebook Ad Library Search")
|
|
3340
4870
|
}, async (input) => formatFacebookAdSearch(await executor.facebookAdSearch(input), input));
|
|
3341
4871
|
server2.registerTool("facebook_ad_transcribe", {
|
|
3342
4872
|
title: "Facebook Ad Transcription",
|
|
3343
|
-
description: "Transcribe audio from a Facebook ad video CDN URL. Returns full transcript
|
|
4873
|
+
description: "Transcribe audio from a Facebook ad video CDN URL. Use this when facebook_page_intel returned a direct videoUrl and the user asks what the ad says, what claims it makes, or wants ad-message extraction. Returns full transcript, timestamped chunks, word count, and resolvedInputs. Use only with the direct videoUrl value from facebook_page_intel results; do not pass public Facebook post/reel/share URLs. Use facebook_video_transcribe for organic Facebook URLs, and facebook_page_intel first when you only have a brand/page/ad library handle.",
|
|
3344
4874
|
inputSchema: FacebookAdTranscribeInputSchema,
|
|
4875
|
+
outputSchema: FacebookAdTranscribeOutputSchema,
|
|
3345
4876
|
annotations: liveWebToolAnnotations("Facebook Ad Transcription")
|
|
3346
4877
|
}, async (input) => formatFacebookAdTranscribe(await executor.facebookAdTranscribe(input), input));
|
|
3347
4878
|
server2.registerTool("facebook_video_transcribe", {
|
|
@@ -3360,14 +4891,14 @@ function registerPaaExtractorMcpTools(server2, executor, options = {}) {
|
|
|
3360
4891
|
}, async (input) => formatMapsPlaceIntel(await executor.mapsPlaceIntel(input), input));
|
|
3361
4892
|
server2.registerTool("maps_search", {
|
|
3362
4893
|
title: "Google Maps Business Search",
|
|
3363
|
-
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."
|
|
4894
|
+
description: withReportNote('Search Google Maps for multiple businesses/profiles by category, niche, keyword, or local market. Use this when the user asks for several Google Business Profiles, GMBs, GBPs, leads, prospects, competitors, or "more than the 3-pack." Maps is the default location-targeted surface: omit proxyMode for normal Maps use so the service creates residential proxy evidence for the requested market and rotates on retryable failures. Pass proxyZip only when a specific ZIP or city-center ZIP is known. Use proxyMode configured only when you explicitly do not want city/ZIP proxy targeting. Returns up to 50 candidates with names, place URLs, CIDs when available, ratings, review counts, profile metadata, and sanitized attempt telemetry. Default maxResults is 10; maximum is 50. Use maps_place_intel afterward only when a selected business needs full details and reviews.'),
|
|
3364
4895
|
inputSchema: MapsSearchInputSchema,
|
|
3365
4896
|
outputSchema: MapsSearchOutputSchema,
|
|
3366
4897
|
annotations: liveWebToolAnnotations("Google Maps Business Search")
|
|
3367
4898
|
}, async (input) => formatMapsSearch(await executor.mapsSearch(input), input));
|
|
3368
4899
|
server2.registerTool("directory_workflow", {
|
|
3369
4900
|
title: "Directory Workflow: Markets + Maps",
|
|
3370
|
-
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.
|
|
4901
|
+
description: withReportNote('Build directory/prospecting datasets by selecting US city markets from the free Census Population Estimates city/place dataset, optionally joining configured US ZIPS/Lead Magician ZIP groups, then running Google Maps business searches for each city in parallel. Use this when the user wants "all cities over 100k population in a state", "build a directory CSV", "find markets then get Maps data", or similar location-database + Maps workflows. Set minPopulation, state, query, maxResultsPerCity, and concurrency. Maps workflows default to location-targeted proxying so each city can use city/state or ZIP-group residential proxy evidence; use proxyMode configured only when you explicitly do not want city/ZIP proxy targeting. Saved CSV rows include source_location, result_position, business_name, review_stars, review_count, category, address, phone, hours_status, website_url, directions_url, place_url, cid, cid_decimal, city population, and ZIP groups. Structured city results include sanitized attempt telemetry. Use maps_place_intel only when a selected profile needs deeper review topics, profile review count confirmation, or review cards. For local Lead Magician ZIP enrichment, set MCP_SCRAPER_USZIPS_CSV_PATH on the API server or pass usZipsCsvPath only in local/test mode.'),
|
|
3371
4902
|
inputSchema: DirectoryWorkflowInputSchema,
|
|
3372
4903
|
outputSchema: DirectoryWorkflowOutputSchema,
|
|
3373
4904
|
annotations: liveWebToolAnnotations("Directory Workflow: Markets + Maps")
|
|
@@ -3388,21 +4919,28 @@ function registerPaaExtractorMcpTools(server2, executor, options = {}) {
|
|
|
3388
4919
|
}, async (input) => formatWorkflowSuggest(input));
|
|
3389
4920
|
server2.registerTool("workflow_run", {
|
|
3390
4921
|
title: "Run Workflow",
|
|
3391
|
-
description: withReportNote("
|
|
4922
|
+
description: withReportNote("Start a higher-level MCP Scraper workflow. Use after workflow_suggest or workflow_list. Runnable workflow ids: directory, agent-packet, local-competitive-audit, map-comparison, serp-comparison, paa-expansion-brief, ai-overview-language. This is the main MCP tool for market analysis, ICP evidence packets, local competitive audits, Maps/SERP comparisons, content gap briefs, and AI Overview language guidance. Stepwise workflows (e.g. agent-packet) run ONE leg per call and return runId, the step output, and nextStep \u2014 when nextStep is present, call workflow_step with the runId to run the next leg, and keep calling it until done is true. This keeps each call short instead of one long blocking run, so report each step result to the user as it arrives."),
|
|
3392
4923
|
inputSchema: WorkflowRunInputSchema,
|
|
3393
4924
|
outputSchema: WorkflowRunOutputSchema,
|
|
3394
4925
|
annotations: liveWebToolAnnotations("Run Workflow")
|
|
3395
4926
|
}, async (input) => formatWorkflowRun(await executor.workflowRun(input), input));
|
|
4927
|
+
server2.registerTool("workflow_step", {
|
|
4928
|
+
title: "Advance Workflow Step",
|
|
4929
|
+
description: withReportNote("Run the next leg of a stepwise MCP Scraper workflow started with workflow_run. Pass the runId. Each call executes exactly one logical step (typically one live harvest), persists that step's artifacts, and returns the step output plus nextStep. Keep calling workflow_step with the same runId until done is true, reporting each step result to the user as it lands. Use this instead of waiting on one long workflow call \u2014 it avoids client timeouts on long multi-step jobs."),
|
|
4930
|
+
inputSchema: WorkflowStepInputSchema,
|
|
4931
|
+
outputSchema: WorkflowStepOutputSchema,
|
|
4932
|
+
annotations: liveWebToolAnnotations("Advance Workflow Step")
|
|
4933
|
+
}, async (input) => formatWorkflowStep(await executor.workflowStep(input), input));
|
|
3396
4934
|
server2.registerTool("workflow_status", {
|
|
3397
4935
|
title: "Workflow Status",
|
|
3398
|
-
description: "Fetch a hosted workflow run by id and list its current status and artifacts. Use
|
|
4936
|
+
description: "Fetch a hosted workflow run by id and list its current status and artifacts. Use when a workflow may still be running, when the model needs to re-open a run, inspect artifact ids, recover from a long workflow, or decide whether to call workflow_step or workflow_artifact_read next. Use only a runId returned by workflow_run/workflow_step/workflow_status; do not construct one yourself.",
|
|
3399
4937
|
inputSchema: WorkflowStatusInputSchema,
|
|
3400
4938
|
outputSchema: WorkflowStatusOutputSchema,
|
|
3401
4939
|
annotations: liveWebToolAnnotations("Workflow Status")
|
|
3402
4940
|
}, async (input) => formatWorkflowStatus(await executor.workflowStatus(input), input));
|
|
3403
4941
|
server2.registerTool("workflow_artifact_read", {
|
|
3404
4942
|
title: "Read Workflow Artifact",
|
|
3405
|
-
description: "Read a workflow artifact back into MCP context by run id and artifact id. Use this before writing final deliverables so the answer is grounded in generated evidence.json, CSVs, Markdown briefs, reports, and task files instead of memory. Use maxBytes to limit large CSV/JSON artifacts.",
|
|
4943
|
+
description: "Read a workflow artifact back into MCP context by run id and artifact id. Use this before writing final deliverables so the answer is grounded in generated evidence.json, CSVs, Markdown briefs, reports, and task files instead of memory. Use workflow_status first when artifact ids are unknown. Use only artifactId values returned by workflow_run/workflow_step/workflow_status; do not construct one yourself. Use maxBytes to limit large CSV/JSON artifacts.",
|
|
3406
4944
|
inputSchema: WorkflowArtifactReadInputSchema,
|
|
3407
4945
|
outputSchema: WorkflowArtifactReadOutputSchema,
|
|
3408
4946
|
annotations: liveWebToolAnnotations("Read Workflow Artifact")
|
|
@@ -3482,7 +5020,7 @@ function renderInstallTerminal(options) {
|
|
|
3482
5020
|
"1/1 install surfaces ready",
|
|
3483
5021
|
colorize("Newest: MCP workflow tools for market analysis, ICP research, CRO audits, brand briefs, and AI search visibility.", "lime", color),
|
|
3484
5022
|
"",
|
|
3485
|
-
`${colorize("Tools", "cyan", color)} ${colorize("(
|
|
5023
|
+
`${colorize("Tools", "cyan", color)} ${colorize("(39 MCP tools)", "muted", color)}`,
|
|
3486
5024
|
toolRow("search", ["harvest_paa", "search_serp", "maps_search", "maps_place_intel"], color),
|
|
3487
5025
|
toolRow("extract", ["extract_url", "map_site_urls", "extract_site", "directory_workflow"], color),
|
|
3488
5026
|
toolRow("build", ["rank_tracker_blueprint", "cron plan", "database prompt"], color),
|
|
@@ -3490,7 +5028,7 @@ function renderInstallTerminal(options) {
|
|
|
3490
5028
|
toolRow("browser", ["browser_open", "browser_screenshot", "browser_read", "browser_locate", "browser_replay_mark", "browser_replay_annotate"], color),
|
|
3491
5029
|
toolRow("account", ["credits_info", "local reports", "MCP resources"], color),
|
|
3492
5030
|
`${colorize("Workflows", "cyan", color)} ${colorize("(MCP + CLI + API)", "muted", color)}`,
|
|
3493
|
-
toolRow("route", ["workflow_list", "workflow_suggest", "workflow_run", "workflow_status", "workflow_artifact_read"], color),
|
|
5031
|
+
toolRow("route", ["workflow_list", "workflow_suggest", "workflow_run", "workflow_step", "workflow_status", "workflow_artifact_read"], color),
|
|
3494
5032
|
toolRow("seo", ["directory", "agent-packet", "local audit", "map/serp comparison", "PAA/AIO briefs", "scheduled runs"], color),
|
|
3495
5033
|
"",
|
|
3496
5034
|
colorize("Usage tips:", "amber", color),
|
|
@@ -3536,10 +5074,10 @@ if (!forceStdio && (interactiveTerminal || wantsHelp)) {
|
|
|
3536
5074
|
}
|
|
3537
5075
|
function readApiKeyFile() {
|
|
3538
5076
|
const explicitPath = process.env.MCP_SCRAPER_KEY_PATH?.trim();
|
|
3539
|
-
const paths = [explicitPath, (0,
|
|
5077
|
+
const paths = [explicitPath, (0, import_node_path8.join)((0, import_node_os6.homedir)(), ".mcp-scraper-key")].filter(Boolean);
|
|
3540
5078
|
for (const path of paths) {
|
|
3541
5079
|
try {
|
|
3542
|
-
const value = (0,
|
|
5080
|
+
const value = (0, import_node_fs6.readFileSync)(path, "utf8").trim();
|
|
3543
5081
|
if (value) return value;
|
|
3544
5082
|
} catch {
|
|
3545
5083
|
}
|