mcp-scraper 0.3.26 → 0.3.28
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/dist/bin/api-server.cjs +2367 -305
- package/dist/bin/api-server.cjs.map +1 -1
- package/dist/bin/api-server.js +2 -2
- package/dist/bin/mcp-scraper-cli.cjs +1 -1
- package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
- package/dist/bin/mcp-scraper-cli.js +1 -1
- package/dist/bin/mcp-scraper-install.cjs +2 -2
- 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 +132 -9
- package/dist/bin/mcp-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-stdio-server.js +13 -1357
- package/dist/bin/mcp-stdio-server.js.map +1 -1
- package/dist/bin/paa-harvest.cjs +1 -1
- package/dist/bin/paa-harvest.cjs.map +1 -1
- package/dist/bin/paa-harvest.js +1 -1
- package/dist/chunk-7MY2NSQX.js +7 -0
- package/dist/chunk-7MY2NSQX.js.map +1 -0
- package/dist/{chunk-WYCER2HW.js → chunk-H74L743B.js} +12 -11
- package/dist/chunk-H74L743B.js.map +1 -0
- package/dist/{chunk-2GHI2D6Q.js → chunk-NGD4ITAO.js} +1383 -17
- package/dist/chunk-NGD4ITAO.js.map +1 -0
- package/dist/{chunk-G7PQ64LM.js → chunk-QSSH4RCX.js} +2 -2
- package/dist/chunk-QSSH4RCX.js.map +1 -0
- package/dist/{chunk-DOBQN3EY.js → chunk-T3VZD2I4.js} +2 -2
- package/dist/{chunk-DOBQN3EY.js.map → chunk-T3VZD2I4.js.map} +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/dist/{server-HIGHKTEV.js → server-IDDX7KGT.js} +696 -42
- package/dist/server-IDDX7KGT.js.map +1 -0
- package/dist/{worker-UC6D2756.js → worker-NV4GTIRG.js} +3 -3
- package/docs/spec-kernel-computer-controls.md +166 -0
- package/package.json +1 -1
- package/dist/chunk-2GHI2D6Q.js.map +0 -1
- package/dist/chunk-G7PQ64LM.js.map +0 -1
- package/dist/chunk-MS2UZ6HR.js +0 -7
- package/dist/chunk-MS2UZ6HR.js.map +0 -1
- package/dist/chunk-WYCER2HW.js.map +0 -1
- package/dist/server-HIGHKTEV.js.map +0 -1
- /package/dist/{worker-UC6D2756.js.map → worker-NV4GTIRG.js.map} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
PACKAGE_VERSION
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-7MY2NSQX.js";
|
|
4
4
|
import {
|
|
5
5
|
browserServiceProfileName,
|
|
6
6
|
browserServiceProfileSaveChanges,
|
|
@@ -913,9 +913,18 @@ function parseData(raw) {
|
|
|
913
913
|
function entityIdsSection(ids) {
|
|
914
914
|
if (!ids) return "";
|
|
915
915
|
const lines = [];
|
|
916
|
-
if (ids.
|
|
917
|
-
|
|
918
|
-
|
|
916
|
+
if (ids.entities?.length) {
|
|
917
|
+
for (const e of ids.entities) {
|
|
918
|
+
const idParts = [e.kgId && `MID ${e.kgId}`, e.cid && `CID ${e.cid}`, e.gcid && `GCID ${e.gcid}`].filter(Boolean);
|
|
919
|
+
if (idParts.length) lines.push(`- **${e.name}** \u2014 ${idParts.join(", ")}`);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
const linkedNames = new Set((ids.entities ?? []).map((e) => e.name));
|
|
923
|
+
if (!linkedNames.size) {
|
|
924
|
+
if (ids.kgIds?.length) lines.push(`- **Knowledge Graph MID:** ${ids.kgIds.join(", ")}`);
|
|
925
|
+
if (ids.cids?.length) lines.push(`- **CID:** ${ids.cids.join(", ")}`);
|
|
926
|
+
if (ids.gcids?.length) lines.push(`- **GCID:** ${ids.gcids.join(", ")}`);
|
|
927
|
+
}
|
|
919
928
|
return lines.length ? `
|
|
920
929
|
## Entity IDs
|
|
921
930
|
${lines.join("\n")}` : "";
|
|
@@ -1037,7 +1046,7 @@ ${paaTable}${serpTable}${entityIdsSection(entityIds)}${aiSection}${statsLine}${d
|
|
|
1037
1046
|
snippet: r.snippet ?? null
|
|
1038
1047
|
})),
|
|
1039
1048
|
aiOverview: aiOvw ? { detected: aiOvw.detected === true, text: aiOvw.text ?? null } : null,
|
|
1040
|
-
entityIds: entityIds ? { kgIds: entityIds.kgIds ?? [], cids: entityIds.cids ?? [], gcids: entityIds.gcids ?? [] } : null,
|
|
1049
|
+
entityIds: entityIds ? { entities: entityIds.entities ?? [], kgIds: entityIds.kgIds ?? [], cids: entityIds.cids ?? [], gcids: entityIds.gcids ?? [] } : null,
|
|
1041
1050
|
durationMs: durationMs ?? null
|
|
1042
1051
|
}
|
|
1043
1052
|
};
|
|
@@ -1098,7 +1107,7 @@ ${serpTable}${localSection}${entityIdsSection(entityIds)}${aiSection}${debugSect
|
|
|
1098
1107
|
websiteUrl: b.websiteUrl ?? null
|
|
1099
1108
|
})),
|
|
1100
1109
|
aiOverview: aiOvw ? { detected: aiOvw.detected === true, text: aiOvw.text ?? null } : null,
|
|
1101
|
-
entityIds: entityIds ? { kgIds: entityIds.kgIds ?? [], cids: entityIds.cids ?? [], gcids: entityIds.gcids ?? [] } : null
|
|
1110
|
+
entityIds: entityIds ? { entities: entityIds.entities ?? [], kgIds: entityIds.kgIds ?? [], cids: entityIds.cids ?? [], gcids: entityIds.gcids ?? [] } : null
|
|
1102
1111
|
}
|
|
1103
1112
|
};
|
|
1104
1113
|
}
|
|
@@ -2923,6 +2932,12 @@ var AiOverviewOutput = z.object({
|
|
|
2923
2932
|
text: NullableString
|
|
2924
2933
|
}).nullable();
|
|
2925
2934
|
var EntityIdsOutput = z.object({
|
|
2935
|
+
entities: z.array(z.object({
|
|
2936
|
+
name: z.string(),
|
|
2937
|
+
kgId: z.string().nullable(),
|
|
2938
|
+
cid: z.string().nullable(),
|
|
2939
|
+
gcid: z.string().nullable()
|
|
2940
|
+
})).describe("Each entity actually named on the page, with whichever of its kgId/cid/gcid were found \u2014 the linked view. kgIds/cids/gcids below are the same IDs as flat deduplicated lists, without the name link, kept for backward compatibility."),
|
|
2926
2941
|
kgIds: z.array(z.string()),
|
|
2927
2942
|
cids: z.array(z.string()),
|
|
2928
2943
|
gcids: z.array(z.string())
|
|
@@ -3693,7 +3708,7 @@ function buildPrompt(input, modes, tools, tables, jobs, metrics, expression, tar
|
|
|
3693
3708
|
"- Every scheduled batch must insert or update rank_tracker_runs before calling MCP tools.",
|
|
3694
3709
|
"- Use idempotency_key = project_id + keyword_id + location_id + mode + device + scheduled_date.",
|
|
3695
3710
|
"- Mark runs as running, succeeded, failed, or skipped; persist error_message on failures.",
|
|
3696
|
-
"-
|
|
3711
|
+
"- Call credits_info first to read the account's real concurrency_limit (most paid accounts have 3, 10, or 20, not 1), then run up to that many MCP calls in parallel \u2014 never sequential-by-default and never more than the limit, or calls start failing with 429s.",
|
|
3697
3712
|
"- Retry retryable upstream failures with backoff, but do not duplicate rows for the same idempotency key.",
|
|
3698
3713
|
"",
|
|
3699
3714
|
"Mode-specific ingestion rules:",
|
|
@@ -3786,6 +3801,22 @@ function liveWebToolAnnotations(title) {
|
|
|
3786
3801
|
openWorldHint: true
|
|
3787
3802
|
};
|
|
3788
3803
|
}
|
|
3804
|
+
function registerSerpIntelligenceCaptureTools(server, executor) {
|
|
3805
|
+
server.registerTool("capture_serp_snapshot", {
|
|
3806
|
+
title: "SERP Intelligence Snapshot",
|
|
3807
|
+
description: "Capture a structured SERP Intelligence Google snapshot through POST /serp-intelligence/capture, the same product capture path used by Phoenix. Split query from location and infer gl/hl. Routing is automatic \u2014 leave proxyMode unset. Set debug true when investigating location evidence, proxy behavior, CAPTCHA, or capture reliability.",
|
|
3808
|
+
inputSchema: CaptureSerpSnapshotInputSchema,
|
|
3809
|
+
outputSchema: CaptureSerpSnapshotOutputSchema,
|
|
3810
|
+
annotations: liveWebToolAnnotations("SERP Intelligence Snapshot")
|
|
3811
|
+
}, async (input) => formatCaptureSerpSnapshot(await executor.captureSerpSnapshot(input), input));
|
|
3812
|
+
server.registerTool("capture_serp_page_snapshots", {
|
|
3813
|
+
title: "SERP Intelligence Page Snapshots",
|
|
3814
|
+
description: "Capture public ranking-page evidence through POST /serp-intelligence/page-snapshots, the same product page snapshot path used by Phoenix. Provide urls for simple captures or targets when preserving organic, AI citation, local-pack, configured target, or site-subject source metadata. Private IPs, localhost, file URLs, and internal URLs are rejected by the service. Use timeoutMs for slow pages and debug true for sanitized proxy/browser diagnostics.",
|
|
3815
|
+
inputSchema: CaptureSerpPageSnapshotsInputSchema,
|
|
3816
|
+
outputSchema: CaptureSerpPageSnapshotsOutputSchema,
|
|
3817
|
+
annotations: liveWebToolAnnotations("SERP Intelligence Page Snapshots")
|
|
3818
|
+
}, async (input) => formatCaptureSerpPageSnapshots(await executor.captureSerpPageSnapshots(input), input));
|
|
3819
|
+
}
|
|
3789
3820
|
function localPlanningToolAnnotations(title) {
|
|
3790
3821
|
return {
|
|
3791
3822
|
title,
|
|
@@ -4295,6 +4326,12 @@ var HttpMcpToolExecutor = class {
|
|
|
4295
4326
|
}
|
|
4296
4327
|
};
|
|
4297
4328
|
|
|
4329
|
+
// src/mcp/browser-agent-mcp-server.ts
|
|
4330
|
+
import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4331
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
4332
|
+
import { homedir as homedir3 } from "os";
|
|
4333
|
+
import { join as join5 } from "path";
|
|
4334
|
+
|
|
4298
4335
|
// src/services/fanout/export.ts
|
|
4299
4336
|
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
4300
4337
|
import { homedir as homedir2 } from "os";
|
|
@@ -4424,9 +4461,1343 @@ render();
|
|
|
4424
4461
|
</body></html>`;
|
|
4425
4462
|
}
|
|
4426
4463
|
|
|
4464
|
+
// src/mcp/browser-agent-tool-schemas.ts
|
|
4465
|
+
import { z as z2 } from "zod";
|
|
4466
|
+
var NullableString2 = z2.string().nullable();
|
|
4467
|
+
var BrowserRawObject = z2.record(z2.unknown());
|
|
4468
|
+
var BrowserElementOutput = z2.record(z2.unknown());
|
|
4469
|
+
var BrowserBaseOutput = {
|
|
4470
|
+
ok: z2.boolean().describe("Whether the browser-agent action succeeded."),
|
|
4471
|
+
tool: z2.string().describe("Browser Agent MCP tool that produced this response."),
|
|
4472
|
+
session_id: NullableString2.describe("Browser session id when the response is scoped to a session.")
|
|
4473
|
+
};
|
|
4474
|
+
var BrowserReplayBaseOutput = {
|
|
4475
|
+
...BrowserBaseOutput,
|
|
4476
|
+
replay_id: NullableString2.describe("Replay id when the response is scoped to a replay.")
|
|
4477
|
+
};
|
|
4478
|
+
var BrowserOpenInputSchema = {
|
|
4479
|
+
label: z2.string().optional().describe("Optional human label for this session, shown in the watch console."),
|
|
4480
|
+
url: z2.string().url().optional().describe("Optional URL to navigate to immediately after opening."),
|
|
4481
|
+
profile: z2.string().optional().describe("Optional saved hosted profile name to load a logged-in session for a site."),
|
|
4482
|
+
save_profile_changes: z2.boolean().optional().describe("Persist cookies and browser storage back to the named profile when the session is closed. Use this for profile setup or intentional auth refreshes; avoid parallel sessions writing to the same profile."),
|
|
4483
|
+
timeout_seconds: z2.number().int().min(60).max(259200).optional().describe("How long the direct no-proxy browser session may live before auto-termination. Defaults to 600. The browser idles into a zero-cost standby between actions, so a longer timeout is cheap.")
|
|
4484
|
+
};
|
|
4485
|
+
var BrowserProfileConnectInputSchema = {
|
|
4486
|
+
email: z2.string().optional().describe("Account email for the login being saved. Used to derive a stable, reusable profile name AND recorded as a note of who is connected. Does NOT import existing cookies \u2014 the user signs in fresh via the returned watch_url."),
|
|
4487
|
+
profile: z2.string().optional().describe("Profile to add this login to. Omit to derive one from email. A single profile holds MANY site logins \u2014 pass the same profile name again with a different domain to stack another account (e.g. add claude.ai to a profile that already has chatgpt.com)."),
|
|
4488
|
+
domain: z2.string().optional().describe("Site to log into, e.g. chatgpt.com, claude.ai, reddit.com. Defaults to chatgpt.com. Each domain is a separate login inside the profile."),
|
|
4489
|
+
login_url: z2.string().url().optional().describe("Login page for the domain. Defaults to https://<domain>/ (https://chatgpt.com/ when domain is omitted)."),
|
|
4490
|
+
url: z2.string().url().optional().describe("Deprecated alias for login_url. Use login_url."),
|
|
4491
|
+
note: z2.string().optional().describe('Free-text note describing this login, e.g. "ChatGPT Plus, work account". Surfaced by browser_profile_list so you can see what the profile holds.'),
|
|
4492
|
+
label: z2.string().optional().describe("Optional human label for this sign-in setup session."),
|
|
4493
|
+
timeout_seconds: z2.number().int().min(60).max(259200).optional().describe("How long the sign-in setup session may live before auto-termination. Defaults to 600.")
|
|
4494
|
+
};
|
|
4495
|
+
var BrowserProfileListInputSchema = {
|
|
4496
|
+
profile: z2.string().optional().describe("Profile whose saved logins to list. Omit to derive from email."),
|
|
4497
|
+
email: z2.string().optional().describe("Account email used to derive the profile name when profile is not given."),
|
|
4498
|
+
domain: z2.string().optional().describe("Restrict to one site login, e.g. chatgpt.com. Use this to poll a single login until its status reads AUTHENTICATED."),
|
|
4499
|
+
connection_id: z2.string().optional().describe("A specific login connection id returned by browser_profile_connect, to poll just that one.")
|
|
4500
|
+
};
|
|
4501
|
+
var BrowserSessionInputSchema = {
|
|
4502
|
+
session_id: z2.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself.")
|
|
4503
|
+
};
|
|
4504
|
+
var BrowserLocateTargetSchema = z2.object({
|
|
4505
|
+
name: z2.string().optional().describe("Optional label for this target, echoed in the result."),
|
|
4506
|
+
selector: z2.string().optional().describe('CSS selector for the exact DOM element to locate, for example h1, input[name="q"], or [data-testid="result"].'),
|
|
4507
|
+
text: z2.string().optional().describe("Visible text to locate when a selector is not known. The tool returns the text range bounds when possible."),
|
|
4508
|
+
match: z2.enum(["contains", "exact"]).default("contains").describe("How to match text targets. Defaults to contains."),
|
|
4509
|
+
index: z2.number().int().min(0).default(0).describe("Zero-based match index when multiple elements match.")
|
|
4510
|
+
}).refine((value) => Boolean(value.selector || value.text), {
|
|
4511
|
+
message: "target requires selector or text"
|
|
4512
|
+
});
|
|
4513
|
+
var BrowserLocateInputSchema = {
|
|
4514
|
+
session_id: z2.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
4515
|
+
targets: z2.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.")
|
|
4516
|
+
};
|
|
4517
|
+
var BrowserGotoInputSchema = {
|
|
4518
|
+
session_id: z2.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
4519
|
+
url: z2.string().url().describe("URL to navigate the browser to.")
|
|
4520
|
+
};
|
|
4521
|
+
var BrowserClickInputSchema = {
|
|
4522
|
+
session_id: z2.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
4523
|
+
x: z2.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."),
|
|
4524
|
+
y: z2.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."),
|
|
4525
|
+
button: z2.enum(["left", "right", "middle"]).default("left").describe("Mouse button."),
|
|
4526
|
+
num_clicks: z2.number().int().min(1).max(3).optional().describe("Number of clicks, e.g. 2 for double-click.")
|
|
4527
|
+
};
|
|
4528
|
+
var BrowserTypeInputSchema = {
|
|
4529
|
+
session_id: z2.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
4530
|
+
text: z2.string().describe("Text to type at the current focus. Click a field first to focus it."),
|
|
4531
|
+
delay: z2.number().int().min(0).max(500).optional().describe("Optional per-keystroke delay in ms for human-like typing.")
|
|
4532
|
+
};
|
|
4533
|
+
var BrowserScrollInputSchema = {
|
|
4534
|
+
session_id: z2.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
4535
|
+
delta_y: z2.number().default(5).describe("Vertical scroll in wheel units. Positive scrolls down, negative up."),
|
|
4536
|
+
delta_x: z2.number().default(0).describe("Horizontal scroll in wheel units."),
|
|
4537
|
+
x: z2.number().optional().describe("X position to scroll at. Defaults to screen center."),
|
|
4538
|
+
y: z2.number().optional().describe("Y position to scroll at. Defaults to screen center.")
|
|
4539
|
+
};
|
|
4540
|
+
var BrowserPressInputSchema = {
|
|
4541
|
+
session_id: z2.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
4542
|
+
keys: z2.array(z2.string()).min(1).describe('Keys or combinations to press, e.g. ["Return"], ["Ctrl+a"], ["Ctrl+Shift+Tab"].')
|
|
4543
|
+
};
|
|
4544
|
+
var BrowserReplayStopInputSchema = {
|
|
4545
|
+
session_id: z2.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
4546
|
+
replay_id: z2.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.")
|
|
4547
|
+
};
|
|
4548
|
+
var BrowserReplayDownloadInputSchema = {
|
|
4549
|
+
session_id: z2.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
4550
|
+
replay_id: z2.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."),
|
|
4551
|
+
filename: z2.string().optional().describe("Optional local MP4 filename. Defaults to a timestamped replay filename.")
|
|
4552
|
+
};
|
|
4553
|
+
var BrowserReplayAnnotationSchema = z2.object({
|
|
4554
|
+
type: z2.enum(["box", "circle", "underline", "arrow", "label"]).default("box").describe("Annotation style to draw over the replay."),
|
|
4555
|
+
start_seconds: z2.number().min(0).default(0).describe("When the annotation should appear in the replay."),
|
|
4556
|
+
end_seconds: z2.number().min(0).optional().describe("When the annotation should disappear. Defaults to two seconds after start_seconds."),
|
|
4557
|
+
left: z2.number().optional().describe("Target left edge in browser screenshot pixels. Use element.left from browser_screenshot/browser_read."),
|
|
4558
|
+
top: z2.number().optional().describe("Target top edge in browser screenshot pixels. Use element.top from browser_screenshot/browser_read."),
|
|
4559
|
+
width: z2.number().positive().optional().describe("Target width in browser screenshot pixels. Use element.width from browser_screenshot/browser_read."),
|
|
4560
|
+
height: z2.number().positive().optional().describe("Target height in browser screenshot pixels. Use element.height from browser_screenshot/browser_read."),
|
|
4561
|
+
x: z2.number().optional().describe("Point target x coordinate in browser screenshot pixels when no box is available."),
|
|
4562
|
+
y: z2.number().optional().describe("Point target y coordinate in browser screenshot pixels when no box is available."),
|
|
4563
|
+
from_x: z2.number().optional().describe("Arrow start x coordinate in browser screenshot pixels. Defaults near the target."),
|
|
4564
|
+
from_y: z2.number().optional().describe("Arrow start y coordinate in browser screenshot pixels. Defaults near the target."),
|
|
4565
|
+
to_x: z2.number().optional().describe("Arrow target x coordinate in browser screenshot pixels. Defaults to the target box center."),
|
|
4566
|
+
to_y: z2.number().optional().describe("Arrow target y coordinate in browser screenshot pixels. Defaults to the target box center."),
|
|
4567
|
+
label: z2.string().max(120).optional().describe("Optional text callout to render near the annotation."),
|
|
4568
|
+
color: z2.string().regex(/^#?[0-9a-fA-F]{6}$/).optional().describe("Annotation color as hex, for example #ff3b30."),
|
|
4569
|
+
thickness: z2.number().min(1).max(24).optional().describe("Stroke thickness in pixels. Defaults to 5.")
|
|
4570
|
+
});
|
|
4571
|
+
var BrowserReplayMarkInputSchema = {
|
|
4572
|
+
session_id: z2.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."),
|
|
4573
|
+
target: BrowserLocateTargetSchema.describe("The exact DOM element or text range to mark in the current viewport."),
|
|
4574
|
+
type: z2.enum(["box", "circle", "underline", "arrow"]).default("box").describe("Annotation style to generate. Labels are included on the returned annotation when label is provided."),
|
|
4575
|
+
label: z2.string().max(120).optional().describe("Optional callout text to render near the target."),
|
|
4576
|
+
color: z2.string().regex(/^#?[0-9a-fA-F]{6}$/).optional().describe("Annotation color as hex, for example #ff3b30."),
|
|
4577
|
+
thickness: z2.number().min(1).max(24).optional().describe("Stroke thickness in pixels. Defaults to 5."),
|
|
4578
|
+
padding: z2.number().min(0).max(80).default(8).describe("Pixels to expand the DOM bounds so the highlight does not touch the text edge."),
|
|
4579
|
+
start_offset_seconds: z2.number().min(-5).max(10).default(-0.25).describe("Offset from the current replay time. Negative values make the callout appear just before the mark action is captured."),
|
|
4580
|
+
duration_seconds: z2.number().min(0.5).max(30).default(4).describe("How long the generated annotation should remain visible.")
|
|
4581
|
+
};
|
|
4582
|
+
var BrowserReplayAnnotateInputSchema = {
|
|
4583
|
+
session_id: z2.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
4584
|
+
replay_id: z2.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."),
|
|
4585
|
+
annotations: z2.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."),
|
|
4586
|
+
filename: z2.string().optional().describe("Optional output MP4 filename. Defaults to a timestamped annotated replay filename."),
|
|
4587
|
+
source_width: z2.number().positive().optional().describe("Width of the screenshot coordinate space used for annotations. Defaults to the replay video width."),
|
|
4588
|
+
source_height: z2.number().positive().optional().describe("Height of the coordinate space used for annotations. When this is a page viewport height smaller than the replay video height, the annotator infers the browser chrome top offset."),
|
|
4589
|
+
source_left_offset: z2.number().min(0).optional().describe("Optional explicit X offset from annotation coordinates to replay video coordinates. Usually omitted."),
|
|
4590
|
+
source_top_offset: z2.number().min(0).optional().describe("Optional explicit Y offset from annotation coordinates to replay video coordinates. Usually omitted because browser chrome offset is inferred when source_height is a page viewport height.")
|
|
4591
|
+
};
|
|
4592
|
+
var BrowserListInputSchema = {
|
|
4593
|
+
include_closed: z2.boolean().default(false).describe("Include closed sessions in the list.")
|
|
4594
|
+
};
|
|
4595
|
+
var BrowserCaptureFanoutInputSchema = {
|
|
4596
|
+
session_id: z2.string().describe("The session id returned by browser_open or browser_list_sessions. The session must be on chatgpt.com or claude.ai and logged in through a saved hosted profile. Use only a returned session_id; do not construct one yourself."),
|
|
4597
|
+
prompt: z2.string().optional().describe("Optional prompt to type into the chat composer and submit before capturing. Omit to passively capture the fan-out of a prompt the user just ran in the visible browser. The prompt must trigger web search for there to be a fan-out."),
|
|
4598
|
+
wait_ms: z2.number().int().min(0).max(18e4).optional().describe("How long to wait for the answer stream to finish before parsing. Defaults to 90000 when a prompt is sent, 8000 for passive capture. Capture ends as soon as the answer stream finishes."),
|
|
4599
|
+
first_party_domain: z2.string().optional().describe("The brand/site being researched, for example example.com. Sources on this domain are tagged First-party/vendor. Set this when the prompt is about a specific business."),
|
|
4600
|
+
reset: z2.boolean().default(false).describe("Clear any previously buffered stream for this page before capturing. Use when re-running to avoid mixing turns."),
|
|
4601
|
+
export: z2.boolean().default(false).describe("When true, write JSON, CSV, TSV exports (queries, citations, all sources, browsed-only sources, snippets, domains) and a self-contained HTML report to MCP_SCRAPER_OUTPUT_DIR/fanout, returning paths relative to MCP_SCRAPER_OUTPUT_DIR.")
|
|
4602
|
+
};
|
|
4603
|
+
var FanoutSourceOutput = z2.object({
|
|
4604
|
+
url: z2.string(),
|
|
4605
|
+
domain: z2.string(),
|
|
4606
|
+
title: z2.string(),
|
|
4607
|
+
cited: z2.boolean(),
|
|
4608
|
+
timesCited: z2.number().int().min(0),
|
|
4609
|
+
snippet: z2.string(),
|
|
4610
|
+
round: z2.number().int().nullable(),
|
|
4611
|
+
siteType: z2.string().describe("URL category: First-party/vendor, News/media, Reddit, Social/video, Encyclopedia, Review site, Docs, or Blog.")
|
|
4612
|
+
});
|
|
4613
|
+
var BrowserCaptureFanoutOutputSchema = {
|
|
4614
|
+
...BrowserBaseOutput,
|
|
4615
|
+
tool: z2.literal("query_fanout_workflow"),
|
|
4616
|
+
platform: z2.enum(["ChatGPT", "Claude"]).describe("Which AI-search surface the fan-out was captured from."),
|
|
4617
|
+
captured_at: z2.string(),
|
|
4618
|
+
prompt: z2.string().describe("The user prompt that triggered the captured fan-out, when recoverable."),
|
|
4619
|
+
meta: z2.object({
|
|
4620
|
+
model: z2.string(),
|
|
4621
|
+
finishType: z2.string(),
|
|
4622
|
+
title: z2.string(),
|
|
4623
|
+
rounds: z2.number().int().min(0)
|
|
4624
|
+
}),
|
|
4625
|
+
queries: z2.array(z2.string()).describe("Every web-search sub-query the model issued, in capture order. Classify these by funnel stage and type yourself from the text."),
|
|
4626
|
+
browsed_urls: z2.array(FanoutSourceOutput).describe("Every researched URL (cited and browsed-only), cited first."),
|
|
4627
|
+
cited_urls: z2.array(FanoutSourceOutput).describe("The subset of researched URLs actually cited in the final answer."),
|
|
4628
|
+
browsed_only: z2.array(FanoutSourceOutput).describe("Researched URLs the model pulled but did not cite."),
|
|
4629
|
+
snippets: z2.array(z2.object({ url: z2.string(), domain: z2.string(), title: z2.string(), text: z2.string() })),
|
|
4630
|
+
counts: z2.object({
|
|
4631
|
+
subQueries: z2.number().int().min(0),
|
|
4632
|
+
browsed: z2.number().int().min(0),
|
|
4633
|
+
cited: z2.number().int().min(0),
|
|
4634
|
+
browsedOnly: z2.number().int().min(0)
|
|
4635
|
+
}),
|
|
4636
|
+
aggregates: z2.object({
|
|
4637
|
+
topSites: z2.array(z2.object({ domain: z2.string(), count: z2.number().int(), cited: z2.boolean(), timesCited: z2.number().int(), siteType: z2.string() })),
|
|
4638
|
+
citationOrder: z2.array(z2.object({ rank: z2.number().int(), domain: z2.string(), url: z2.string(), timesCited: z2.number().int() })),
|
|
4639
|
+
byCategory: z2.record(z2.number().int())
|
|
4640
|
+
}).describe("Objective aggregates: top sourced sites by frequency, citation order, and URL-category counts."),
|
|
4641
|
+
first_party_domain: z2.string().nullable(),
|
|
4642
|
+
exports: z2.object({
|
|
4643
|
+
relativeTo: z2.string(),
|
|
4644
|
+
dir: z2.string(),
|
|
4645
|
+
json: z2.string(),
|
|
4646
|
+
queriesCsv: z2.string(),
|
|
4647
|
+
queriesTsv: z2.string(),
|
|
4648
|
+
citationsCsv: z2.string(),
|
|
4649
|
+
sourcesCsv: z2.string(),
|
|
4650
|
+
browsedOnlyCsv: z2.string(),
|
|
4651
|
+
snippetsCsv: z2.string(),
|
|
4652
|
+
domainsCsv: z2.string(),
|
|
4653
|
+
report: z2.string()
|
|
4654
|
+
}).nullable().describe("Relative export paths when export=true, otherwise null. Paths are relative to MCP_SCRAPER_OUTPUT_DIR, or ~/Downloads/mcp-scraper when that env var is not set."),
|
|
4655
|
+
debug: z2.object({
|
|
4656
|
+
interceptorReady: z2.boolean(),
|
|
4657
|
+
rawBytes: z2.number().int().min(0),
|
|
4658
|
+
unmappedKeys: z2.array(z2.string()),
|
|
4659
|
+
note: z2.string()
|
|
4660
|
+
}).optional()
|
|
4661
|
+
};
|
|
4662
|
+
var BrowserOpenOutputSchema = {
|
|
4663
|
+
ok: z2.boolean(),
|
|
4664
|
+
tool: z2.literal("browser_open"),
|
|
4665
|
+
session_id: z2.string().describe("Session id returned by browser_open. Use only this exact value in later browser_* calls; do not construct one yourself."),
|
|
4666
|
+
watch_url: z2.string().describe("Human watch/takeover URL for this browser session on mcpscraper.dev."),
|
|
4667
|
+
live_view_url: NullableString2.describe("Deprecated; always null. Open watch_url to view the live session."),
|
|
4668
|
+
url: NullableString2.describe("Initial URL requested by the caller, when provided."),
|
|
4669
|
+
hint: z2.string(),
|
|
4670
|
+
raw: BrowserRawObject.optional()
|
|
4671
|
+
};
|
|
4672
|
+
var BrowserProfileLogin = z2.object({
|
|
4673
|
+
connection_id: NullableString2.describe("Auth connection id for this login."),
|
|
4674
|
+
domain: z2.string().describe("Site this login is for, e.g. chatgpt.com."),
|
|
4675
|
+
status: z2.string().describe("Auth status, e.g. NEEDS_AUTH or AUTHENTICATED."),
|
|
4676
|
+
account_email: NullableString2.describe("Account email recorded for this login, when known."),
|
|
4677
|
+
note: NullableString2.describe("Free-text note describing this login."),
|
|
4678
|
+
watch_url: NullableString2.describe("mcpscraper.dev sign-in link when this login still needs the user to authenticate."),
|
|
4679
|
+
last_connected_at: NullableString2.describe("When this login was last saved or refreshed.")
|
|
4680
|
+
});
|
|
4681
|
+
var BrowserProfileConnectOutputSchema = {
|
|
4682
|
+
ok: z2.boolean(),
|
|
4683
|
+
tool: z2.literal("browser_profile_connect"),
|
|
4684
|
+
session_id: NullableString2,
|
|
4685
|
+
auth_connection_id: z2.string(),
|
|
4686
|
+
watch_url: z2.string().describe("mcpscraper.dev sign-in link to give the user so they can complete this login."),
|
|
4687
|
+
live_view_url: NullableString2.describe("Deprecated; always null. Open watch_url to view the sign-in."),
|
|
4688
|
+
profile: z2.string().describe("Profile this login was added to. Reuse it to stack more logins or to open a session."),
|
|
4689
|
+
domain: z2.string(),
|
|
4690
|
+
setup_url: z2.string(),
|
|
4691
|
+
account_email: NullableString2,
|
|
4692
|
+
note: NullableString2,
|
|
4693
|
+
status: z2.string(),
|
|
4694
|
+
flow_status: NullableString2,
|
|
4695
|
+
flow_step: NullableString2,
|
|
4696
|
+
flow_expires_at: NullableString2,
|
|
4697
|
+
post_login_url: NullableString2,
|
|
4698
|
+
connected_logins: z2.array(BrowserProfileLogin).describe("Every login currently saved in this profile, so you can see what else is connected."),
|
|
4699
|
+
next_steps: z2.array(z2.string()),
|
|
4700
|
+
raw: BrowserRawObject.optional()
|
|
4701
|
+
};
|
|
4702
|
+
var BrowserProfileListOutputSchema = {
|
|
4703
|
+
ok: z2.boolean(),
|
|
4704
|
+
tool: z2.literal("browser_profile_list"),
|
|
4705
|
+
session_id: z2.null(),
|
|
4706
|
+
profile: z2.string().describe("Profile these logins belong to."),
|
|
4707
|
+
connections: z2.array(BrowserProfileLogin).describe("All site logins saved in this profile, each with its current auth status and note."),
|
|
4708
|
+
count: z2.number().int().min(0)
|
|
4709
|
+
};
|
|
4710
|
+
var BrowserScreenshotOutputSchema = {
|
|
4711
|
+
...BrowserBaseOutput,
|
|
4712
|
+
tool: z2.literal("browser_screenshot"),
|
|
4713
|
+
url: NullableString2,
|
|
4714
|
+
title: NullableString2,
|
|
4715
|
+
text: z2.string(),
|
|
4716
|
+
elements: z2.array(BrowserElementOutput),
|
|
4717
|
+
screenshot: z2.object({
|
|
4718
|
+
mime_type: z2.string(),
|
|
4719
|
+
inline: z2.boolean()
|
|
4720
|
+
}).nullable()
|
|
4721
|
+
};
|
|
4722
|
+
var BrowserReadOutputSchema = {
|
|
4723
|
+
...BrowserBaseOutput,
|
|
4724
|
+
tool: z2.literal("browser_read"),
|
|
4725
|
+
url: NullableString2,
|
|
4726
|
+
title: NullableString2,
|
|
4727
|
+
text: z2.string(),
|
|
4728
|
+
elements: z2.array(BrowserElementOutput),
|
|
4729
|
+
raw: BrowserRawObject.optional()
|
|
4730
|
+
};
|
|
4731
|
+
var BrowserLocateOutputSchema = {
|
|
4732
|
+
...BrowserBaseOutput,
|
|
4733
|
+
tool: z2.literal("browser_locate"),
|
|
4734
|
+
url: NullableString2,
|
|
4735
|
+
title: NullableString2,
|
|
4736
|
+
viewport: BrowserRawObject.nullable(),
|
|
4737
|
+
replay: BrowserRawObject.nullable(),
|
|
4738
|
+
targets: z2.array(BrowserRawObject),
|
|
4739
|
+
raw: BrowserRawObject.optional()
|
|
4740
|
+
};
|
|
4741
|
+
var BrowserActionOutputSchema = {
|
|
4742
|
+
...BrowserBaseOutput,
|
|
4743
|
+
result: BrowserRawObject.describe("Provider action result. Check ok and follow with browser_screenshot/browser_read when page state matters."),
|
|
4744
|
+
nextRecommendedTool: z2.string().nullable()
|
|
4745
|
+
};
|
|
4746
|
+
var BrowserReplayStartOutputSchema = {
|
|
4747
|
+
...BrowserReplayBaseOutput,
|
|
4748
|
+
tool: z2.literal("browser_replay_start"),
|
|
4749
|
+
view_url: NullableString2,
|
|
4750
|
+
download_url: NullableString2,
|
|
4751
|
+
raw: BrowserRawObject.optional()
|
|
4752
|
+
};
|
|
4753
|
+
var BrowserReplayStopOutputSchema = {
|
|
4754
|
+
...BrowserReplayBaseOutput,
|
|
4755
|
+
tool: z2.literal("browser_replay_stop"),
|
|
4756
|
+
view_url: NullableString2,
|
|
4757
|
+
download_url: NullableString2,
|
|
4758
|
+
raw: BrowserRawObject.optional()
|
|
4759
|
+
};
|
|
4760
|
+
var BrowserListReplaysOutputSchema = {
|
|
4761
|
+
...BrowserBaseOutput,
|
|
4762
|
+
tool: z2.literal("browser_list_replays"),
|
|
4763
|
+
replays: z2.array(BrowserRawObject),
|
|
4764
|
+
count: z2.number().int().min(0)
|
|
4765
|
+
};
|
|
4766
|
+
var BrowserReplayDownloadOutputSchema = {
|
|
4767
|
+
...BrowserReplayBaseOutput,
|
|
4768
|
+
tool: z2.literal("browser_replay_download"),
|
|
4769
|
+
file_path: NullableString2,
|
|
4770
|
+
bytes: z2.number().int().min(0).nullable(),
|
|
4771
|
+
mime_type: NullableString2,
|
|
4772
|
+
download_url: NullableString2
|
|
4773
|
+
};
|
|
4774
|
+
var BrowserReplayMarkOutputSchema = {
|
|
4775
|
+
...BrowserReplayBaseOutput,
|
|
4776
|
+
tool: z2.literal("browser_replay_mark"),
|
|
4777
|
+
annotation: BrowserRawObject,
|
|
4778
|
+
source_width: z2.number().nullable(),
|
|
4779
|
+
source_height: z2.number().nullable(),
|
|
4780
|
+
target: BrowserRawObject.nullable(),
|
|
4781
|
+
hint: z2.string()
|
|
4782
|
+
};
|
|
4783
|
+
var BrowserReplayAnnotateOutputSchema = {
|
|
4784
|
+
...BrowserReplayBaseOutput,
|
|
4785
|
+
tool: z2.literal("browser_replay_annotate"),
|
|
4786
|
+
source_file_path: NullableString2,
|
|
4787
|
+
annotated_file_path: NullableString2,
|
|
4788
|
+
bytes: z2.number().int().min(0).nullable(),
|
|
4789
|
+
width: z2.number().int().min(0).nullable(),
|
|
4790
|
+
height: z2.number().int().min(0).nullable(),
|
|
4791
|
+
annotation_count: z2.number().int().min(0).nullable(),
|
|
4792
|
+
mime_type: NullableString2
|
|
4793
|
+
};
|
|
4794
|
+
var BrowserCloseOutputSchema = {
|
|
4795
|
+
...BrowserBaseOutput,
|
|
4796
|
+
tool: z2.literal("browser_close"),
|
|
4797
|
+
closed: z2.boolean(),
|
|
4798
|
+
raw: BrowserRawObject.optional()
|
|
4799
|
+
};
|
|
4800
|
+
var BrowserListSessionsOutputSchema = {
|
|
4801
|
+
ok: z2.boolean(),
|
|
4802
|
+
tool: z2.literal("browser_list_sessions"),
|
|
4803
|
+
session_id: z2.null(),
|
|
4804
|
+
sessions: z2.array(BrowserRawObject),
|
|
4805
|
+
count: z2.number().int().min(0)
|
|
4806
|
+
};
|
|
4807
|
+
|
|
4808
|
+
// src/mcp/replay-annotator.ts
|
|
4809
|
+
import { execFile } from "child_process";
|
|
4810
|
+
import { mkdtemp, rm, stat, writeFile } from "fs/promises";
|
|
4811
|
+
import { tmpdir } from "os";
|
|
4812
|
+
import { join as join4 } from "path";
|
|
4813
|
+
import { promisify } from "util";
|
|
4814
|
+
var execFileAsync = promisify(execFile);
|
|
4815
|
+
function finiteNumber(value) {
|
|
4816
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
4817
|
+
}
|
|
4818
|
+
function clamp(value, min, max) {
|
|
4819
|
+
return Math.min(max, Math.max(min, value));
|
|
4820
|
+
}
|
|
4821
|
+
function formatAssTime(seconds) {
|
|
4822
|
+
const safe2 = Math.max(0, seconds);
|
|
4823
|
+
const hours = Math.floor(safe2 / 3600);
|
|
4824
|
+
const minutes = Math.floor(safe2 % 3600 / 60);
|
|
4825
|
+
const wholeSeconds = Math.floor(safe2 % 60);
|
|
4826
|
+
const centiseconds = Math.floor((safe2 - Math.floor(safe2)) * 100);
|
|
4827
|
+
return `${hours}:${String(minutes).padStart(2, "0")}:${String(wholeSeconds).padStart(2, "0")}.${String(centiseconds).padStart(2, "0")}`;
|
|
4828
|
+
}
|
|
4829
|
+
function cssHexToAssColor(color) {
|
|
4830
|
+
const normalized = color?.trim().match(/^#?([0-9a-fA-F]{6})$/)?.[1] ?? "ff3b30";
|
|
4831
|
+
const red = normalized.slice(0, 2);
|
|
4832
|
+
const green = normalized.slice(2, 4);
|
|
4833
|
+
const blue = normalized.slice(4, 6);
|
|
4834
|
+
return `&H${blue}${green}${red}&`;
|
|
4835
|
+
}
|
|
4836
|
+
function escapeAssText(value) {
|
|
4837
|
+
return value.replace(/\\/g, "\\\\").replace(/\{/g, "\\{").replace(/\}/g, "\\}").replace(/\r?\n/g, "\\N");
|
|
4838
|
+
}
|
|
4839
|
+
function resolveCoordinateTransform(video, source, options) {
|
|
4840
|
+
const explicitOffsetX = finiteNumber(options.sourceLeftOffset) ? options.sourceLeftOffset : null;
|
|
4841
|
+
const explicitOffsetY = finiteNumber(options.sourceTopOffset) ? options.sourceTopOffset : null;
|
|
4842
|
+
const inferredTopOffset = explicitOffsetY == null && Math.round(source.width) === Math.round(video.width) && source.height < video.height && video.height - source.height <= 220 ? video.height - source.height : 0;
|
|
4843
|
+
const offsetX = explicitOffsetX ?? 0;
|
|
4844
|
+
const offsetY = explicitOffsetY ?? inferredTopOffset;
|
|
4845
|
+
return {
|
|
4846
|
+
scaleX: (video.width - offsetX) / source.width,
|
|
4847
|
+
scaleY: (video.height - offsetY) / source.height,
|
|
4848
|
+
offsetX,
|
|
4849
|
+
offsetY
|
|
4850
|
+
};
|
|
4851
|
+
}
|
|
4852
|
+
function transformPoint(x, y, transform) {
|
|
4853
|
+
return {
|
|
4854
|
+
x: x * transform.scaleX + transform.offsetX,
|
|
4855
|
+
y: y * transform.scaleY + transform.offsetY
|
|
4856
|
+
};
|
|
4857
|
+
}
|
|
4858
|
+
function scaleRect(annotation, transform) {
|
|
4859
|
+
if (finiteNumber(annotation.left) && finiteNumber(annotation.top) && finiteNumber(annotation.width) && finiteNumber(annotation.height)) {
|
|
4860
|
+
const origin = transformPoint(annotation.left, annotation.top, transform);
|
|
4861
|
+
return {
|
|
4862
|
+
left: origin.x,
|
|
4863
|
+
top: origin.y,
|
|
4864
|
+
width: annotation.width * transform.scaleX,
|
|
4865
|
+
height: annotation.height * transform.scaleY
|
|
4866
|
+
};
|
|
4867
|
+
}
|
|
4868
|
+
if (finiteNumber(annotation.x) && finiteNumber(annotation.y)) {
|
|
4869
|
+
const radius = finiteNumber(annotation.width) ? annotation.width / 2 : 28;
|
|
4870
|
+
const point = transformPoint(annotation.x, annotation.y, transform);
|
|
4871
|
+
return {
|
|
4872
|
+
left: point.x - radius * transform.scaleX,
|
|
4873
|
+
top: point.y - radius * transform.scaleY,
|
|
4874
|
+
width: radius * 2 * transform.scaleX,
|
|
4875
|
+
height: radius * 2 * transform.scaleY
|
|
4876
|
+
};
|
|
4877
|
+
}
|
|
4878
|
+
throw new Error("annotation needs either left/top/width/height or x/y");
|
|
4879
|
+
}
|
|
4880
|
+
function rectPath(rect) {
|
|
4881
|
+
const x1 = Math.round(rect.left);
|
|
4882
|
+
const y1 = Math.round(rect.top);
|
|
4883
|
+
const x2 = Math.round(rect.left + rect.width);
|
|
4884
|
+
const y2 = Math.round(rect.top + rect.height);
|
|
4885
|
+
return `m ${x1} ${y1} l ${x2} ${y1} l ${x2} ${y2} l ${x1} ${y2} l ${x1} ${y1}`;
|
|
4886
|
+
}
|
|
4887
|
+
function ellipsePath(rect) {
|
|
4888
|
+
const cx = rect.left + rect.width / 2;
|
|
4889
|
+
const cy = rect.top + rect.height / 2;
|
|
4890
|
+
const rx = Math.max(4, rect.width / 2);
|
|
4891
|
+
const ry = Math.max(4, rect.height / 2);
|
|
4892
|
+
const points = [];
|
|
4893
|
+
for (let i = 0; i <= 32; i += 1) {
|
|
4894
|
+
const t = Math.PI * 2 * i / 32;
|
|
4895
|
+
const x = Math.round(cx + Math.cos(t) * rx);
|
|
4896
|
+
const y = Math.round(cy + Math.sin(t) * ry);
|
|
4897
|
+
points.push(`${i === 0 ? "m" : "l"} ${x} ${y}`);
|
|
4898
|
+
}
|
|
4899
|
+
return points.join(" ");
|
|
4900
|
+
}
|
|
4901
|
+
function filledRectPath(left, top, width, height) {
|
|
4902
|
+
return rectPath({ left, top, width, height });
|
|
4903
|
+
}
|
|
4904
|
+
function arrowPath(fromX, fromY, toX, toY, thickness) {
|
|
4905
|
+
const dx = toX - fromX;
|
|
4906
|
+
const dy = toY - fromY;
|
|
4907
|
+
const length = Math.max(1, Math.hypot(dx, dy));
|
|
4908
|
+
const ux = dx / length;
|
|
4909
|
+
const uy = dy / length;
|
|
4910
|
+
const px = -uy;
|
|
4911
|
+
const py = ux;
|
|
4912
|
+
const half = thickness / 2;
|
|
4913
|
+
const head = Math.max(14, thickness * 4);
|
|
4914
|
+
const baseX = toX - ux * head;
|
|
4915
|
+
const baseY = toY - uy * head;
|
|
4916
|
+
const p1x = Math.round(fromX + px * half);
|
|
4917
|
+
const p1y = Math.round(fromY + py * half);
|
|
4918
|
+
const p2x = Math.round(baseX + px * half);
|
|
4919
|
+
const p2y = Math.round(baseY + py * half);
|
|
4920
|
+
const p3x = Math.round(baseX + px * head * 0.55);
|
|
4921
|
+
const p3y = Math.round(baseY + py * head * 0.55);
|
|
4922
|
+
const p4x = Math.round(toX);
|
|
4923
|
+
const p4y = Math.round(toY);
|
|
4924
|
+
const p5x = Math.round(baseX - px * head * 0.55);
|
|
4925
|
+
const p5y = Math.round(baseY - py * head * 0.55);
|
|
4926
|
+
const p6x = Math.round(baseX - px * half);
|
|
4927
|
+
const p6y = Math.round(baseY - py * half);
|
|
4928
|
+
const p7x = Math.round(fromX - px * half);
|
|
4929
|
+
const p7y = Math.round(fromY - py * half);
|
|
4930
|
+
return `m ${p1x} ${p1y} l ${p2x} ${p2y} l ${p3x} ${p3y} l ${p4x} ${p4y} l ${p5x} ${p5y} l ${p6x} ${p6y} l ${p7x} ${p7y} l ${p1x} ${p1y}`;
|
|
4931
|
+
}
|
|
4932
|
+
function eventLine(start, end, body) {
|
|
4933
|
+
return `Dialogue: 0,${formatAssTime(start)},${formatAssTime(end)},Default,,0,0,0,,${body}`;
|
|
4934
|
+
}
|
|
4935
|
+
function labelLine(start, end, x, y, label) {
|
|
4936
|
+
const safeX = Math.round(x);
|
|
4937
|
+
const safeY = Math.round(y);
|
|
4938
|
+
return eventLine(
|
|
4939
|
+
start,
|
|
4940
|
+
end,
|
|
4941
|
+
`{\\an7\\pos(${safeX},${safeY})\\fs30\\bord4\\shad0\\c&HFFFFFF&\\3c&H000000&}${escapeAssText(label)}`
|
|
4942
|
+
);
|
|
4943
|
+
}
|
|
4944
|
+
function shapeLine(start, end, path, color, thickness, filled = false) {
|
|
4945
|
+
const fillAlpha = filled ? "&H00&" : "&HFF&";
|
|
4946
|
+
const border = filled ? 0 : thickness;
|
|
4947
|
+
return eventLine(
|
|
4948
|
+
start,
|
|
4949
|
+
end,
|
|
4950
|
+
`{\\an7\\pos(0,0)\\p1\\bord${border}\\shad0\\1a${fillAlpha}\\1c${color}\\3c${color}}${path}`
|
|
4951
|
+
);
|
|
4952
|
+
}
|
|
4953
|
+
function buildAssSubtitle(options, video) {
|
|
4954
|
+
const source = {
|
|
4955
|
+
width: options.sourceWidth && options.sourceWidth > 0 ? options.sourceWidth : video.width,
|
|
4956
|
+
height: options.sourceHeight && options.sourceHeight > 0 ? options.sourceHeight : video.height
|
|
4957
|
+
};
|
|
4958
|
+
const transform = resolveCoordinateTransform(video, source, options);
|
|
4959
|
+
const lines = [];
|
|
4960
|
+
for (const annotation of options.annotations) {
|
|
4961
|
+
const start = Math.max(0, annotation.start_seconds ?? 0);
|
|
4962
|
+
const end = annotation.end_seconds && annotation.end_seconds > start ? annotation.end_seconds : start + 2;
|
|
4963
|
+
const type = annotation.type ?? "box";
|
|
4964
|
+
const color = cssHexToAssColor(annotation.color);
|
|
4965
|
+
const thickness = Math.round(clamp(annotation.thickness ?? 5, 2, 24));
|
|
4966
|
+
if (type === "label") {
|
|
4967
|
+
if (!annotation.label) throw new Error("label annotation needs label");
|
|
4968
|
+
const rect2 = scaleRect(annotation, transform);
|
|
4969
|
+
lines.push(labelLine(start, end, rect2.left, Math.max(8, rect2.top), annotation.label));
|
|
4970
|
+
continue;
|
|
4971
|
+
}
|
|
4972
|
+
if (type === "arrow") {
|
|
4973
|
+
const rect2 = finiteNumber(annotation.to_x) && finiteNumber(annotation.to_y) ? null : scaleRect(annotation, transform);
|
|
4974
|
+
const toPoint = finiteNumber(annotation.to_x) && finiteNumber(annotation.to_y) ? transformPoint(annotation.to_x, annotation.to_y, transform) : null;
|
|
4975
|
+
const toX = toPoint ? toPoint.x : rect2.left + rect2.width / 2;
|
|
4976
|
+
const toY = toPoint ? toPoint.y : rect2.top + rect2.height / 2;
|
|
4977
|
+
const fromPoint = finiteNumber(annotation.from_x) && finiteNumber(annotation.from_y) ? transformPoint(annotation.from_x, annotation.from_y, transform) : null;
|
|
4978
|
+
const fromX = fromPoint ? fromPoint.x : clamp(toX - 120, 12, video.width - 12);
|
|
4979
|
+
const fromY = fromPoint ? fromPoint.y : clamp(toY - 90, 12, video.height - 12);
|
|
4980
|
+
lines.push(shapeLine(start, end, arrowPath(fromX, fromY, toX, toY, thickness), color, thickness, true));
|
|
4981
|
+
if (annotation.label) lines.push(labelLine(start, end, fromX + 8, Math.max(96, fromY - 36), annotation.label));
|
|
4982
|
+
continue;
|
|
4983
|
+
}
|
|
4984
|
+
const rect = scaleRect(annotation, transform);
|
|
4985
|
+
if (type === "underline") {
|
|
4986
|
+
const underlineTop = rect.top + rect.height + thickness;
|
|
4987
|
+
lines.push(shapeLine(start, end, filledRectPath(rect.left, underlineTop, rect.width, thickness), color, 0, true));
|
|
4988
|
+
} else if (type === "circle") {
|
|
4989
|
+
lines.push(shapeLine(start, end, ellipsePath(rect), color, thickness));
|
|
4990
|
+
} else {
|
|
4991
|
+
lines.push(shapeLine(start, end, rectPath(rect), color, thickness));
|
|
4992
|
+
}
|
|
4993
|
+
if (annotation.label) lines.push(labelLine(start, end, rect.left, Math.max(8, rect.top - 38), annotation.label));
|
|
4994
|
+
}
|
|
4995
|
+
return [
|
|
4996
|
+
"[Script Info]",
|
|
4997
|
+
"ScriptType: v4.00+",
|
|
4998
|
+
`PlayResX: ${Math.round(video.width)}`,
|
|
4999
|
+
`PlayResY: ${Math.round(video.height)}`,
|
|
5000
|
+
"ScaledBorderAndShadow: yes",
|
|
5001
|
+
"",
|
|
5002
|
+
"[V4+ Styles]",
|
|
5003
|
+
"Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding",
|
|
5004
|
+
"Style: Default,Arial,30,&H00FFFFFF,&H000000FF,&H00000000,&H90000000,0,0,0,0,100,100,0,0,1,2,0,7,0,0,0,1",
|
|
5005
|
+
"",
|
|
5006
|
+
"[Events]",
|
|
5007
|
+
"Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text",
|
|
5008
|
+
...lines,
|
|
5009
|
+
""
|
|
5010
|
+
].join("\n");
|
|
5011
|
+
}
|
|
5012
|
+
async function videoSize(inputFilePath) {
|
|
5013
|
+
const { stdout } = await execFileAsync("ffprobe", [
|
|
5014
|
+
"-v",
|
|
5015
|
+
"error",
|
|
5016
|
+
"-select_streams",
|
|
5017
|
+
"v:0",
|
|
5018
|
+
"-show_entries",
|
|
5019
|
+
"stream=width,height",
|
|
5020
|
+
"-of",
|
|
5021
|
+
"json",
|
|
5022
|
+
inputFilePath
|
|
5023
|
+
], { maxBuffer: 1024 * 1024 });
|
|
5024
|
+
const parsed = JSON.parse(stdout);
|
|
5025
|
+
const stream = parsed.streams?.[0];
|
|
5026
|
+
if (!stream?.width || !stream.height) throw new Error("could not read replay video dimensions");
|
|
5027
|
+
return { width: stream.width, height: stream.height };
|
|
5028
|
+
}
|
|
5029
|
+
function ffmpegFilterPath(path) {
|
|
5030
|
+
return path.replace(/\\/g, "\\\\").replace(/:/g, "\\:");
|
|
5031
|
+
}
|
|
5032
|
+
async function annotateReplayVideo(inputFilePath, outputFilePath, options) {
|
|
5033
|
+
if (!options.annotations.length) throw new Error("annotations must include at least one item");
|
|
5034
|
+
const size = await videoSize(inputFilePath);
|
|
5035
|
+
const tmp = await mkdtemp(join4(tmpdir(), "mcp-scraper-ass-"));
|
|
5036
|
+
const assPath = join4(tmp, "annotations.ass");
|
|
5037
|
+
try {
|
|
5038
|
+
await writeFile(assPath, buildAssSubtitle(options, size), "utf8");
|
|
5039
|
+
await execFileAsync("ffmpeg", [
|
|
5040
|
+
"-y",
|
|
5041
|
+
"-i",
|
|
5042
|
+
inputFilePath,
|
|
5043
|
+
"-vf",
|
|
5044
|
+
`ass=${ffmpegFilterPath(assPath)}`,
|
|
5045
|
+
"-c:v",
|
|
5046
|
+
"libx264",
|
|
5047
|
+
"-pix_fmt",
|
|
5048
|
+
"yuv420p",
|
|
5049
|
+
"-movflags",
|
|
5050
|
+
"+faststart",
|
|
5051
|
+
"-c:a",
|
|
5052
|
+
"copy",
|
|
5053
|
+
outputFilePath
|
|
5054
|
+
], { maxBuffer: 1024 * 1024 * 20 });
|
|
5055
|
+
const out = await stat(outputFilePath);
|
|
5056
|
+
return {
|
|
5057
|
+
filePath: outputFilePath,
|
|
5058
|
+
bytes: out.size,
|
|
5059
|
+
width: size.width,
|
|
5060
|
+
height: size.height,
|
|
5061
|
+
annotationCount: options.annotations.length
|
|
5062
|
+
};
|
|
5063
|
+
} finally {
|
|
5064
|
+
await rm(tmp, { recursive: true, force: true });
|
|
5065
|
+
}
|
|
5066
|
+
}
|
|
5067
|
+
|
|
5068
|
+
// src/mcp/browser-agent-mcp-server.ts
|
|
5069
|
+
function structuredResult(value, isError = false) {
|
|
5070
|
+
const safe2 = sanitizeOutboundDiagnostics(value);
|
|
5071
|
+
return {
|
|
5072
|
+
content: [{ type: "text", text: JSON.stringify(safe2) }],
|
|
5073
|
+
structuredContent: safe2,
|
|
5074
|
+
isError
|
|
5075
|
+
};
|
|
5076
|
+
}
|
|
5077
|
+
function errorMessage(value) {
|
|
5078
|
+
if (value && typeof value === "object") {
|
|
5079
|
+
const data = value;
|
|
5080
|
+
if (typeof data.error === "string") return data.error;
|
|
5081
|
+
if (typeof data.message === "string") return data.message;
|
|
5082
|
+
}
|
|
5083
|
+
return typeof value === "string" ? value : "Browser Agent request failed";
|
|
5084
|
+
}
|
|
5085
|
+
function errorResult(tool, value, sessionId = null, replayId = null) {
|
|
5086
|
+
return structuredResult({
|
|
5087
|
+
ok: false,
|
|
5088
|
+
tool,
|
|
5089
|
+
session_id: sessionId,
|
|
5090
|
+
...replayId !== null ? { replay_id: replayId } : {},
|
|
5091
|
+
error: errorMessage(value),
|
|
5092
|
+
raw: value && typeof value === "object" ? value : { value }
|
|
5093
|
+
}, true);
|
|
5094
|
+
}
|
|
5095
|
+
function actionResult(tool, sessionId, ok, data, nextRecommendedTool = "browser_screenshot") {
|
|
5096
|
+
if (!ok) return errorResult(tool, data, sessionId);
|
|
5097
|
+
return structuredResult({
|
|
5098
|
+
ok: true,
|
|
5099
|
+
tool,
|
|
5100
|
+
session_id: sessionId,
|
|
5101
|
+
result: data && typeof data === "object" ? data : { value: data },
|
|
5102
|
+
nextRecommendedTool
|
|
5103
|
+
});
|
|
5104
|
+
}
|
|
5105
|
+
function outputBaseDir3() {
|
|
5106
|
+
return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || join5(homedir3(), "Downloads", "mcp-scraper");
|
|
5107
|
+
}
|
|
5108
|
+
function safeFilePart(value) {
|
|
5109
|
+
return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 120) || "replay";
|
|
5110
|
+
}
|
|
5111
|
+
function replayFilePath(sessionId, replayId, filename) {
|
|
5112
|
+
const requested = filename?.trim();
|
|
5113
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
5114
|
+
const name = requested ? safeFilePart(requested).replace(/\.mp4$/i, "") : `${stamp}-${safeFilePart(sessionId)}-${safeFilePart(replayId)}`;
|
|
5115
|
+
return join5(outputBaseDir3(), "browser-replays", `${name}.mp4`);
|
|
5116
|
+
}
|
|
5117
|
+
function slugPart(value) {
|
|
5118
|
+
const trimmed = value?.trim().toLowerCase();
|
|
5119
|
+
if (!trimmed) return null;
|
|
5120
|
+
return trimmed.replace(/^https?:\/\//, "").replace(/^www\./, "").replace(/\/.*$/, "").replace(/[^a-z0-9._-]+/g, "-").replace(/[._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
5121
|
+
}
|
|
5122
|
+
function savedProfileNameFromEmail(email) {
|
|
5123
|
+
const value = slugPart(email);
|
|
5124
|
+
if (!value) return null;
|
|
5125
|
+
return value.slice(0, 80) || null;
|
|
5126
|
+
}
|
|
5127
|
+
function isEnrichedFanout(value) {
|
|
5128
|
+
if (!value || typeof value !== "object") return false;
|
|
5129
|
+
const candidate = value;
|
|
5130
|
+
return typeof candidate.platform === "string" && typeof candidate.capturedAt === "string" && typeof candidate.prompt === "string" && Array.isArray(candidate.queries) && Array.isArray(candidate.browsedUrls) && Array.isArray(candidate.citedUrls) && Array.isArray(candidate.browsedOnly) && Array.isArray(candidate.snippets) && !!candidate.counts && !!candidate.aggregates;
|
|
5131
|
+
}
|
|
5132
|
+
function annotatedReplayFilePath(sessionId, replayId, filename) {
|
|
5133
|
+
const requested = filename?.trim();
|
|
5134
|
+
if (requested) return replayFilePath(sessionId, replayId, requested);
|
|
5135
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
5136
|
+
return replayFilePath(sessionId, replayId, `${stamp}-${safeFilePart(sessionId)}-${safeFilePart(replayId)}-annotated`);
|
|
5137
|
+
}
|
|
5138
|
+
function finiteNumber2(value) {
|
|
5139
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
5140
|
+
}
|
|
5141
|
+
function expandElementBounds(element, viewport, padding) {
|
|
5142
|
+
const sourceWidth = finiteNumber2(viewport?.width) && viewport.width > 0 ? viewport.width : 1920;
|
|
5143
|
+
const sourceHeight = finiteNumber2(viewport?.height) && viewport.height > 0 ? viewport.height : 1080;
|
|
5144
|
+
const left = Math.max(0, element.left - padding);
|
|
5145
|
+
const top = Math.max(0, element.top - padding);
|
|
5146
|
+
const right = Math.min(sourceWidth, element.left + element.width + padding);
|
|
5147
|
+
const bottom = Math.min(sourceHeight, element.top + element.height + padding);
|
|
5148
|
+
return {
|
|
5149
|
+
left,
|
|
5150
|
+
top,
|
|
5151
|
+
width: Math.max(1, right - left),
|
|
5152
|
+
height: Math.max(1, bottom - top),
|
|
5153
|
+
sourceWidth,
|
|
5154
|
+
sourceHeight
|
|
5155
|
+
};
|
|
5156
|
+
}
|
|
5157
|
+
function registerBrowserAgentMcpTools(server, opts) {
|
|
5158
|
+
const baseUrl = opts.baseUrl.replace(/\/$/, "");
|
|
5159
|
+
const consoleBase = (opts.consoleBaseUrl ?? opts.baseUrl).replace(/\/$/, "");
|
|
5160
|
+
const timeoutMs = opts.timeoutMs ?? 9e4;
|
|
5161
|
+
async function req(method, path, body, requestTimeoutMs = timeoutMs) {
|
|
5162
|
+
try {
|
|
5163
|
+
const res = await fetch(`${baseUrl}${path}`, {
|
|
5164
|
+
method,
|
|
5165
|
+
headers: { "Content-Type": "application/json", "x-api-key": opts.apiKey },
|
|
5166
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
5167
|
+
signal: AbortSignal.timeout(requestTimeoutMs)
|
|
5168
|
+
});
|
|
5169
|
+
const data = await res.json().catch(() => ({}));
|
|
5170
|
+
return { ok: res.ok, data };
|
|
5171
|
+
} catch (err) {
|
|
5172
|
+
return { ok: false, data: { error: err instanceof Error ? err.message : String(err) } };
|
|
5173
|
+
}
|
|
5174
|
+
}
|
|
5175
|
+
async function downloadReplay(sessionId, replayId, filename) {
|
|
5176
|
+
const path = `/agent/sessions/${encodeURIComponent(sessionId)}/replays/${encodeURIComponent(replayId)}/download`;
|
|
5177
|
+
try {
|
|
5178
|
+
const res = await fetch(`${baseUrl}${path}`, {
|
|
5179
|
+
method: "GET",
|
|
5180
|
+
headers: { "x-api-key": opts.apiKey },
|
|
5181
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
5182
|
+
});
|
|
5183
|
+
if (!res.ok) {
|
|
5184
|
+
const data = await res.json().catch(async () => ({ error: await res.text().catch(() => `HTTP ${res.status}`) }));
|
|
5185
|
+
return { ok: false, data };
|
|
5186
|
+
}
|
|
5187
|
+
const bytes = Buffer.from(await res.arrayBuffer());
|
|
5188
|
+
const filePath = replayFilePath(sessionId, replayId, filename);
|
|
5189
|
+
mkdirSync3(join5(outputBaseDir3(), "browser-replays"), { recursive: true });
|
|
5190
|
+
writeFileSync3(filePath, bytes);
|
|
5191
|
+
return {
|
|
5192
|
+
ok: true,
|
|
5193
|
+
data: {
|
|
5194
|
+
replay_id: replayId,
|
|
5195
|
+
file_path: filePath,
|
|
5196
|
+
bytes: bytes.length,
|
|
5197
|
+
mime_type: res.headers.get("content-type") ?? "video/mp4",
|
|
5198
|
+
download_url: `${baseUrl}${path}`
|
|
5199
|
+
}
|
|
5200
|
+
};
|
|
5201
|
+
} catch (err) {
|
|
5202
|
+
return { ok: false, data: { error: err instanceof Error ? err.message : String(err) } };
|
|
5203
|
+
}
|
|
5204
|
+
}
|
|
5205
|
+
const annotations = (title, readOnly = false) => ({
|
|
5206
|
+
title,
|
|
5207
|
+
readOnlyHint: readOnly,
|
|
5208
|
+
destructiveHint: false,
|
|
5209
|
+
idempotentHint: false,
|
|
5210
|
+
openWorldHint: true
|
|
5211
|
+
});
|
|
5212
|
+
function normalizeLogin(conn) {
|
|
5213
|
+
return {
|
|
5214
|
+
connection_id: conn.connection_id ?? null,
|
|
5215
|
+
domain: conn.domain,
|
|
5216
|
+
status: conn.status,
|
|
5217
|
+
account_email: conn.account_email ?? null,
|
|
5218
|
+
note: conn.note ?? null,
|
|
5219
|
+
watch_url: conn.connection_id && conn.status !== "AUTHENTICATED" ? `${consoleBase}/console/auth/${conn.connection_id}` : null,
|
|
5220
|
+
last_connected_at: conn.last_connected_at ?? null
|
|
5221
|
+
};
|
|
5222
|
+
}
|
|
5223
|
+
async function fetchConnectedLogins(profile) {
|
|
5224
|
+
const list = await req("POST", "/agent/profiles/list", { profile });
|
|
5225
|
+
return list.ok && Array.isArray(list.data?.connections) ? list.data.connections.map(normalizeLogin) : [];
|
|
5226
|
+
}
|
|
5227
|
+
server.registerTool(
|
|
5228
|
+
"browser_profile_connect",
|
|
5229
|
+
{
|
|
5230
|
+
title: "Save a Site Login to a Profile",
|
|
5231
|
+
description: `Save a logged-in browser session so this server can reuse a site the user is signed into (ChatGPT, Claude, Reddit, or any account-gated site). Returns an mcpscraper.dev watch_url \u2014 give it to the user, they sign in once, and the cookies are saved to a named profile for later browser_open calls. ONE profile holds MANY logins: call this again with the SAME profile and a DIFFERENT domain to stack another account (e.g. add claude.ai to a profile that already has chatgpt.com). Pass note to record what each login is. Side effects: opens a short-lived sign-in session and persists cookies to the profile; it does NOT import the user's existing browser cookies (they log in fresh). NOT for one-off scraping of public pages (use extract_url) and NOT a way to drive the browser (use browser_open). Sample asks: "save my ChatGPT login", "connect my Reddit account", "add my Claude login to the same profile". After the user signs in, poll browser_profile_list until the login reads AUTHENTICATED, then browser_open with the profile.`,
|
|
5232
|
+
inputSchema: BrowserProfileConnectInputSchema,
|
|
5233
|
+
outputSchema: BrowserProfileConnectOutputSchema,
|
|
5234
|
+
annotations: annotations("Save a Site Login to a Profile")
|
|
5235
|
+
},
|
|
5236
|
+
async (input) => {
|
|
5237
|
+
const domain = slugPart(input.domain) || "chatgpt.com";
|
|
5238
|
+
const setupUrl = input.login_url ?? input.url ?? (domain === "chatgpt.com" ? "https://chatgpt.com/" : `https://${domain}/`);
|
|
5239
|
+
const profile = input.profile?.trim() || savedProfileNameFromEmail(input.email) || browserServiceProfileName();
|
|
5240
|
+
if (!profile) {
|
|
5241
|
+
return errorResult("browser_profile_connect", {
|
|
5242
|
+
error: "profile or email is required when BROWSER_AGENT_PROFILE_NAME is not available"
|
|
5243
|
+
});
|
|
5244
|
+
}
|
|
5245
|
+
const accountEmail = input.email?.trim() || null;
|
|
5246
|
+
const note = input.note?.trim() || null;
|
|
5247
|
+
const open = await req("POST", "/agent/profiles/onboard", {
|
|
5248
|
+
label: input.label ?? `Login setup: ${domain} \u2192 ${profile}`,
|
|
5249
|
+
profile,
|
|
5250
|
+
domain,
|
|
5251
|
+
login_url: setupUrl,
|
|
5252
|
+
...accountEmail ? { account_email: accountEmail } : {},
|
|
5253
|
+
...note ? { note } : {}
|
|
5254
|
+
});
|
|
5255
|
+
if (!open.ok) return errorResult("browser_profile_connect", open.data);
|
|
5256
|
+
const connectedLogins = await fetchConnectedLogins(profile).catch(() => []);
|
|
5257
|
+
return structuredResult({
|
|
5258
|
+
ok: true,
|
|
5259
|
+
tool: "browser_profile_connect",
|
|
5260
|
+
session_id: null,
|
|
5261
|
+
auth_connection_id: open.data.connection_id,
|
|
5262
|
+
watch_url: `${consoleBase}/console/auth/${open.data.connection_id}`,
|
|
5263
|
+
live_view_url: null,
|
|
5264
|
+
profile,
|
|
5265
|
+
domain,
|
|
5266
|
+
setup_url: setupUrl,
|
|
5267
|
+
account_email: accountEmail,
|
|
5268
|
+
note,
|
|
5269
|
+
status: open.data.status ?? null,
|
|
5270
|
+
flow_status: open.data.flow_status ?? null,
|
|
5271
|
+
flow_step: open.data.flow_step ?? null,
|
|
5272
|
+
flow_expires_at: open.data.flow_expires_at ?? null,
|
|
5273
|
+
post_login_url: open.data.post_login_url ?? null,
|
|
5274
|
+
connected_logins: connectedLogins,
|
|
5275
|
+
next_steps: [
|
|
5276
|
+
`Give the user watch_url so they can sign in to ${domain}.`,
|
|
5277
|
+
"Poll browser_profile_list (with this profile) until the login reads AUTHENTICATED.",
|
|
5278
|
+
"To add another account to this same profile, call browser_profile_connect again with the same profile and a different domain.",
|
|
5279
|
+
"When authenticated, call browser_open with this profile to drive the logged-in session."
|
|
5280
|
+
],
|
|
5281
|
+
raw: open.data
|
|
5282
|
+
});
|
|
5283
|
+
}
|
|
5284
|
+
);
|
|
5285
|
+
server.registerTool(
|
|
5286
|
+
"browser_profile_list",
|
|
5287
|
+
{
|
|
5288
|
+
title: "List Saved Logins in a Profile",
|
|
5289
|
+
description: `List every site login saved in a profile, each with its current auth status (NEEDS_AUTH / AUTHENTICATED), account email, and note. Use it to (1) see what a profile is connected to before opening a session, and (2) poll a just-saved login until it flips to AUTHENTICATED. Read-only, no cost. Pass profile (or email to derive it); narrow with domain or connection_id to poll a single login. Sample asks: "what's connected in my profile", "is my ChatGPT login ready yet", "which accounts are saved". Pairs with browser_profile_connect (which adds logins) and browser_open (which uses them).`,
|
|
5290
|
+
inputSchema: BrowserProfileListInputSchema,
|
|
5291
|
+
outputSchema: BrowserProfileListOutputSchema,
|
|
5292
|
+
annotations: annotations("List Saved Logins in a Profile", true)
|
|
5293
|
+
},
|
|
5294
|
+
async (input) => {
|
|
5295
|
+
const profile = input.profile?.trim() || savedProfileNameFromEmail(input.email) || browserServiceProfileName();
|
|
5296
|
+
if (!profile) {
|
|
5297
|
+
return errorResult("browser_profile_list", {
|
|
5298
|
+
error: "profile or email is required when BROWSER_AGENT_PROFILE_NAME is not available"
|
|
5299
|
+
});
|
|
5300
|
+
}
|
|
5301
|
+
const domain = slugPart(input.domain) || void 0;
|
|
5302
|
+
const res = await req("POST", "/agent/profiles/list", {
|
|
5303
|
+
profile,
|
|
5304
|
+
...domain ? { domain } : {},
|
|
5305
|
+
...input.connection_id ? { connection_id: input.connection_id } : {}
|
|
5306
|
+
});
|
|
5307
|
+
if (!res.ok) return errorResult("browser_profile_list", res.data);
|
|
5308
|
+
const connections = (Array.isArray(res.data?.connections) ? res.data.connections : []).map(normalizeLogin);
|
|
5309
|
+
return structuredResult({
|
|
5310
|
+
ok: true,
|
|
5311
|
+
tool: "browser_profile_list",
|
|
5312
|
+
session_id: null,
|
|
5313
|
+
profile,
|
|
5314
|
+
connections,
|
|
5315
|
+
count: connections.length
|
|
5316
|
+
});
|
|
5317
|
+
}
|
|
5318
|
+
);
|
|
5319
|
+
server.registerTool(
|
|
5320
|
+
"browser_open",
|
|
5321
|
+
{
|
|
5322
|
+
title: "Open Browser Session",
|
|
5323
|
+
description: "Open a direct no-proxy hosted browser session you can drive. Pass a saved profile name to load a session that is already logged into the sites saved in that profile (one profile can hold several logins). To set up or refresh a login first, use browser_profile_connect, then poll browser_profile_list until it reads AUTHENTICATED. Returns a session_id used by all other browser_* tools.",
|
|
5324
|
+
inputSchema: BrowserOpenInputSchema,
|
|
5325
|
+
outputSchema: BrowserOpenOutputSchema,
|
|
5326
|
+
annotations: annotations("Open Browser Session")
|
|
5327
|
+
},
|
|
5328
|
+
async (input) => {
|
|
5329
|
+
const profile = input.profile ?? browserServiceProfileName();
|
|
5330
|
+
const saveProfileChanges = input.save_profile_changes ?? browserServiceProfileSaveChanges();
|
|
5331
|
+
const open = await req("POST", "/agent/sessions", {
|
|
5332
|
+
label: input.label,
|
|
5333
|
+
...profile ? { profile } : {},
|
|
5334
|
+
...profile && typeof saveProfileChanges === "boolean" ? { save_profile_changes: saveProfileChanges } : {},
|
|
5335
|
+
disable_default_proxy: true,
|
|
5336
|
+
timeout_seconds: input.timeout_seconds,
|
|
5337
|
+
...input.url ? { url: input.url } : {}
|
|
5338
|
+
});
|
|
5339
|
+
if (!open.ok) return errorResult("browser_open", open.data);
|
|
5340
|
+
const session = open.data;
|
|
5341
|
+
return structuredResult({
|
|
5342
|
+
ok: true,
|
|
5343
|
+
tool: "browser_open",
|
|
5344
|
+
session_id: session.session_id,
|
|
5345
|
+
watch_url: `${consoleBase}/console/${session.session_id}`,
|
|
5346
|
+
live_view_url: null,
|
|
5347
|
+
url: input.url ?? null,
|
|
5348
|
+
hint: "Call browser_screenshot to see the page. Click by the x,y of an element from the snapshot.",
|
|
5349
|
+
raw: session
|
|
5350
|
+
});
|
|
5351
|
+
}
|
|
5352
|
+
);
|
|
5353
|
+
server.registerTool(
|
|
5354
|
+
"browser_screenshot",
|
|
5355
|
+
{
|
|
5356
|
+
title: "See Page (Screenshot + Elements)",
|
|
5357
|
+
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.",
|
|
5358
|
+
inputSchema: BrowserSessionInputSchema,
|
|
5359
|
+
outputSchema: BrowserScreenshotOutputSchema,
|
|
5360
|
+
annotations: annotations("See Page", true)
|
|
5361
|
+
},
|
|
5362
|
+
async (input) => {
|
|
5363
|
+
const res = await req("POST", `/agent/sessions/${input.session_id}/screenshot`);
|
|
5364
|
+
if (!res.ok) return errorResult("browser_screenshot", res.data, input.session_id);
|
|
5365
|
+
const { image_base64, mime_type, url, title, elements, text } = res.data;
|
|
5366
|
+
const content = [];
|
|
5367
|
+
if (image_base64) content.push({ type: "image", data: image_base64, mimeType: mime_type ?? "image/png" });
|
|
5368
|
+
const structured = {
|
|
5369
|
+
ok: true,
|
|
5370
|
+
tool: "browser_screenshot",
|
|
5371
|
+
session_id: input.session_id,
|
|
5372
|
+
url: url ?? null,
|
|
5373
|
+
title: title ?? null,
|
|
5374
|
+
text: typeof text === "string" ? text : "",
|
|
5375
|
+
elements: Array.isArray(elements) ? elements : [],
|
|
5376
|
+
screenshot: image_base64 ? { mime_type: mime_type ?? "image/png", inline: true } : null
|
|
5377
|
+
};
|
|
5378
|
+
content.push({
|
|
5379
|
+
type: "text",
|
|
5380
|
+
text: JSON.stringify(structured)
|
|
5381
|
+
});
|
|
5382
|
+
return { content, structuredContent: structured };
|
|
5383
|
+
}
|
|
5384
|
+
);
|
|
5385
|
+
server.registerTool(
|
|
5386
|
+
"browser_read",
|
|
5387
|
+
{
|
|
5388
|
+
title: "Read Page Text + Elements",
|
|
5389
|
+
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.",
|
|
5390
|
+
inputSchema: BrowserSessionInputSchema,
|
|
5391
|
+
outputSchema: BrowserReadOutputSchema,
|
|
5392
|
+
annotations: annotations("Read Page", true)
|
|
5393
|
+
},
|
|
5394
|
+
async (input) => {
|
|
5395
|
+
const res = await req("POST", `/agent/sessions/${input.session_id}/read`);
|
|
5396
|
+
if (!res.ok) return errorResult("browser_read", res.data, input.session_id);
|
|
5397
|
+
return structuredResult({
|
|
5398
|
+
ok: true,
|
|
5399
|
+
tool: "browser_read",
|
|
5400
|
+
session_id: input.session_id,
|
|
5401
|
+
url: res.data?.url ?? null,
|
|
5402
|
+
title: res.data?.title ?? null,
|
|
5403
|
+
text: typeof res.data?.text === "string" ? res.data.text : "",
|
|
5404
|
+
elements: Array.isArray(res.data?.elements) ? res.data.elements : [],
|
|
5405
|
+
raw: res.data
|
|
5406
|
+
});
|
|
5407
|
+
}
|
|
5408
|
+
);
|
|
5409
|
+
server.registerTool(
|
|
5410
|
+
"browser_locate",
|
|
5411
|
+
{
|
|
5412
|
+
title: "Locate DOM Targets",
|
|
5413
|
+
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.",
|
|
5414
|
+
inputSchema: BrowserLocateInputSchema,
|
|
5415
|
+
outputSchema: BrowserLocateOutputSchema,
|
|
5416
|
+
annotations: annotations("Locate DOM Targets", true)
|
|
5417
|
+
},
|
|
5418
|
+
async (input) => {
|
|
5419
|
+
const res = await req("POST", `/agent/sessions/${input.session_id}/locate`, { targets: input.targets });
|
|
5420
|
+
if (!res.ok) return errorResult("browser_locate", res.data, input.session_id);
|
|
5421
|
+
return structuredResult({
|
|
5422
|
+
ok: true,
|
|
5423
|
+
tool: "browser_locate",
|
|
5424
|
+
session_id: input.session_id,
|
|
5425
|
+
url: res.data?.url ?? null,
|
|
5426
|
+
title: res.data?.title ?? null,
|
|
5427
|
+
viewport: res.data?.viewport ?? null,
|
|
5428
|
+
replay: res.data?.replay ?? null,
|
|
5429
|
+
targets: Array.isArray(res.data?.targets) ? res.data.targets : [],
|
|
5430
|
+
raw: res.data
|
|
5431
|
+
});
|
|
5432
|
+
}
|
|
5433
|
+
);
|
|
5434
|
+
server.registerTool(
|
|
5435
|
+
"browser_goto",
|
|
5436
|
+
{
|
|
5437
|
+
title: "Navigate To URL",
|
|
5438
|
+
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.",
|
|
5439
|
+
inputSchema: BrowserGotoInputSchema,
|
|
5440
|
+
outputSchema: BrowserActionOutputSchema,
|
|
5441
|
+
annotations: annotations("Navigate To URL")
|
|
5442
|
+
},
|
|
5443
|
+
async (input) => {
|
|
5444
|
+
const res = await req("POST", `/agent/sessions/${input.session_id}/goto`, { url: input.url });
|
|
5445
|
+
return actionResult("browser_goto", input.session_id, res.ok, res.data, "browser_screenshot");
|
|
5446
|
+
}
|
|
5447
|
+
);
|
|
5448
|
+
server.registerTool(
|
|
5449
|
+
"browser_click",
|
|
5450
|
+
{
|
|
5451
|
+
title: "Click",
|
|
5452
|
+
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.",
|
|
5453
|
+
inputSchema: BrowserClickInputSchema,
|
|
5454
|
+
outputSchema: BrowserActionOutputSchema,
|
|
5455
|
+
annotations: annotations("Click")
|
|
5456
|
+
},
|
|
5457
|
+
async (input) => {
|
|
5458
|
+
const res = await req("POST", `/agent/sessions/${input.session_id}/click`, {
|
|
5459
|
+
x: input.x,
|
|
5460
|
+
y: input.y,
|
|
5461
|
+
button: input.button,
|
|
5462
|
+
num_clicks: input.num_clicks
|
|
5463
|
+
});
|
|
5464
|
+
return actionResult("browser_click", input.session_id, res.ok, res.data, "browser_screenshot");
|
|
5465
|
+
}
|
|
5466
|
+
);
|
|
5467
|
+
server.registerTool(
|
|
5468
|
+
"browser_type",
|
|
5469
|
+
{
|
|
5470
|
+
title: "Type Text",
|
|
5471
|
+
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.',
|
|
5472
|
+
inputSchema: BrowserTypeInputSchema,
|
|
5473
|
+
outputSchema: BrowserActionOutputSchema,
|
|
5474
|
+
annotations: annotations("Type Text")
|
|
5475
|
+
},
|
|
5476
|
+
async (input) => {
|
|
5477
|
+
const res = await req("POST", `/agent/sessions/${input.session_id}/type`, { text: input.text, delay: input.delay });
|
|
5478
|
+
return actionResult("browser_type", input.session_id, res.ok, res.data, "browser_screenshot");
|
|
5479
|
+
}
|
|
5480
|
+
);
|
|
5481
|
+
server.registerTool(
|
|
5482
|
+
"browser_scroll",
|
|
5483
|
+
{
|
|
5484
|
+
title: "Scroll",
|
|
5485
|
+
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.",
|
|
5486
|
+
inputSchema: BrowserScrollInputSchema,
|
|
5487
|
+
outputSchema: BrowserActionOutputSchema,
|
|
5488
|
+
annotations: annotations("Scroll")
|
|
5489
|
+
},
|
|
5490
|
+
async (input) => {
|
|
5491
|
+
const res = await req("POST", `/agent/sessions/${input.session_id}/scroll`, {
|
|
5492
|
+
delta_y: input.delta_y,
|
|
5493
|
+
delta_x: input.delta_x,
|
|
5494
|
+
x: input.x,
|
|
5495
|
+
y: input.y
|
|
5496
|
+
});
|
|
5497
|
+
return actionResult("browser_scroll", input.session_id, res.ok, res.data, "browser_screenshot");
|
|
5498
|
+
}
|
|
5499
|
+
);
|
|
5500
|
+
server.registerTool(
|
|
5501
|
+
"browser_press",
|
|
5502
|
+
{
|
|
5503
|
+
title: "Press Keys",
|
|
5504
|
+
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.',
|
|
5505
|
+
inputSchema: BrowserPressInputSchema,
|
|
5506
|
+
outputSchema: BrowserActionOutputSchema,
|
|
5507
|
+
annotations: annotations("Press Keys")
|
|
5508
|
+
},
|
|
5509
|
+
async (input) => {
|
|
5510
|
+
const res = await req("POST", `/agent/sessions/${input.session_id}/press`, { keys: input.keys });
|
|
5511
|
+
return actionResult("browser_press", input.session_id, res.ok, res.data, "browser_screenshot");
|
|
5512
|
+
}
|
|
5513
|
+
);
|
|
5514
|
+
server.registerTool(
|
|
5515
|
+
"browser_replay_start",
|
|
5516
|
+
{
|
|
5517
|
+
title: "Start Recording",
|
|
5518
|
+
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.",
|
|
5519
|
+
inputSchema: BrowserSessionInputSchema,
|
|
5520
|
+
outputSchema: BrowserReplayStartOutputSchema,
|
|
5521
|
+
annotations: annotations("Start Recording")
|
|
5522
|
+
},
|
|
5523
|
+
async (input) => {
|
|
5524
|
+
const res = await req("POST", `/agent/sessions/${input.session_id}/replay/start`);
|
|
5525
|
+
if (!res.ok) return errorResult("browser_replay_start", res.data, input.session_id);
|
|
5526
|
+
return structuredResult({
|
|
5527
|
+
ok: true,
|
|
5528
|
+
tool: "browser_replay_start",
|
|
5529
|
+
session_id: input.session_id,
|
|
5530
|
+
replay_id: res.data?.replay_id ?? res.data?.replayId ?? null,
|
|
5531
|
+
view_url: res.data?.view_url ?? res.data?.viewUrl ?? null,
|
|
5532
|
+
download_url: res.data?.download_url ?? res.data?.downloadUrl ?? null,
|
|
5533
|
+
raw: res.data
|
|
5534
|
+
});
|
|
5535
|
+
}
|
|
5536
|
+
);
|
|
5537
|
+
server.registerTool(
|
|
5538
|
+
"browser_replay_stop",
|
|
5539
|
+
{
|
|
5540
|
+
title: "Stop Recording",
|
|
5541
|
+
description: "Stop a replay recording and expose the final view_url and download_url. Use browser_replay_download to save the MP4 to the configured output directory.",
|
|
5542
|
+
inputSchema: BrowserReplayStopInputSchema,
|
|
5543
|
+
outputSchema: BrowserReplayStopOutputSchema,
|
|
5544
|
+
annotations: annotations("Stop Recording")
|
|
5545
|
+
},
|
|
5546
|
+
async (input) => {
|
|
5547
|
+
const res = await req("POST", `/agent/sessions/${input.session_id}/replay/stop`, { replay_id: input.replay_id });
|
|
5548
|
+
if (!res.ok) return errorResult("browser_replay_stop", res.data, input.session_id, input.replay_id);
|
|
5549
|
+
return structuredResult({
|
|
5550
|
+
ok: true,
|
|
5551
|
+
tool: "browser_replay_stop",
|
|
5552
|
+
session_id: input.session_id,
|
|
5553
|
+
replay_id: res.data?.replay_id ?? res.data?.replayId ?? input.replay_id,
|
|
5554
|
+
view_url: res.data?.view_url ?? res.data?.viewUrl ?? null,
|
|
5555
|
+
download_url: res.data?.download_url ?? res.data?.downloadUrl ?? null,
|
|
5556
|
+
raw: res.data
|
|
5557
|
+
});
|
|
5558
|
+
}
|
|
5559
|
+
);
|
|
5560
|
+
server.registerTool(
|
|
5561
|
+
"browser_list_replays",
|
|
5562
|
+
{
|
|
5563
|
+
title: "List Replay Videos",
|
|
5564
|
+
description: "List replay recordings for a browser session, including final view_url and authenticated download_url values when available.",
|
|
5565
|
+
inputSchema: BrowserSessionInputSchema,
|
|
5566
|
+
outputSchema: BrowserListReplaysOutputSchema,
|
|
5567
|
+
annotations: annotations("List Replay Videos", true)
|
|
5568
|
+
},
|
|
5569
|
+
async (input) => {
|
|
5570
|
+
const res = await req("GET", `/agent/sessions/${input.session_id}/replays`);
|
|
5571
|
+
if (!res.ok) return errorResult("browser_list_replays", res.data, input.session_id);
|
|
5572
|
+
const replays = Array.isArray(res.data?.replays) ? res.data.replays : [];
|
|
5573
|
+
return structuredResult({
|
|
5574
|
+
ok: true,
|
|
5575
|
+
tool: "browser_list_replays",
|
|
5576
|
+
session_id: input.session_id,
|
|
5577
|
+
replays,
|
|
5578
|
+
count: replays.length
|
|
5579
|
+
});
|
|
5580
|
+
}
|
|
5581
|
+
);
|
|
5582
|
+
server.registerTool(
|
|
5583
|
+
"browser_replay_download",
|
|
5584
|
+
{
|
|
5585
|
+
title: "Download Replay MP4",
|
|
5586
|
+
description: "Download a replay recording through MCP Scraper and save the MP4 under MCP_SCRAPER_OUTPUT_DIR/browser-replays. Use after browser_replay_stop or browser_list_replays.",
|
|
5587
|
+
inputSchema: BrowserReplayDownloadInputSchema,
|
|
5588
|
+
outputSchema: BrowserReplayDownloadOutputSchema,
|
|
5589
|
+
annotations: annotations("Download Replay MP4")
|
|
5590
|
+
},
|
|
5591
|
+
async (input) => {
|
|
5592
|
+
const res = await downloadReplay(input.session_id, input.replay_id, input.filename);
|
|
5593
|
+
if (!res.ok) return errorResult("browser_replay_download", res.data, input.session_id, input.replay_id);
|
|
5594
|
+
return structuredResult({
|
|
5595
|
+
ok: true,
|
|
5596
|
+
tool: "browser_replay_download",
|
|
5597
|
+
session_id: input.session_id,
|
|
5598
|
+
replay_id: input.replay_id,
|
|
5599
|
+
file_path: res.data?.file_path ?? null,
|
|
5600
|
+
bytes: typeof res.data?.bytes === "number" ? res.data.bytes : null,
|
|
5601
|
+
mime_type: res.data?.mime_type ?? null,
|
|
5602
|
+
download_url: res.data?.download_url ?? null
|
|
5603
|
+
});
|
|
5604
|
+
}
|
|
5605
|
+
);
|
|
5606
|
+
server.registerTool(
|
|
5607
|
+
"browser_replay_mark",
|
|
5608
|
+
{
|
|
5609
|
+
title: "Mark Replay Annotation",
|
|
5610
|
+
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.",
|
|
5611
|
+
inputSchema: BrowserReplayMarkInputSchema,
|
|
5612
|
+
outputSchema: BrowserReplayMarkOutputSchema,
|
|
5613
|
+
annotations: annotations("Mark Replay Annotation", true)
|
|
5614
|
+
},
|
|
5615
|
+
async (input) => {
|
|
5616
|
+
const res = await req("POST", `/agent/sessions/${input.session_id}/locate`, { targets: [input.target] });
|
|
5617
|
+
if (!res.ok) return errorResult("browser_replay_mark", res.data, input.session_id);
|
|
5618
|
+
const target = res.data?.targets?.[0];
|
|
5619
|
+
const element = target?.element;
|
|
5620
|
+
const elapsed = res.data?.replay?.replay_elapsed_seconds;
|
|
5621
|
+
if (!target?.found || !element) {
|
|
5622
|
+
return errorResult("browser_replay_mark", { error: target?.error ?? "target not found in current viewport", target }, input.session_id);
|
|
5623
|
+
}
|
|
5624
|
+
if (!finiteNumber2(elapsed)) {
|
|
5625
|
+
return errorResult("browser_replay_mark", { error: "no active replay clock found; call browser_replay_start before browser_replay_mark" }, input.session_id);
|
|
5626
|
+
}
|
|
5627
|
+
const padded = expandElementBounds(element, res.data?.viewport, input.padding ?? 8);
|
|
5628
|
+
const start = Math.max(0, elapsed + (input.start_offset_seconds ?? -0.25));
|
|
5629
|
+
const duration = input.duration_seconds ?? 4;
|
|
5630
|
+
const annotation = {
|
|
5631
|
+
type: input.type ?? "box",
|
|
5632
|
+
start_seconds: Number(start.toFixed(3)),
|
|
5633
|
+
end_seconds: Number((start + duration).toFixed(3)),
|
|
5634
|
+
left: Math.round(padded.left),
|
|
5635
|
+
top: Math.round(padded.top),
|
|
5636
|
+
width: Math.round(padded.width),
|
|
5637
|
+
height: Math.round(padded.height),
|
|
5638
|
+
...input.label ? { label: input.label } : {},
|
|
5639
|
+
...input.color ? { color: input.color } : {},
|
|
5640
|
+
...input.thickness ? { thickness: input.thickness } : {}
|
|
5641
|
+
};
|
|
5642
|
+
return structuredResult({
|
|
5643
|
+
ok: true,
|
|
5644
|
+
tool: "browser_replay_mark",
|
|
5645
|
+
session_id: input.session_id,
|
|
5646
|
+
replay_id: res.data?.replay?.replay_id ?? res.data?.replay?.replayId ?? null,
|
|
5647
|
+
annotation,
|
|
5648
|
+
source_width: padded.sourceWidth,
|
|
5649
|
+
source_height: padded.sourceHeight,
|
|
5650
|
+
replay: res.data.replay,
|
|
5651
|
+
target,
|
|
5652
|
+
hint: "Append annotation to your annotations array. Use source_width/source_height with browser_replay_annotate."
|
|
5653
|
+
});
|
|
5654
|
+
}
|
|
5655
|
+
);
|
|
5656
|
+
server.registerTool(
|
|
5657
|
+
"browser_replay_annotate",
|
|
5658
|
+
{
|
|
5659
|
+
title: "Annotate Replay MP4",
|
|
5660
|
+
description: "Download a browser replay MP4, render visual annotations over it, and save a new annotated MP4 under MCP_SCRAPER_OUTPUT_DIR/browser-replays. Use this after browser_replay_stop when the user wants proof videos with circles, boxes, arrows, underlines, or labels. For accurate timing and placement, prefer annotations returned by browser_replay_mark while the replay is recording; otherwise use exact left/top/width/height bounds from browser_locate. If the replay video size differs from the screenshot coordinate space, pass source_width and source_height.",
|
|
5661
|
+
inputSchema: BrowserReplayAnnotateInputSchema,
|
|
5662
|
+
outputSchema: BrowserReplayAnnotateOutputSchema,
|
|
5663
|
+
annotations: annotations("Annotate Replay MP4")
|
|
5664
|
+
},
|
|
5665
|
+
async (input) => {
|
|
5666
|
+
const sourceName = input.filename ? `${input.filename}-source` : void 0;
|
|
5667
|
+
const downloaded = await downloadReplay(input.session_id, input.replay_id, sourceName);
|
|
5668
|
+
if (!downloaded.ok) return errorResult("browser_replay_annotate", downloaded.data, input.session_id, input.replay_id);
|
|
5669
|
+
try {
|
|
5670
|
+
const sourcePath = String(downloaded.data.file_path);
|
|
5671
|
+
const outputPath = annotatedReplayFilePath(input.session_id, input.replay_id, input.filename);
|
|
5672
|
+
mkdirSync3(join5(outputBaseDir3(), "browser-replays"), { recursive: true });
|
|
5673
|
+
const result = await annotateReplayVideo(sourcePath, outputPath, {
|
|
5674
|
+
annotations: input.annotations,
|
|
5675
|
+
sourceWidth: input.source_width,
|
|
5676
|
+
sourceHeight: input.source_height,
|
|
5677
|
+
sourceLeftOffset: input.source_left_offset,
|
|
5678
|
+
sourceTopOffset: input.source_top_offset
|
|
5679
|
+
});
|
|
5680
|
+
return structuredResult({
|
|
5681
|
+
ok: true,
|
|
5682
|
+
tool: "browser_replay_annotate",
|
|
5683
|
+
session_id: input.session_id,
|
|
5684
|
+
replay_id: input.replay_id,
|
|
5685
|
+
source_file_path: sourcePath,
|
|
5686
|
+
annotated_file_path: result.filePath,
|
|
5687
|
+
bytes: result.bytes,
|
|
5688
|
+
width: result.width,
|
|
5689
|
+
height: result.height,
|
|
5690
|
+
annotation_count: result.annotationCount,
|
|
5691
|
+
mime_type: "video/mp4"
|
|
5692
|
+
});
|
|
5693
|
+
} catch (err) {
|
|
5694
|
+
return errorResult("browser_replay_annotate", { error: err instanceof Error ? err.message : String(err) }, input.session_id, input.replay_id);
|
|
5695
|
+
}
|
|
5696
|
+
}
|
|
5697
|
+
);
|
|
5698
|
+
server.registerTool(
|
|
5699
|
+
"browser_close",
|
|
5700
|
+
{
|
|
5701
|
+
title: "Close Browser Session",
|
|
5702
|
+
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.",
|
|
5703
|
+
inputSchema: BrowserSessionInputSchema,
|
|
5704
|
+
outputSchema: BrowserCloseOutputSchema,
|
|
5705
|
+
annotations: { title: "Close Browser Session", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
|
|
5706
|
+
},
|
|
5707
|
+
async (input) => {
|
|
5708
|
+
const res = await req("DELETE", `/agent/sessions/${input.session_id}`);
|
|
5709
|
+
if (!res.ok) return errorResult("browser_close", res.data, input.session_id);
|
|
5710
|
+
return structuredResult({
|
|
5711
|
+
ok: true,
|
|
5712
|
+
tool: "browser_close",
|
|
5713
|
+
session_id: input.session_id,
|
|
5714
|
+
closed: true,
|
|
5715
|
+
raw: res.data
|
|
5716
|
+
});
|
|
5717
|
+
}
|
|
5718
|
+
);
|
|
5719
|
+
server.registerTool(
|
|
5720
|
+
"browser_list_sessions",
|
|
5721
|
+
{
|
|
5722
|
+
title: "List Browser Sessions",
|
|
5723
|
+
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.",
|
|
5724
|
+
inputSchema: BrowserListInputSchema,
|
|
5725
|
+
outputSchema: BrowserListSessionsOutputSchema,
|
|
5726
|
+
annotations: annotations("List Browser Sessions", true)
|
|
5727
|
+
},
|
|
5728
|
+
async (input) => {
|
|
5729
|
+
const res = await req("GET", `/agent/sessions${input.include_closed ? "?all=1" : ""}`);
|
|
5730
|
+
if (!res.ok) return errorResult("browser_list_sessions", res.data);
|
|
5731
|
+
const sessions = (res.data.sessions ?? []).map((s) => ({ ...s, watch_url: `${consoleBase}/console/${s.session_id}` }));
|
|
5732
|
+
return structuredResult({
|
|
5733
|
+
ok: true,
|
|
5734
|
+
tool: "browser_list_sessions",
|
|
5735
|
+
session_id: null,
|
|
5736
|
+
sessions,
|
|
5737
|
+
count: sessions.length
|
|
5738
|
+
});
|
|
5739
|
+
}
|
|
5740
|
+
);
|
|
5741
|
+
server.registerTool(
|
|
5742
|
+
"query_fanout_workflow",
|
|
5743
|
+
{
|
|
5744
|
+
title: "Capture AI Search Fan-Out",
|
|
5745
|
+
description: `Capture the query fan-out behind a ChatGPT or Claude web-search answer for Answer Engine Optimization (AEO): the exact sub-queries the model issued, every researched URL split into cited vs browsed-only (with citation frequency and snippet), each source tagged by category (First-party/vendor, News/media, Reddit, Social/video, Encyclopedia, Review site, Docs, Blog), plus top sourced sites and citation order. Set export=true for durable JSON, CSV, TSV, and HTML artifacts; export paths are returned relative to MCP_SCRAPER_OUTPUT_DIR. This returns the raw structured data; YOU analyze it \u2014 classify each sub-query by funnel stage (Problem-aware, Solution-aware, Decision-aware, Retention) and type (BoFu, Branded, Comparison, How-to, Search operator), name the brands the model researched, and give AEO insights. Sample asks: "capture the fan-out for this ChatGPT answer", "what did Claude search and cite for best CRM", "show the sub-queries and sources behind this AI answer". WRITE NOTE: passing prompt submits a real message in the user's logged-in account (a new conversation turn) \u2014 only send when the user wants that; omit prompt to capture a prompt the user just ran. Setup, if not already connected or if the profile disconnected: call browser_profile_connect, give the user the watch_url, let them complete login, then poll browser_profile_list until status is AUTHENTICATED. After that, browser_open the saved direct no-proxy profile and go to chatgpt.com or claude.ai. Fan-out is captured only as it streams, so the session must be open when the prompt runs. NOT for Google AI Overview citations \u2014 use harvest_paa for those; this tool is ChatGPT and Claude only.`,
|
|
5746
|
+
inputSchema: BrowserCaptureFanoutInputSchema,
|
|
5747
|
+
outputSchema: BrowserCaptureFanoutOutputSchema,
|
|
5748
|
+
annotations: annotations("Capture AI Search Fan-Out")
|
|
5749
|
+
},
|
|
5750
|
+
async (input) => {
|
|
5751
|
+
const emit = (result, exports2) => structuredResult({
|
|
5752
|
+
ok: true,
|
|
5753
|
+
tool: "query_fanout_workflow",
|
|
5754
|
+
session_id: input.session_id,
|
|
5755
|
+
platform: result.platform,
|
|
5756
|
+
captured_at: result.capturedAt,
|
|
5757
|
+
prompt: result.prompt,
|
|
5758
|
+
meta: result.meta,
|
|
5759
|
+
queries: result.queries,
|
|
5760
|
+
browsed_urls: result.browsedUrls,
|
|
5761
|
+
cited_urls: result.citedUrls,
|
|
5762
|
+
browsed_only: result.browsedOnly,
|
|
5763
|
+
snippets: result.snippets,
|
|
5764
|
+
counts: result.counts,
|
|
5765
|
+
aggregates: result.aggregates,
|
|
5766
|
+
first_party_domain: result.firstPartyDomain,
|
|
5767
|
+
exports: exports2 ?? null,
|
|
5768
|
+
...result.debug ? { debug: result.debug } : {}
|
|
5769
|
+
});
|
|
5770
|
+
const res = await req("POST", `/agent/sessions/${input.session_id}/capture-fanout`, {
|
|
5771
|
+
prompt: input.prompt,
|
|
5772
|
+
wait_ms: input.wait_ms,
|
|
5773
|
+
first_party_domain: input.first_party_domain,
|
|
5774
|
+
reset: input.reset,
|
|
5775
|
+
export: input.export
|
|
5776
|
+
}, Math.max(timeoutMs, (input.wait_ms ?? (input.prompt ? 9e4 : 8e3)) + 3e4));
|
|
5777
|
+
if (!res.ok) return errorResult("query_fanout_workflow", res.data, input.session_id);
|
|
5778
|
+
const hosted = res.data?.result ?? res.data;
|
|
5779
|
+
let exports = res.data?.exports ?? null;
|
|
5780
|
+
if (input.export && !exports && isEnrichedFanout(hosted)) {
|
|
5781
|
+
try {
|
|
5782
|
+
exports = exportFanout(hosted);
|
|
5783
|
+
} catch (err) {
|
|
5784
|
+
return errorResult(
|
|
5785
|
+
"query_fanout_workflow",
|
|
5786
|
+
{
|
|
5787
|
+
error: `Fan-out captured but local export failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
5788
|
+
result: hosted
|
|
5789
|
+
},
|
|
5790
|
+
input.session_id
|
|
5791
|
+
);
|
|
5792
|
+
}
|
|
5793
|
+
}
|
|
5794
|
+
return emit(hosted, exports);
|
|
5795
|
+
}
|
|
5796
|
+
);
|
|
5797
|
+
}
|
|
5798
|
+
|
|
4427
5799
|
export {
|
|
4428
5800
|
harvestTimeoutBudget,
|
|
4429
|
-
sanitizeOutboundDiagnostics,
|
|
4430
5801
|
sanitizeAttempts,
|
|
4431
5802
|
sanitizeHarvestResult,
|
|
4432
5803
|
buildLinkGraph,
|
|
@@ -4438,17 +5809,12 @@ export {
|
|
|
4438
5809
|
renderImageSection,
|
|
4439
5810
|
configureReportSaving,
|
|
4440
5811
|
outputBaseDir,
|
|
4441
|
-
formatCaptureSerpSnapshot,
|
|
4442
|
-
formatCaptureSerpPageSnapshots,
|
|
4443
5812
|
SERVER_INSTRUCTIONS,
|
|
4444
|
-
|
|
4445
|
-
CaptureSerpPageSnapshotsOutputSchema,
|
|
4446
|
-
CaptureSerpSnapshotInputSchema,
|
|
4447
|
-
CaptureSerpPageSnapshotsInputSchema,
|
|
4448
|
-
liveWebToolAnnotations,
|
|
5813
|
+
registerSerpIntelligenceCaptureTools,
|
|
4449
5814
|
buildPaaExtractorMcpServer,
|
|
4450
5815
|
registerPaaExtractorMcpTools,
|
|
4451
5816
|
HttpMcpToolExecutor,
|
|
4452
|
-
exportFanout
|
|
5817
|
+
exportFanout,
|
|
5818
|
+
registerBrowserAgentMcpTools
|
|
4453
5819
|
};
|
|
4454
|
-
//# sourceMappingURL=chunk-
|
|
5820
|
+
//# sourceMappingURL=chunk-NGD4ITAO.js.map
|