mcp-scraper 0.2.19 → 0.2.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -5
- package/dist/bin/api-server.cjs +1072 -349
- package/dist/bin/api-server.cjs.map +1 -1
- package/dist/bin/api-server.js +3 -3
- package/dist/bin/browser-agent-stdio-server.cjs +1274 -75
- package/dist/bin/browser-agent-stdio-server.cjs.map +1 -1
- package/dist/bin/browser-agent-stdio-server.js +4 -2
- package/dist/bin/browser-agent-stdio-server.js.map +1 -1
- package/dist/bin/mcp-scraper-cli.cjs +659 -210
- package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
- package/dist/bin/mcp-scraper-cli.js +108 -9
- package/dist/bin/mcp-scraper-cli.js.map +1 -1
- package/dist/bin/mcp-scraper-combined-stdio-server.cjs +2104 -566
- package/dist/bin/mcp-scraper-combined-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-scraper-combined-stdio-server.js +7 -4
- package/dist/bin/mcp-scraper-combined-stdio-server.js.map +1 -1
- package/dist/bin/mcp-scraper-install.cjs +3 -3
- package/dist/bin/mcp-scraper-install.cjs.map +1 -1
- package/dist/bin/mcp-scraper-install.js +2 -2
- package/dist/bin/mcp-stdio-server.cjs +726 -387
- package/dist/bin/mcp-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-stdio-server.js +3 -2
- package/dist/bin/mcp-stdio-server.js.map +1 -1
- package/dist/bin/paa-harvest.cjs +5 -3
- package/dist/bin/paa-harvest.cjs.map +1 -1
- package/dist/bin/paa-harvest.js +5 -2
- package/dist/bin/paa-harvest.js.map +1 -1
- package/dist/{chunk-TL7YTFLH.js → chunk-DBQDG7EH.js} +99 -27
- package/dist/chunk-DBQDG7EH.js.map +1 -0
- package/dist/chunk-DUEW4EOO.js +214 -0
- package/dist/chunk-DUEW4EOO.js.map +1 -0
- package/dist/{chunk-F44RBOJ5.js → chunk-H2R232HK.js} +216 -108
- package/dist/chunk-H2R232HK.js.map +1 -0
- package/dist/chunk-HS6OKELW.js +1786 -0
- package/dist/chunk-HS6OKELW.js.map +1 -0
- package/dist/{chunk-CQTAKXBN.js → chunk-IPW4LFOT.js} +11 -116
- package/dist/chunk-IPW4LFOT.js.map +1 -0
- package/dist/chunk-LFATOGDF.js +111 -0
- package/dist/chunk-LFATOGDF.js.map +1 -0
- package/dist/{chunk-MCBOZ2JF.js → chunk-MLNCKQ2B.js} +3 -3
- package/dist/chunk-MLNCKQ2B.js.map +1 -0
- package/dist/{chunk-4UEVYYDW.js → chunk-O5J7E4VX.js} +394 -57
- package/dist/chunk-O5J7E4VX.js.map +1 -0
- package/dist/{chunk-BSYPATSM.js → chunk-SXTXMFEQ.js} +2 -2
- package/dist/chunk-WN7PBKMV.js +28 -0
- package/dist/chunk-WN7PBKMV.js.map +1 -0
- package/dist/chunk-XKUDVN2E.js +7 -0
- package/dist/chunk-XKUDVN2E.js.map +1 -0
- package/dist/{db-P5X6UQ3E.js → db-BE4JVB3V.js} +8 -2
- package/dist/index.cjs +5 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/{server-CRUSZA2Q.js → server-24XIQ6W6.js} +248 -40
- package/dist/server-24XIQ6W6.js.map +1 -0
- package/dist/{worker-OZSWIS3F.js → worker-SLQ375UG.js} +8 -5
- package/dist/{worker-OZSWIS3F.js.map → worker-SLQ375UG.js.map} +1 -1
- package/docs/adr/0001-in-page-graphql-interception-for-anti-bot-scraping.md +2 -2
- package/docs/mcp-tool-craft-lint.generated.md +51 -0
- package/docs/mcp-tool-manifest.generated.json +1534 -0
- package/docs/specs/local-competitive-audit-spec.md +1 -2
- package/docs/specs/mcp-tool-definition-quality-audit-spec.md +1602 -0
- package/package.json +4 -1
- package/dist/chunk-4UEVYYDW.js.map +0 -1
- package/dist/chunk-76P4DKWR.js +0 -793
- package/dist/chunk-76P4DKWR.js.map +0 -1
- package/dist/chunk-CQTAKXBN.js.map +0 -1
- package/dist/chunk-F44RBOJ5.js.map +0 -1
- package/dist/chunk-MCBOZ2JF.js.map +0 -1
- package/dist/chunk-RU2HZD55.js +0 -7
- package/dist/chunk-RU2HZD55.js.map +0 -1
- package/dist/chunk-TL7YTFLH.js.map +0 -1
- package/dist/server-CRUSZA2Q.js.map +0 -1
- /package/dist/{chunk-BSYPATSM.js.map → chunk-SXTXMFEQ.js.map} +0 -0
- /package/dist/{db-P5X6UQ3E.js.map → db-BE4JVB3V.js.map} +0 -0
|
@@ -2,30 +2,642 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
// bin/browser-agent-stdio-server.ts
|
|
5
|
-
var
|
|
6
|
-
var
|
|
7
|
-
var
|
|
5
|
+
var import_node_fs4 = require("fs");
|
|
6
|
+
var import_node_os5 = require("os");
|
|
7
|
+
var import_node_path6 = require("path");
|
|
8
8
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
9
9
|
|
|
10
10
|
// src/mcp/browser-agent-mcp-server.ts
|
|
11
11
|
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
12
|
+
var import_node_fs3 = require("fs");
|
|
13
|
+
var import_node_os4 = require("os");
|
|
14
|
+
var import_node_path5 = require("path");
|
|
15
|
+
|
|
16
|
+
// src/lib/chrome-profiles.ts
|
|
17
|
+
var import_promises = require("fs/promises");
|
|
18
|
+
var import_node_os = require("os");
|
|
19
|
+
var import_node_path = require("path");
|
|
20
|
+
function defaultChromeUserDataDir() {
|
|
21
|
+
if (process.platform === "darwin") return (0, import_node_path.join)((0, import_node_os.homedir)(), "Library", "Application Support", "Google", "Chrome");
|
|
22
|
+
if (process.platform === "win32") {
|
|
23
|
+
return (0, import_node_path.join)(process.env.LOCALAPPDATA ?? (0, import_node_path.join)((0, import_node_os.homedir)(), "AppData", "Local"), "Google", "Chrome", "User Data");
|
|
24
|
+
}
|
|
25
|
+
return (0, import_node_path.join)(process.env.XDG_CONFIG_HOME ?? (0, import_node_path.join)((0, import_node_os.homedir)(), ".config"), "google-chrome");
|
|
26
|
+
}
|
|
27
|
+
function recommendedKernelProfileName(value) {
|
|
28
|
+
return value.trim().toLowerCase().replace(/@/g, "-").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "browser-profile";
|
|
29
|
+
}
|
|
30
|
+
async function listLocalChromeProfiles(userDataDir = defaultChromeUserDataDir(), emailFilter) {
|
|
31
|
+
const raw = await (0, import_promises.readFile)((0, import_node_path.join)(userDataDir, "Local State"), "utf8");
|
|
32
|
+
const localState = JSON.parse(raw);
|
|
33
|
+
const wanted = emailFilter?.trim().toLowerCase();
|
|
34
|
+
return Object.entries(localState.profile?.info_cache ?? {}).map(([directory, info]) => {
|
|
35
|
+
const email = (info.user_name ?? "").trim();
|
|
36
|
+
const displayName = (info.name ?? info.shortcut_name ?? "").trim();
|
|
37
|
+
const labelSource = email || displayName || directory;
|
|
38
|
+
return {
|
|
39
|
+
email,
|
|
40
|
+
displayName,
|
|
41
|
+
chromeProfileDirectory: directory,
|
|
42
|
+
chromeProfilePath: (0, import_node_path.join)(userDataDir, directory),
|
|
43
|
+
recommendedKernelProfileName: recommendedKernelProfileName(labelSource)
|
|
44
|
+
};
|
|
45
|
+
}).filter((row) => !wanted || row.email.toLowerCase() === wanted || row.displayName.toLowerCase().includes(wanted));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/lib/browser-service-env.ts
|
|
49
|
+
function browserServiceProfileName() {
|
|
50
|
+
const value = (process.env.BROWSER_AGENT_PROFILE_NAME ?? process.env.BROWSER_SERVICE_PROFILE_NAME ?? process.env.KERNEL_BROWSER_PROFILE_NAME ?? process.env.KERNEL_PROFILE_NAME)?.trim();
|
|
51
|
+
return value || void 0;
|
|
52
|
+
}
|
|
53
|
+
function browserServiceProfileSaveChanges() {
|
|
54
|
+
const value = (process.env.BROWSER_AGENT_PROFILE_SAVE_CHANGES ?? process.env.BROWSER_SERVICE_PROFILE_SAVE_CHANGES ?? process.env.KERNEL_BROWSER_PROFILE_SAVE_CHANGES ?? process.env.KERNEL_PROFILE_SAVE_CHANGES)?.trim().toLowerCase();
|
|
55
|
+
if (!value) return void 0;
|
|
56
|
+
if (["1", "true", "yes", "on"].includes(value)) return true;
|
|
57
|
+
if (["0", "false", "no", "off"].includes(value)) return false;
|
|
58
|
+
return void 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/lib/local-browser-profiles.ts
|
|
12
62
|
var import_node_fs = require("fs");
|
|
63
|
+
var import_promises2 = require("fs/promises");
|
|
13
64
|
var import_node_os2 = require("os");
|
|
14
65
|
var import_node_path2 = require("path");
|
|
66
|
+
var EXCLUDED_PROFILE_NAMES = [
|
|
67
|
+
"Cache",
|
|
68
|
+
"Code Cache",
|
|
69
|
+
"Crashpad",
|
|
70
|
+
"DawnCache",
|
|
71
|
+
"DawnGraphiteCache",
|
|
72
|
+
"DawnWebGPUCache",
|
|
73
|
+
"GPUCache",
|
|
74
|
+
"GrShaderCache",
|
|
75
|
+
"ShaderCache",
|
|
76
|
+
"Shared Dictionary"
|
|
77
|
+
];
|
|
78
|
+
function defaultImportedBrowserProfilesDir() {
|
|
79
|
+
return process.env.MCP_SCRAPER_BROWSER_PROFILE_BASE_DIR?.trim() || (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".mcp-scraper", "browser-profiles");
|
|
80
|
+
}
|
|
81
|
+
function defaultChromeExecutablePath() {
|
|
82
|
+
const envPath = process.env.MCP_SCRAPER_BROWSER_EXECUTABLE?.trim() || process.env.CHROME_PATH?.trim();
|
|
83
|
+
if (envPath) return envPath;
|
|
84
|
+
const candidates = process.platform === "darwin" ? ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"] : process.platform === "win32" ? [
|
|
85
|
+
(0, import_node_path2.join)(process.env.PROGRAMFILES ?? "C:\\Program Files", "Google", "Chrome", "Application", "chrome.exe"),
|
|
86
|
+
(0, import_node_path2.join)(process.env["PROGRAMFILES(X86)"] ?? "C:\\Program Files (x86)", "Google", "Chrome", "Application", "chrome.exe")
|
|
87
|
+
] : ["/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/usr/bin/chromium-browser", "/usr/bin/chromium"];
|
|
88
|
+
return candidates.find((candidate) => (0, import_node_fs.existsSync)(candidate));
|
|
89
|
+
}
|
|
90
|
+
function localBrowserModeEnabled() {
|
|
91
|
+
return process.env.MCP_SCRAPER_BROWSER_MODE?.trim().toLowerCase() === "local";
|
|
92
|
+
}
|
|
93
|
+
function importedProfileName(value) {
|
|
94
|
+
return recommendedKernelProfileName(value);
|
|
95
|
+
}
|
|
96
|
+
function importedProfileRoot(name, outputDir = defaultImportedBrowserProfilesDir()) {
|
|
97
|
+
return (0, import_node_path2.join)(outputDir, importedProfileName(name));
|
|
98
|
+
}
|
|
99
|
+
function importedProfileManifestPath(name, outputDir = defaultImportedBrowserProfilesDir()) {
|
|
100
|
+
return (0, import_node_path2.join)(importedProfileRoot(name, outputDir), "manifest.json");
|
|
101
|
+
}
|
|
102
|
+
function importedProfileUserDataDir(name, outputDir = defaultImportedBrowserProfilesDir()) {
|
|
103
|
+
return (0, import_node_path2.join)(importedProfileRoot(name, outputDir), "user-data");
|
|
104
|
+
}
|
|
105
|
+
async function readExistingManifest(path) {
|
|
106
|
+
try {
|
|
107
|
+
return JSON.parse(await (0, import_promises2.readFile)(path, "utf8"));
|
|
108
|
+
} catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function isExcludedProfilePath(sourceRoot, sourcePath) {
|
|
113
|
+
const rel = (0, import_node_path2.relative)(sourceRoot, sourcePath);
|
|
114
|
+
if (!rel) return false;
|
|
115
|
+
const parts = rel.split(import_node_path2.sep).filter(Boolean);
|
|
116
|
+
const last = parts[parts.length - 1] ?? "";
|
|
117
|
+
if (last.startsWith("Singleton")) return true;
|
|
118
|
+
if (last === "LOCK" || last.endsWith(".lock")) return true;
|
|
119
|
+
if (parts.some((part) => EXCLUDED_PROFILE_NAMES.includes(part))) return true;
|
|
120
|
+
if (parts.includes("Service Worker") && parts.includes("CacheStorage")) return true;
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
async function copyProfileTree(sourceProfilePath, destinationProfilePath) {
|
|
124
|
+
await (0, import_promises2.cp)(sourceProfilePath, destinationProfilePath, {
|
|
125
|
+
recursive: true,
|
|
126
|
+
force: true,
|
|
127
|
+
errorOnExist: false,
|
|
128
|
+
dereference: false,
|
|
129
|
+
filter: (source) => !isExcludedProfilePath(sourceProfilePath, source)
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
function selectProfile(profiles, options) {
|
|
133
|
+
const requestedDirectory = options.chromeProfileDirectory?.trim();
|
|
134
|
+
const requestedEmail = options.email?.trim().toLowerCase();
|
|
135
|
+
const exactDirectory = requestedDirectory ? profiles.find((profile) => profile.chromeProfileDirectory === requestedDirectory) : void 0;
|
|
136
|
+
if (exactDirectory) return exactDirectory;
|
|
137
|
+
const exactEmail = requestedEmail ? profiles.find((profile) => profile.email.toLowerCase() === requestedEmail) : void 0;
|
|
138
|
+
if (exactEmail) return exactEmail;
|
|
139
|
+
const first = profiles[0];
|
|
140
|
+
if (first) return first;
|
|
141
|
+
const hint = requestedDirectory ? `Chrome profile directory "${requestedDirectory}"` : requestedEmail ? `Chrome account "${requestedEmail}"` : "Chrome profile";
|
|
142
|
+
throw new Error(`${hint} was not found in ${options.sourceUserDataDir ?? defaultChromeUserDataDir()}`);
|
|
143
|
+
}
|
|
144
|
+
async function ensureProfilePathExists(profile) {
|
|
145
|
+
const details = await (0, import_promises2.stat)(profile.chromeProfilePath).catch(() => null);
|
|
146
|
+
if (!details?.isDirectory()) {
|
|
147
|
+
throw new Error(`Chrome profile path does not exist or is not a directory: ${profile.chromeProfilePath}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async function writeNormalizedLocalState(sourceLocalStatePath, destinationLocalStatePath, sourceProfileDirectory) {
|
|
151
|
+
const raw = await (0, import_promises2.readFile)(sourceLocalStatePath, "utf8");
|
|
152
|
+
const localState = JSON.parse(raw);
|
|
153
|
+
const selectedInfo = localState.profile?.info_cache?.[sourceProfileDirectory] ?? localState.profile?.info_cache?.Default ?? {};
|
|
154
|
+
localState.profile = {
|
|
155
|
+
...localState.profile ?? {},
|
|
156
|
+
info_cache: { Default: selectedInfo },
|
|
157
|
+
last_used: "Default",
|
|
158
|
+
last_active_profiles: ["Default"]
|
|
159
|
+
};
|
|
160
|
+
await (0, import_promises2.writeFile)(destinationLocalStatePath, `${JSON.stringify(localState, null, 2)}
|
|
161
|
+
`);
|
|
162
|
+
}
|
|
163
|
+
async function importChromeProfile(options = {}) {
|
|
164
|
+
const sourceUserDataDir = options.sourceUserDataDir?.trim() || defaultChromeUserDataDir();
|
|
165
|
+
const profiles = await listLocalChromeProfiles(sourceUserDataDir, options.email);
|
|
166
|
+
const sourceProfile = selectProfile(profiles, { ...options, sourceUserDataDir });
|
|
167
|
+
await ensureProfilePathExists(sourceProfile);
|
|
168
|
+
const name = importedProfileName(options.name?.trim() || sourceProfile.recommendedKernelProfileName);
|
|
169
|
+
const outputDir = options.outputDir?.trim() || defaultImportedBrowserProfilesDir();
|
|
170
|
+
const profileRoot = importedProfileRoot(name, outputDir);
|
|
171
|
+
const manifestPath = importedProfileManifestPath(name, outputDir);
|
|
172
|
+
const userDataDir = importedProfileUserDataDir(name, outputDir);
|
|
173
|
+
const existingManifest = await readExistingManifest(manifestPath);
|
|
174
|
+
const existing = (0, import_node_fs.existsSync)(profileRoot);
|
|
175
|
+
if (existing && !options.overwrite) {
|
|
176
|
+
throw new Error(`Imported browser profile "${name}" already exists at ${profileRoot}. Re-run with --overwrite to refresh it.`);
|
|
177
|
+
}
|
|
178
|
+
if (existing) await (0, import_promises2.rm)(profileRoot, { recursive: true, force: true });
|
|
179
|
+
await (0, import_promises2.mkdir)((0, import_node_path2.join)(userDataDir, "Default"), { recursive: true });
|
|
180
|
+
await writeNormalizedLocalState(
|
|
181
|
+
(0, import_node_path2.join)(sourceUserDataDir, "Local State"),
|
|
182
|
+
(0, import_node_path2.join)(userDataDir, "Local State"),
|
|
183
|
+
sourceProfile.chromeProfileDirectory
|
|
184
|
+
);
|
|
185
|
+
await copyProfileTree(sourceProfile.chromeProfilePath, (0, import_node_path2.join)(userDataDir, "Default"));
|
|
186
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
187
|
+
const manifest = {
|
|
188
|
+
layoutVersion: 1,
|
|
189
|
+
name,
|
|
190
|
+
profileRoot,
|
|
191
|
+
userDataDir,
|
|
192
|
+
browserExecutablePath: options.browserExecutablePath?.trim() || defaultChromeExecutablePath() || null,
|
|
193
|
+
sourceUserDataDir,
|
|
194
|
+
sourceProfileDirectory: sourceProfile.chromeProfileDirectory,
|
|
195
|
+
sourceProfilePath: sourceProfile.chromeProfilePath,
|
|
196
|
+
email: sourceProfile.email,
|
|
197
|
+
displayName: sourceProfile.displayName,
|
|
198
|
+
createdAt: existingManifest?.createdAt ?? now,
|
|
199
|
+
updatedAt: now,
|
|
200
|
+
copyMode: "chrome-profile-clone"
|
|
201
|
+
};
|
|
202
|
+
await (0, import_promises2.writeFile)(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
203
|
+
`);
|
|
204
|
+
return {
|
|
205
|
+
profile: manifest,
|
|
206
|
+
sourceProfile,
|
|
207
|
+
excludedNames: EXCLUDED_PROFILE_NAMES
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
async function loadImportedBrowserProfile(name = process.env.MCP_SCRAPER_BROWSER_PROFILE?.trim() || "default", outputDir = defaultImportedBrowserProfilesDir()) {
|
|
211
|
+
const profileName = importedProfileName(name);
|
|
212
|
+
const manifestPath = importedProfileManifestPath(profileName, outputDir);
|
|
213
|
+
const manifest = await readExistingManifest(manifestPath);
|
|
214
|
+
if (!manifest) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
`Imported browser profile "${profileName}" was not found. Run: mcp-scraper-cli browser import-chrome --name ${profileName}`
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
return manifest;
|
|
220
|
+
}
|
|
221
|
+
async function syncImportedChromeProfile(name, options = {}) {
|
|
222
|
+
const outputDir = options.outputDir?.trim() || defaultImportedBrowserProfilesDir();
|
|
223
|
+
const manifest = await loadImportedBrowserProfile(name, outputDir);
|
|
224
|
+
return importChromeProfile({
|
|
225
|
+
name: manifest.name,
|
|
226
|
+
sourceUserDataDir: manifest.sourceUserDataDir,
|
|
227
|
+
chromeProfileDirectory: manifest.sourceProfileDirectory,
|
|
228
|
+
outputDir,
|
|
229
|
+
overwrite: true,
|
|
230
|
+
browserExecutablePath: options.browserExecutablePath ?? manifest.browserExecutablePath ?? void 0
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// src/services/browser-agent/local-browser-agent-service.ts
|
|
235
|
+
var import_node_crypto = require("crypto");
|
|
236
|
+
var import_node_fs2 = require("fs");
|
|
237
|
+
var import_node_path3 = require("path");
|
|
238
|
+
var import_playwright = require("playwright");
|
|
239
|
+
function directProfileDir() {
|
|
240
|
+
return process.env.MCP_SCRAPER_BROWSER_PROFILE_DIR?.trim() || void 0;
|
|
241
|
+
}
|
|
242
|
+
function configuredProfileName(profile) {
|
|
243
|
+
return profile?.trim() || process.env.MCP_SCRAPER_BROWSER_PROFILE?.trim() || (directProfileDir() ? (0, import_node_path3.basename)(directProfileDir()) : "default");
|
|
244
|
+
}
|
|
245
|
+
function launchExecutablePath(profile) {
|
|
246
|
+
const configured = process.env.MCP_SCRAPER_BROWSER_EXECUTABLE?.trim() || profile.browserExecutablePath || defaultChromeExecutablePath();
|
|
247
|
+
return configured && (0, import_node_fs2.existsSync)(configured) ? configured : void 0;
|
|
248
|
+
}
|
|
249
|
+
function ignoreDefaultArgsFor(executablePath) {
|
|
250
|
+
if (!executablePath) return void 0;
|
|
251
|
+
if (process.platform !== "darwin") return void 0;
|
|
252
|
+
if (!/Google Chrome\.app\/Contents\/MacOS\/Google Chrome$/.test(executablePath)) return void 0;
|
|
253
|
+
return ["--password-store=basic", "--use-mock-keychain"];
|
|
254
|
+
}
|
|
255
|
+
function normalizeKeyCombo(value) {
|
|
256
|
+
return value.split("+").map((part) => {
|
|
257
|
+
const trimmed = part.trim();
|
|
258
|
+
const lower = trimmed.toLowerCase();
|
|
259
|
+
if (lower === "ctrl" || lower === "control") return "Control";
|
|
260
|
+
if (lower === "cmd" || lower === "command" || lower === "meta") return "Meta";
|
|
261
|
+
if (lower === "option" || lower === "alt") return "Alt";
|
|
262
|
+
if (lower === "return") return "Enter";
|
|
263
|
+
if (lower === "esc") return "Escape";
|
|
264
|
+
if (trimmed.length === 1) return trimmed.toUpperCase();
|
|
265
|
+
return trimmed;
|
|
266
|
+
}).join("+");
|
|
267
|
+
}
|
|
268
|
+
function sessionSummary(session) {
|
|
269
|
+
return {
|
|
270
|
+
session_id: session.sessionId,
|
|
271
|
+
status: session.status,
|
|
272
|
+
label: session.label,
|
|
273
|
+
profile: session.profileName,
|
|
274
|
+
user_data_dir: session.userDataDir,
|
|
275
|
+
created_at: session.createdAt,
|
|
276
|
+
last_action_at: session.lastActionAt,
|
|
277
|
+
closed_at: session.closedAt,
|
|
278
|
+
watch_url: `local://browser-session/${session.sessionId}`,
|
|
279
|
+
url: session.status === "open" ? session.page.url() : null
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
var LocalBrowserAgentService = class {
|
|
283
|
+
contexts = /* @__PURE__ */ new Map();
|
|
284
|
+
sessions = /* @__PURE__ */ new Map();
|
|
285
|
+
async open(input) {
|
|
286
|
+
const profile = await this.resolveProfile(input.profile);
|
|
287
|
+
const contextState = await this.ensureContext(profile);
|
|
288
|
+
const pages = contextState.context.pages();
|
|
289
|
+
const page = pages.length === 1 && !this.hasOpenSessionForContext(contextState.key) ? pages[0] : await contextState.context.newPage();
|
|
290
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
291
|
+
const sessionId = `lbs_${(0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 20)}`;
|
|
292
|
+
const session = {
|
|
293
|
+
sessionId,
|
|
294
|
+
label: input.label ?? null,
|
|
295
|
+
page,
|
|
296
|
+
contextKey: contextState.key,
|
|
297
|
+
profileName: profile.name,
|
|
298
|
+
userDataDir: profile.userDataDir,
|
|
299
|
+
createdAt: now,
|
|
300
|
+
lastActionAt: now,
|
|
301
|
+
closedAt: null,
|
|
302
|
+
status: "open"
|
|
303
|
+
};
|
|
304
|
+
const timeoutSeconds = input.timeout_seconds ?? 600;
|
|
305
|
+
if (timeoutSeconds > 0) {
|
|
306
|
+
session.timeout = setTimeout(() => {
|
|
307
|
+
void this.close(sessionId);
|
|
308
|
+
}, timeoutSeconds * 1e3);
|
|
309
|
+
session.timeout.unref();
|
|
310
|
+
}
|
|
311
|
+
this.sessions.set(sessionId, session);
|
|
312
|
+
if (input.url) await this.goto(sessionId, input.url);
|
|
313
|
+
return {
|
|
314
|
+
session_id: sessionId,
|
|
315
|
+
watch_url: `local://browser-session/${sessionId}`,
|
|
316
|
+
live_view_url: null,
|
|
317
|
+
profile: profile.name,
|
|
318
|
+
user_data_dir: profile.userDataDir,
|
|
319
|
+
url: input.url ?? null,
|
|
320
|
+
mode: "local"
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
async screenshot(sessionId) {
|
|
324
|
+
const session = this.requireOpenSession(sessionId);
|
|
325
|
+
await this.waitForPage(session.page);
|
|
326
|
+
const image = await session.page.screenshot({ type: "png", fullPage: false });
|
|
327
|
+
const page = await this.read(sessionId);
|
|
328
|
+
return {
|
|
329
|
+
...page,
|
|
330
|
+
image_base64: image.toString("base64"),
|
|
331
|
+
mime_type: "image/png"
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
async read(sessionId) {
|
|
335
|
+
const session = this.requireOpenSession(sessionId);
|
|
336
|
+
await this.waitForPage(session.page);
|
|
337
|
+
this.touch(session);
|
|
338
|
+
const title = await session.page.title().catch(() => "");
|
|
339
|
+
const snapshot = await session.page.evaluate(() => {
|
|
340
|
+
const cssEscape = (value) => {
|
|
341
|
+
const escapeFn = globalThis.CSS?.escape;
|
|
342
|
+
return escapeFn ? escapeFn(value) : value.replace(/["\\#.:>+~[\]()]/g, "\\$&");
|
|
343
|
+
};
|
|
344
|
+
const textFor = (element) => {
|
|
345
|
+
const html = element;
|
|
346
|
+
return (element.getAttribute("aria-label") || element.getAttribute("title") || html.placeholder || html.value || html.innerText || element.textContent || "").replace(/\s+/g, " ").trim();
|
|
347
|
+
};
|
|
348
|
+
const selectorFor = (element) => {
|
|
349
|
+
const id = element.id;
|
|
350
|
+
if (id) return `#${cssEscape(id)}`;
|
|
351
|
+
const name = element.getAttribute("name");
|
|
352
|
+
const tag = element.tagName.toLowerCase();
|
|
353
|
+
if (name) return `${tag}[name="${name.replace(/"/g, '\\"')}"]`;
|
|
354
|
+
const parts = [];
|
|
355
|
+
let current = element;
|
|
356
|
+
while (current && current !== document.body && parts.length < 4) {
|
|
357
|
+
const currentTag = current.tagName.toLowerCase();
|
|
358
|
+
const parent = current.parentElement;
|
|
359
|
+
if (!parent) {
|
|
360
|
+
parts.unshift(currentTag);
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
const children = Array.from(parent.children);
|
|
364
|
+
const siblings = children.filter((child) => child.tagName === current.tagName);
|
|
365
|
+
const index = siblings.indexOf(current) + 1;
|
|
366
|
+
parts.unshift(siblings.length > 1 ? `${currentTag}:nth-of-type(${index})` : currentTag);
|
|
367
|
+
current = parent;
|
|
368
|
+
}
|
|
369
|
+
return parts.join(" > ") || tag;
|
|
370
|
+
};
|
|
371
|
+
const candidates = Array.from(document.querySelectorAll(
|
|
372
|
+
'a, button, input, textarea, select, summary, [role], [contenteditable="true"], [tabindex]:not([tabindex="-1"])'
|
|
373
|
+
));
|
|
374
|
+
const elements = candidates.map((element, index) => {
|
|
375
|
+
const rect = element.getBoundingClientRect();
|
|
376
|
+
const style = window.getComputedStyle(element);
|
|
377
|
+
const visible = rect.width > 0 && rect.height > 0 && style.display !== "none" && style.visibility !== "hidden" && Number(style.opacity || "1") > 0;
|
|
378
|
+
if (!visible) return null;
|
|
379
|
+
const label = textFor(element);
|
|
380
|
+
return {
|
|
381
|
+
index,
|
|
382
|
+
tag: element.tagName.toLowerCase(),
|
|
383
|
+
role: element.getAttribute("role") || null,
|
|
384
|
+
text: label.slice(0, 180),
|
|
385
|
+
selector: selectorFor(element),
|
|
386
|
+
x: Math.round(rect.left + rect.width / 2),
|
|
387
|
+
y: Math.round(rect.top + rect.height / 2),
|
|
388
|
+
left: Math.round(rect.left),
|
|
389
|
+
top: Math.round(rect.top),
|
|
390
|
+
width: Math.round(rect.width),
|
|
391
|
+
height: Math.round(rect.height)
|
|
392
|
+
};
|
|
393
|
+
}).filter(Boolean).slice(0, 120);
|
|
394
|
+
return {
|
|
395
|
+
text: (document.body?.innerText || "").replace(/\n{3,}/g, "\n\n").slice(0, 3e4),
|
|
396
|
+
elements,
|
|
397
|
+
viewport: {
|
|
398
|
+
width: window.innerWidth,
|
|
399
|
+
height: window.innerHeight,
|
|
400
|
+
devicePixelRatio: window.devicePixelRatio,
|
|
401
|
+
scrollX: window.scrollX,
|
|
402
|
+
scrollY: window.scrollY
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
});
|
|
406
|
+
return {
|
|
407
|
+
url: session.page.url(),
|
|
408
|
+
title,
|
|
409
|
+
text: snapshot.text,
|
|
410
|
+
elements: snapshot.elements,
|
|
411
|
+
viewport: snapshot.viewport
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
async locate(sessionId, targets) {
|
|
415
|
+
const session = this.requireOpenSession(sessionId);
|
|
416
|
+
this.touch(session);
|
|
417
|
+
const located = [];
|
|
418
|
+
for (const target of targets) {
|
|
419
|
+
try {
|
|
420
|
+
const locator = target.selector ? session.page.locator(target.selector) : session.page.getByText(target.text ?? "", { exact: target.match === "exact" });
|
|
421
|
+
const selected = locator.nth(target.index ?? 0);
|
|
422
|
+
await selected.waitFor({ state: "visible", timeout: 3e3 }).catch(() => void 0);
|
|
423
|
+
const box = await selected.boundingBox();
|
|
424
|
+
const text = await selected.textContent({ timeout: 1e3 }).catch(() => null);
|
|
425
|
+
located.push({
|
|
426
|
+
input: target,
|
|
427
|
+
found: Boolean(box),
|
|
428
|
+
element: box ? {
|
|
429
|
+
left: Math.round(box.x),
|
|
430
|
+
top: Math.round(box.y),
|
|
431
|
+
width: Math.round(box.width),
|
|
432
|
+
height: Math.round(box.height),
|
|
433
|
+
x: Math.round(box.x + box.width / 2),
|
|
434
|
+
y: Math.round(box.y + box.height / 2),
|
|
435
|
+
text: (text ?? "").replace(/\s+/g, " ").trim().slice(0, 220)
|
|
436
|
+
} : null
|
|
437
|
+
});
|
|
438
|
+
} catch (err) {
|
|
439
|
+
located.push({
|
|
440
|
+
input: target,
|
|
441
|
+
found: false,
|
|
442
|
+
element: null,
|
|
443
|
+
error: err instanceof Error ? err.message : String(err)
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
const read = await this.read(sessionId);
|
|
448
|
+
return {
|
|
449
|
+
url: read.url,
|
|
450
|
+
title: read.title,
|
|
451
|
+
viewport: read.viewport,
|
|
452
|
+
replay: null,
|
|
453
|
+
targets: located
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
async goto(sessionId, url) {
|
|
457
|
+
const session = this.requireOpenSession(sessionId);
|
|
458
|
+
this.touch(session);
|
|
459
|
+
const response = await session.page.goto(url, { waitUntil: "domcontentloaded", timeout: 45e3 }).catch((err) => {
|
|
460
|
+
throw new Error(err instanceof Error ? err.message : String(err));
|
|
461
|
+
});
|
|
462
|
+
await this.waitForPage(session.page);
|
|
463
|
+
return {
|
|
464
|
+
ok: true,
|
|
465
|
+
url: session.page.url(),
|
|
466
|
+
status: response?.status() ?? null,
|
|
467
|
+
mode: "local"
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
async click(sessionId, input) {
|
|
471
|
+
const session = this.requireOpenSession(sessionId);
|
|
472
|
+
this.touch(session);
|
|
473
|
+
await session.page.mouse.click(input.x, input.y, {
|
|
474
|
+
button: input.button ?? "left",
|
|
475
|
+
clickCount: input.num_clicks ?? 1
|
|
476
|
+
});
|
|
477
|
+
await this.waitForPage(session.page);
|
|
478
|
+
return { ok: true, mode: "local", url: session.page.url() };
|
|
479
|
+
}
|
|
480
|
+
async type(sessionId, input) {
|
|
481
|
+
const session = this.requireOpenSession(sessionId);
|
|
482
|
+
this.touch(session);
|
|
483
|
+
await session.page.keyboard.type(input.text, { delay: input.delay });
|
|
484
|
+
return { ok: true, mode: "local", url: session.page.url() };
|
|
485
|
+
}
|
|
486
|
+
async scroll(sessionId, input) {
|
|
487
|
+
const session = this.requireOpenSession(sessionId);
|
|
488
|
+
this.touch(session);
|
|
489
|
+
if (typeof input.x === "number" && typeof input.y === "number") {
|
|
490
|
+
await session.page.mouse.move(input.x, input.y);
|
|
491
|
+
}
|
|
492
|
+
await session.page.mouse.wheel((input.delta_x ?? 0) * 120, (input.delta_y ?? 5) * 120);
|
|
493
|
+
await session.page.waitForTimeout(150).catch(() => void 0);
|
|
494
|
+
return { ok: true, mode: "local", url: session.page.url() };
|
|
495
|
+
}
|
|
496
|
+
async press(sessionId, input) {
|
|
497
|
+
const session = this.requireOpenSession(sessionId);
|
|
498
|
+
this.touch(session);
|
|
499
|
+
for (const key of input.keys) {
|
|
500
|
+
await session.page.keyboard.press(normalizeKeyCombo(key));
|
|
501
|
+
}
|
|
502
|
+
await this.waitForPage(session.page);
|
|
503
|
+
return { ok: true, mode: "local", url: session.page.url() };
|
|
504
|
+
}
|
|
505
|
+
async close(sessionId) {
|
|
506
|
+
const session = this.sessions.get(sessionId);
|
|
507
|
+
if (!session || session.status === "closed") {
|
|
508
|
+
return { ok: true, closed: true, already_closed: true, mode: "local" };
|
|
509
|
+
}
|
|
510
|
+
if (session.timeout) clearTimeout(session.timeout);
|
|
511
|
+
session.status = "closed";
|
|
512
|
+
session.closedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
513
|
+
await session.page.close().catch(() => void 0);
|
|
514
|
+
await this.closeContextIfUnused(session.contextKey);
|
|
515
|
+
return { ok: true, closed: true, mode: "local" };
|
|
516
|
+
}
|
|
517
|
+
list(includeClosed = false) {
|
|
518
|
+
return Array.from(this.sessions.values()).filter((session) => includeClosed || session.status === "open").map(sessionSummary);
|
|
519
|
+
}
|
|
520
|
+
async resolveProfile(profile) {
|
|
521
|
+
const directDir = directProfileDir();
|
|
522
|
+
const requestedName = configuredProfileName(profile);
|
|
523
|
+
if (directDir && (!profile || requestedName === process.env.MCP_SCRAPER_BROWSER_PROFILE?.trim())) {
|
|
524
|
+
return {
|
|
525
|
+
name: requestedName,
|
|
526
|
+
userDataDir: directDir,
|
|
527
|
+
browserExecutablePath: process.env.MCP_SCRAPER_BROWSER_EXECUTABLE?.trim() || defaultChromeExecutablePath() || null,
|
|
528
|
+
manifest: null
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
const manifest = await loadImportedBrowserProfile(requestedName);
|
|
532
|
+
return {
|
|
533
|
+
name: manifest.name,
|
|
534
|
+
userDataDir: manifest.userDataDir,
|
|
535
|
+
browserExecutablePath: manifest.browserExecutablePath,
|
|
536
|
+
manifest
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
async ensureContext(profile) {
|
|
540
|
+
const existing = this.contexts.get(profile.userDataDir);
|
|
541
|
+
if (existing) return existing;
|
|
542
|
+
const executablePath = launchExecutablePath(profile);
|
|
543
|
+
const context = await import_playwright.chromium.launchPersistentContext(profile.userDataDir, {
|
|
544
|
+
headless: false,
|
|
545
|
+
viewport: { width: 1440, height: 900 },
|
|
546
|
+
acceptDownloads: true,
|
|
547
|
+
...executablePath ? { executablePath } : {},
|
|
548
|
+
...ignoreDefaultArgsFor(executablePath) ? { ignoreDefaultArgs: ignoreDefaultArgsFor(executablePath) } : {}
|
|
549
|
+
});
|
|
550
|
+
const state = { key: profile.userDataDir, context, profile };
|
|
551
|
+
context.on("close", () => {
|
|
552
|
+
this.contexts.delete(profile.userDataDir);
|
|
553
|
+
const closedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
554
|
+
for (const session of this.sessions.values()) {
|
|
555
|
+
if (session.contextKey === profile.userDataDir && session.status === "open") {
|
|
556
|
+
session.status = "closed";
|
|
557
|
+
session.closedAt = closedAt;
|
|
558
|
+
if (session.timeout) clearTimeout(session.timeout);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
this.contexts.set(profile.userDataDir, state);
|
|
563
|
+
return state;
|
|
564
|
+
}
|
|
565
|
+
requireOpenSession(sessionId) {
|
|
566
|
+
const session = this.sessions.get(sessionId);
|
|
567
|
+
if (!session || session.status !== "open") throw new Error(`Local browser session not found or closed: ${sessionId}`);
|
|
568
|
+
return session;
|
|
569
|
+
}
|
|
570
|
+
touch(session) {
|
|
571
|
+
session.lastActionAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
572
|
+
}
|
|
573
|
+
hasOpenSessionForContext(contextKey) {
|
|
574
|
+
return Array.from(this.sessions.values()).some((session) => session.contextKey === contextKey && session.status === "open");
|
|
575
|
+
}
|
|
576
|
+
async closeContextIfUnused(contextKey) {
|
|
577
|
+
if (this.hasOpenSessionForContext(contextKey)) return;
|
|
578
|
+
const state = this.contexts.get(contextKey);
|
|
579
|
+
if (!state) return;
|
|
580
|
+
this.contexts.delete(contextKey);
|
|
581
|
+
await state.context.close().catch(() => void 0);
|
|
582
|
+
}
|
|
583
|
+
async waitForPage(page) {
|
|
584
|
+
await page.waitForLoadState("domcontentloaded", { timeout: 5e3 }).catch(() => void 0);
|
|
585
|
+
await page.waitForTimeout(200).catch(() => void 0);
|
|
586
|
+
}
|
|
587
|
+
};
|
|
15
588
|
|
|
16
589
|
// src/version.ts
|
|
17
|
-
var PACKAGE_VERSION = "0.2.
|
|
590
|
+
var PACKAGE_VERSION = "0.2.20";
|
|
18
591
|
|
|
19
592
|
// src/mcp/browser-agent-tool-schemas.ts
|
|
20
593
|
var import_zod = require("zod");
|
|
594
|
+
var NullableString = import_zod.z.string().nullable();
|
|
595
|
+
var BrowserRawObject = import_zod.z.record(import_zod.z.unknown());
|
|
596
|
+
var BrowserElementOutput = import_zod.z.record(import_zod.z.unknown());
|
|
597
|
+
var BrowserBaseOutput = {
|
|
598
|
+
ok: import_zod.z.boolean().describe("Whether the browser-agent action succeeded."),
|
|
599
|
+
tool: import_zod.z.string().describe("Browser Agent MCP tool that produced this response."),
|
|
600
|
+
session_id: NullableString.describe("Browser session id when the response is scoped to a session.")
|
|
601
|
+
};
|
|
602
|
+
var BrowserReplayBaseOutput = {
|
|
603
|
+
...BrowserBaseOutput,
|
|
604
|
+
replay_id: NullableString.describe("Replay id when the response is scoped to a replay.")
|
|
605
|
+
};
|
|
21
606
|
var BrowserOpenInputSchema = {
|
|
22
607
|
label: import_zod.z.string().optional().describe("Optional human label for this session, shown in the watch console."),
|
|
23
608
|
url: import_zod.z.string().url().optional().describe("Optional URL to navigate to immediately after opening."),
|
|
24
609
|
profile: import_zod.z.string().optional().describe("Optional saved profile name to load a logged-in session for a site."),
|
|
610
|
+
save_profile_changes: import_zod.z.boolean().optional().describe("Persist cookies and local storage back to the named profile when the session is closed. Use this for profile setup or intentional auth refreshes; avoid parallel sessions writing to the same profile."),
|
|
25
611
|
timeout_seconds: import_zod.z.number().int().min(60).max(259200).optional().describe("How long the session may live before auto-termination. Defaults to 600. The browser idles into a zero-cost standby between actions, so a longer timeout is cheap.")
|
|
26
612
|
};
|
|
613
|
+
var BrowserProfileListInputSchema = {
|
|
614
|
+
email: import_zod.z.string().optional().describe("Optional Chrome account email or displayed profile name to filter for, for example seo@example.com."),
|
|
615
|
+
user_data_dir: import_zod.z.string().optional().describe("Optional Chrome user data directory. Defaults to the current OS Chrome profile root.")
|
|
616
|
+
};
|
|
617
|
+
var BrowserProfileOnboardInputSchema = {
|
|
618
|
+
email: import_zod.z.string().optional().describe("Optional local Chrome account email to find and convert into a suggested Kernel profile name."),
|
|
619
|
+
profile: import_zod.z.string().optional().describe("Optional Kernel browser profile name to create/load. If omitted, email is used to derive one."),
|
|
620
|
+
user_data_dir: import_zod.z.string().optional().describe("Optional Chrome user data directory for email lookup. Defaults to the current OS Chrome profile root."),
|
|
621
|
+
url: import_zod.z.string().url().optional().describe("Setup URL to open after creating the browser. Defaults to https://accounts.google.com/."),
|
|
622
|
+
label: import_zod.z.string().optional().describe("Optional human label for this setup session."),
|
|
623
|
+
timeout_seconds: import_zod.z.number().int().min(60).max(259200).optional().describe("How long the setup session may live before auto-termination. Defaults to 600.")
|
|
624
|
+
};
|
|
625
|
+
var BrowserProfileImportInputSchema = {
|
|
626
|
+
email: import_zod.z.string().optional().describe("Optional Chrome account email or displayed profile name to select from Chrome Local State."),
|
|
627
|
+
name: import_zod.z.string().optional().describe("Name for the managed MCP Scraper browser profile. Defaults to a slug derived from the selected Chrome account."),
|
|
628
|
+
chrome_profile_directory: import_zod.z.string().optional().describe("Optional Chrome profile directory to import, for example Default or Profile 1."),
|
|
629
|
+
user_data_dir: import_zod.z.string().optional().describe("Optional source Chrome user data directory. Defaults to the current OS Chrome profile root."),
|
|
630
|
+
output_dir: import_zod.z.string().optional().describe("Optional managed profile base directory. Defaults to ~/.mcp-scraper/browser-profiles."),
|
|
631
|
+
browser_executable_path: import_zod.z.string().optional().describe("Optional Chrome executable path for local browser launches. Defaults to the installed Google Chrome path when found."),
|
|
632
|
+
overwrite: import_zod.z.boolean().default(false).describe("Overwrite an existing managed profile clone with the same name.")
|
|
633
|
+
};
|
|
634
|
+
var BrowserProfileSyncInputSchema = {
|
|
635
|
+
name: import_zod.z.string().describe("Managed MCP Scraper browser profile name to refresh from its recorded source Chrome profile."),
|
|
636
|
+
output_dir: import_zod.z.string().optional().describe("Optional managed profile base directory. Defaults to ~/.mcp-scraper/browser-profiles."),
|
|
637
|
+
browser_executable_path: import_zod.z.string().optional().describe("Optional Chrome executable path to store in the refreshed profile manifest.")
|
|
638
|
+
};
|
|
27
639
|
var BrowserSessionInputSchema = {
|
|
28
|
-
session_id: import_zod.z.string().describe("The session id returned by browser_open.")
|
|
640
|
+
session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself.")
|
|
29
641
|
};
|
|
30
642
|
var BrowserLocateTargetSchema = import_zod.z.object({
|
|
31
643
|
name: import_zod.z.string().optional().describe("Optional label for this target, echoed in the result."),
|
|
@@ -37,43 +649,43 @@ var BrowserLocateTargetSchema = import_zod.z.object({
|
|
|
37
649
|
message: "target requires selector or text"
|
|
38
650
|
});
|
|
39
651
|
var BrowserLocateInputSchema = {
|
|
40
|
-
session_id: import_zod.z.string().describe("The session id returned by browser_open."),
|
|
652
|
+
session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
41
653
|
targets: import_zod.z.array(BrowserLocateTargetSchema).min(1).max(20).describe("DOM targets to locate in the current viewport. Use selectors for exact elements, or text for visible text ranges.")
|
|
42
654
|
};
|
|
43
655
|
var BrowserGotoInputSchema = {
|
|
44
|
-
session_id: import_zod.z.string().describe("The session id returned by browser_open."),
|
|
656
|
+
session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
45
657
|
url: import_zod.z.string().url().describe("URL to navigate the browser to.")
|
|
46
658
|
};
|
|
47
659
|
var BrowserClickInputSchema = {
|
|
48
|
-
session_id: import_zod.z.string().describe("The session id returned by browser_open."),
|
|
49
|
-
x: import_zod.z.number().describe("X coordinate to click, in screenshot pixels. Use the
|
|
50
|
-
y: import_zod.z.number().describe("Y coordinate to click, in screenshot pixels."),
|
|
660
|
+
session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
661
|
+
x: import_zod.z.number().describe("X coordinate to click, in screenshot pixels. Use only coordinates from the latest browser_screenshot, browser_read, or browser_locate result; do not guess."),
|
|
662
|
+
y: import_zod.z.number().describe("Y coordinate to click, in screenshot pixels. Use only coordinates from the latest browser_screenshot, browser_read, or browser_locate result; do not guess."),
|
|
51
663
|
button: import_zod.z.enum(["left", "right", "middle"]).default("left").describe("Mouse button."),
|
|
52
664
|
num_clicks: import_zod.z.number().int().min(1).max(3).optional().describe("Number of clicks, e.g. 2 for double-click.")
|
|
53
665
|
};
|
|
54
666
|
var BrowserTypeInputSchema = {
|
|
55
|
-
session_id: import_zod.z.string().describe("The session id returned by browser_open."),
|
|
667
|
+
session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
56
668
|
text: import_zod.z.string().describe("Text to type at the current focus. Click a field first to focus it."),
|
|
57
669
|
delay: import_zod.z.number().int().min(0).max(500).optional().describe("Optional per-keystroke delay in ms for human-like typing.")
|
|
58
670
|
};
|
|
59
671
|
var BrowserScrollInputSchema = {
|
|
60
|
-
session_id: import_zod.z.string().describe("The session id returned by browser_open."),
|
|
672
|
+
session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
61
673
|
delta_y: import_zod.z.number().default(5).describe("Vertical scroll in wheel units. Positive scrolls down, negative up."),
|
|
62
674
|
delta_x: import_zod.z.number().default(0).describe("Horizontal scroll in wheel units."),
|
|
63
675
|
x: import_zod.z.number().optional().describe("X position to scroll at. Defaults to screen center."),
|
|
64
676
|
y: import_zod.z.number().optional().describe("Y position to scroll at. Defaults to screen center.")
|
|
65
677
|
};
|
|
66
678
|
var BrowserPressInputSchema = {
|
|
67
|
-
session_id: import_zod.z.string().describe("The session id returned by browser_open."),
|
|
679
|
+
session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
68
680
|
keys: import_zod.z.array(import_zod.z.string()).min(1).describe('Keys or combinations to press, e.g. ["Return"], ["Ctrl+a"], ["Ctrl+Shift+Tab"].')
|
|
69
681
|
};
|
|
70
682
|
var BrowserReplayStopInputSchema = {
|
|
71
|
-
session_id: import_zod.z.string().describe("The session id returned by browser_open."),
|
|
72
|
-
replay_id: import_zod.z.string().describe("The replay id returned by browser_replay_start.")
|
|
683
|
+
session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
684
|
+
replay_id: import_zod.z.string().describe("The replay id returned by browser_replay_start or browser_list_replays. Use only a returned replay_id; do not construct one yourself.")
|
|
73
685
|
};
|
|
74
686
|
var BrowserReplayDownloadInputSchema = {
|
|
75
|
-
session_id: import_zod.z.string().describe("The session id returned by browser_open."),
|
|
76
|
-
replay_id: import_zod.z.string().describe("The replay id returned by browser_replay_start or browser_list_replays."),
|
|
687
|
+
session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
688
|
+
replay_id: import_zod.z.string().describe("The replay id returned by browser_replay_start or browser_list_replays. Use only a returned replay_id; do not construct one yourself."),
|
|
77
689
|
filename: import_zod.z.string().optional().describe("Optional local MP4 filename. Defaults to a timestamped replay filename.")
|
|
78
690
|
};
|
|
79
691
|
var BrowserReplayAnnotationSchema = import_zod.z.object({
|
|
@@ -95,7 +707,7 @@ var BrowserReplayAnnotationSchema = import_zod.z.object({
|
|
|
95
707
|
thickness: import_zod.z.number().min(1).max(24).optional().describe("Stroke thickness in pixels. Defaults to 5.")
|
|
96
708
|
});
|
|
97
709
|
var BrowserReplayMarkInputSchema = {
|
|
98
|
-
session_id: import_zod.z.string().describe("The session id returned by browser_open. A replay must already be recording."),
|
|
710
|
+
session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself. A replay must already be recording."),
|
|
99
711
|
target: BrowserLocateTargetSchema.describe("The exact DOM element or text range to mark in the current viewport."),
|
|
100
712
|
type: import_zod.z.enum(["box", "circle", "underline", "arrow"]).default("box").describe("Annotation style to generate. Labels are included on the returned annotation when label is provided."),
|
|
101
713
|
label: import_zod.z.string().max(120).optional().describe("Optional callout text to render near the target."),
|
|
@@ -106,8 +718,8 @@ var BrowserReplayMarkInputSchema = {
|
|
|
106
718
|
duration_seconds: import_zod.z.number().min(0.5).max(30).default(4).describe("How long the generated annotation should remain visible.")
|
|
107
719
|
};
|
|
108
720
|
var BrowserReplayAnnotateInputSchema = {
|
|
109
|
-
session_id: import_zod.z.string().describe("The session id returned by browser_open."),
|
|
110
|
-
replay_id: import_zod.z.string().describe("The replay id returned by browser_replay_start or browser_list_replays."),
|
|
721
|
+
session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
722
|
+
replay_id: import_zod.z.string().describe("The replay id returned by browser_replay_start or browser_list_replays. Use only a returned replay_id; do not construct one yourself."),
|
|
111
723
|
annotations: import_zod.z.array(BrowserReplayAnnotationSchema).min(1).max(50).describe("Timed overlay annotations to render on the replay. Prefer annotations returned by browser_replay_mark; otherwise use exact DOM bounds from browser_locate/browser_screenshot/browser_read."),
|
|
112
724
|
filename: import_zod.z.string().optional().describe("Optional output MP4 filename. Defaults to a timestamped annotated replay filename."),
|
|
113
725
|
source_width: import_zod.z.number().positive().optional().describe("Width of the screenshot coordinate space used for annotations. Defaults to the replay video width."),
|
|
@@ -118,12 +730,156 @@ var BrowserReplayAnnotateInputSchema = {
|
|
|
118
730
|
var BrowserListInputSchema = {
|
|
119
731
|
include_closed: import_zod.z.boolean().default(false).describe("Include closed sessions in the list.")
|
|
120
732
|
};
|
|
733
|
+
var BrowserOpenOutputSchema = {
|
|
734
|
+
ok: import_zod.z.boolean(),
|
|
735
|
+
tool: import_zod.z.literal("browser_open"),
|
|
736
|
+
session_id: import_zod.z.string().describe("Session id returned by browser_open. Use only this exact value in later browser_* calls; do not construct one yourself."),
|
|
737
|
+
watch_url: import_zod.z.string().describe("Human watch/takeover URL for this browser session."),
|
|
738
|
+
live_view_url: NullableString.describe("Raw live-view URL from the browser service when available."),
|
|
739
|
+
url: NullableString.describe("Initial URL requested by the caller, when provided."),
|
|
740
|
+
hint: import_zod.z.string(),
|
|
741
|
+
raw: BrowserRawObject.optional()
|
|
742
|
+
};
|
|
743
|
+
var BrowserProfileListOutputSchema = {
|
|
744
|
+
ok: import_zod.z.boolean(),
|
|
745
|
+
tool: import_zod.z.literal("browser_profile_list"),
|
|
746
|
+
session_id: import_zod.z.null(),
|
|
747
|
+
profiles: import_zod.z.array(BrowserRawObject),
|
|
748
|
+
count: import_zod.z.number().int().min(0),
|
|
749
|
+
hint: import_zod.z.string()
|
|
750
|
+
};
|
|
751
|
+
var BrowserProfileOnboardOutputSchema = {
|
|
752
|
+
ok: import_zod.z.boolean(),
|
|
753
|
+
tool: import_zod.z.literal("browser_profile_onboard"),
|
|
754
|
+
session_id: import_zod.z.string(),
|
|
755
|
+
watch_url: import_zod.z.string(),
|
|
756
|
+
live_view_url: NullableString,
|
|
757
|
+
profile: import_zod.z.string(),
|
|
758
|
+
setup_url: import_zod.z.string(),
|
|
759
|
+
chrome_profile: BrowserRawObject.nullable(),
|
|
760
|
+
next_steps: import_zod.z.array(import_zod.z.string()),
|
|
761
|
+
raw: BrowserRawObject.optional()
|
|
762
|
+
};
|
|
763
|
+
var BrowserProfileImportOutputSchema = {
|
|
764
|
+
ok: import_zod.z.boolean(),
|
|
765
|
+
tool: import_zod.z.literal("browser_profile_import"),
|
|
766
|
+
session_id: import_zod.z.null(),
|
|
767
|
+
profile: BrowserRawObject,
|
|
768
|
+
source_profile: BrowserRawObject,
|
|
769
|
+
next_steps: import_zod.z.array(import_zod.z.string()),
|
|
770
|
+
hint: import_zod.z.string()
|
|
771
|
+
};
|
|
772
|
+
var BrowserProfileSyncOutputSchema = {
|
|
773
|
+
ok: import_zod.z.boolean(),
|
|
774
|
+
tool: import_zod.z.literal("browser_profile_sync"),
|
|
775
|
+
session_id: import_zod.z.null(),
|
|
776
|
+
profile: BrowserRawObject,
|
|
777
|
+
source_profile: BrowserRawObject,
|
|
778
|
+
hint: import_zod.z.string()
|
|
779
|
+
};
|
|
780
|
+
var BrowserScreenshotOutputSchema = {
|
|
781
|
+
...BrowserBaseOutput,
|
|
782
|
+
tool: import_zod.z.literal("browser_screenshot"),
|
|
783
|
+
url: NullableString,
|
|
784
|
+
title: NullableString,
|
|
785
|
+
text: import_zod.z.string(),
|
|
786
|
+
elements: import_zod.z.array(BrowserElementOutput),
|
|
787
|
+
screenshot: import_zod.z.object({
|
|
788
|
+
mime_type: import_zod.z.string(),
|
|
789
|
+
inline: import_zod.z.boolean()
|
|
790
|
+
}).nullable()
|
|
791
|
+
};
|
|
792
|
+
var BrowserReadOutputSchema = {
|
|
793
|
+
...BrowserBaseOutput,
|
|
794
|
+
tool: import_zod.z.literal("browser_read"),
|
|
795
|
+
url: NullableString,
|
|
796
|
+
title: NullableString,
|
|
797
|
+
text: import_zod.z.string(),
|
|
798
|
+
elements: import_zod.z.array(BrowserElementOutput),
|
|
799
|
+
raw: BrowserRawObject.optional()
|
|
800
|
+
};
|
|
801
|
+
var BrowserLocateOutputSchema = {
|
|
802
|
+
...BrowserBaseOutput,
|
|
803
|
+
tool: import_zod.z.literal("browser_locate"),
|
|
804
|
+
url: NullableString,
|
|
805
|
+
title: NullableString,
|
|
806
|
+
viewport: BrowserRawObject.nullable(),
|
|
807
|
+
replay: BrowserRawObject.nullable(),
|
|
808
|
+
targets: import_zod.z.array(BrowserRawObject),
|
|
809
|
+
raw: BrowserRawObject.optional()
|
|
810
|
+
};
|
|
811
|
+
var BrowserActionOutputSchema = {
|
|
812
|
+
...BrowserBaseOutput,
|
|
813
|
+
result: BrowserRawObject.describe("Provider action result. Check ok and follow with browser_screenshot/browser_read when page state matters."),
|
|
814
|
+
nextRecommendedTool: import_zod.z.string().nullable()
|
|
815
|
+
};
|
|
816
|
+
var BrowserReplayStartOutputSchema = {
|
|
817
|
+
...BrowserReplayBaseOutput,
|
|
818
|
+
tool: import_zod.z.literal("browser_replay_start"),
|
|
819
|
+
view_url: NullableString,
|
|
820
|
+
download_url: NullableString,
|
|
821
|
+
raw: BrowserRawObject.optional()
|
|
822
|
+
};
|
|
823
|
+
var BrowserReplayStopOutputSchema = {
|
|
824
|
+
...BrowserReplayBaseOutput,
|
|
825
|
+
tool: import_zod.z.literal("browser_replay_stop"),
|
|
826
|
+
view_url: NullableString,
|
|
827
|
+
download_url: NullableString,
|
|
828
|
+
raw: BrowserRawObject.optional()
|
|
829
|
+
};
|
|
830
|
+
var BrowserListReplaysOutputSchema = {
|
|
831
|
+
...BrowserBaseOutput,
|
|
832
|
+
tool: import_zod.z.literal("browser_list_replays"),
|
|
833
|
+
replays: import_zod.z.array(BrowserRawObject),
|
|
834
|
+
count: import_zod.z.number().int().min(0)
|
|
835
|
+
};
|
|
836
|
+
var BrowserReplayDownloadOutputSchema = {
|
|
837
|
+
...BrowserReplayBaseOutput,
|
|
838
|
+
tool: import_zod.z.literal("browser_replay_download"),
|
|
839
|
+
file_path: NullableString,
|
|
840
|
+
bytes: import_zod.z.number().int().min(0).nullable(),
|
|
841
|
+
mime_type: NullableString,
|
|
842
|
+
download_url: NullableString
|
|
843
|
+
};
|
|
844
|
+
var BrowserReplayMarkOutputSchema = {
|
|
845
|
+
...BrowserReplayBaseOutput,
|
|
846
|
+
tool: import_zod.z.literal("browser_replay_mark"),
|
|
847
|
+
annotation: BrowserRawObject,
|
|
848
|
+
source_width: import_zod.z.number().nullable(),
|
|
849
|
+
source_height: import_zod.z.number().nullable(),
|
|
850
|
+
target: BrowserRawObject.nullable(),
|
|
851
|
+
hint: import_zod.z.string()
|
|
852
|
+
};
|
|
853
|
+
var BrowserReplayAnnotateOutputSchema = {
|
|
854
|
+
...BrowserReplayBaseOutput,
|
|
855
|
+
tool: import_zod.z.literal("browser_replay_annotate"),
|
|
856
|
+
source_file_path: NullableString,
|
|
857
|
+
annotated_file_path: NullableString,
|
|
858
|
+
bytes: import_zod.z.number().int().min(0).nullable(),
|
|
859
|
+
width: import_zod.z.number().int().min(0).nullable(),
|
|
860
|
+
height: import_zod.z.number().int().min(0).nullable(),
|
|
861
|
+
annotation_count: import_zod.z.number().int().min(0).nullable(),
|
|
862
|
+
mime_type: NullableString
|
|
863
|
+
};
|
|
864
|
+
var BrowserCloseOutputSchema = {
|
|
865
|
+
...BrowserBaseOutput,
|
|
866
|
+
tool: import_zod.z.literal("browser_close"),
|
|
867
|
+
closed: import_zod.z.boolean(),
|
|
868
|
+
raw: BrowserRawObject.optional()
|
|
869
|
+
};
|
|
870
|
+
var BrowserListSessionsOutputSchema = {
|
|
871
|
+
ok: import_zod.z.boolean(),
|
|
872
|
+
tool: import_zod.z.literal("browser_list_sessions"),
|
|
873
|
+
session_id: import_zod.z.null(),
|
|
874
|
+
sessions: import_zod.z.array(BrowserRawObject),
|
|
875
|
+
count: import_zod.z.number().int().min(0)
|
|
876
|
+
};
|
|
121
877
|
|
|
122
878
|
// src/mcp/replay-annotator.ts
|
|
123
879
|
var import_node_child_process = require("child_process");
|
|
124
|
-
var
|
|
125
|
-
var
|
|
126
|
-
var
|
|
880
|
+
var import_promises3 = require("fs/promises");
|
|
881
|
+
var import_node_os3 = require("os");
|
|
882
|
+
var import_node_path4 = require("path");
|
|
127
883
|
var import_node_util = require("util");
|
|
128
884
|
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
129
885
|
function finiteNumber(value) {
|
|
@@ -346,10 +1102,10 @@ function ffmpegFilterPath(path) {
|
|
|
346
1102
|
async function annotateReplayVideo(inputFilePath, outputFilePath, options) {
|
|
347
1103
|
if (!options.annotations.length) throw new Error("annotations must include at least one item");
|
|
348
1104
|
const size = await videoSize(inputFilePath);
|
|
349
|
-
const tmp = await (0,
|
|
350
|
-
const assPath = (0,
|
|
1105
|
+
const tmp = await (0, import_promises3.mkdtemp)((0, import_node_path4.join)((0, import_node_os3.tmpdir)(), "mcp-scraper-ass-"));
|
|
1106
|
+
const assPath = (0, import_node_path4.join)(tmp, "annotations.ass");
|
|
351
1107
|
try {
|
|
352
|
-
await (0,
|
|
1108
|
+
await (0, import_promises3.writeFile)(assPath, buildAssSubtitle(options, size), "utf8");
|
|
353
1109
|
await execFileAsync("ffmpeg", [
|
|
354
1110
|
"-y",
|
|
355
1111
|
"-i",
|
|
@@ -366,7 +1122,7 @@ async function annotateReplayVideo(inputFilePath, outputFilePath, options) {
|
|
|
366
1122
|
"copy",
|
|
367
1123
|
outputFilePath
|
|
368
1124
|
], { maxBuffer: 1024 * 1024 * 20 });
|
|
369
|
-
const out = await (0,
|
|
1125
|
+
const out = await (0, import_promises3.stat)(outputFilePath);
|
|
370
1126
|
return {
|
|
371
1127
|
filePath: outputFilePath,
|
|
372
1128
|
bytes: out.size,
|
|
@@ -375,16 +1131,48 @@ async function annotateReplayVideo(inputFilePath, outputFilePath, options) {
|
|
|
375
1131
|
annotationCount: options.annotations.length
|
|
376
1132
|
};
|
|
377
1133
|
} finally {
|
|
378
|
-
await (0,
|
|
1134
|
+
await (0, import_promises3.rm)(tmp, { recursive: true, force: true });
|
|
379
1135
|
}
|
|
380
1136
|
}
|
|
381
1137
|
|
|
382
1138
|
// src/mcp/browser-agent-mcp-server.ts
|
|
383
|
-
function
|
|
384
|
-
return {
|
|
1139
|
+
function structuredResult(value, isError = false) {
|
|
1140
|
+
return {
|
|
1141
|
+
content: [{ type: "text", text: JSON.stringify(value) }],
|
|
1142
|
+
structuredContent: value,
|
|
1143
|
+
isError
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
function errorMessage(value) {
|
|
1147
|
+
if (value && typeof value === "object") {
|
|
1148
|
+
const data = value;
|
|
1149
|
+
if (typeof data.error === "string") return data.error;
|
|
1150
|
+
if (typeof data.message === "string") return data.message;
|
|
1151
|
+
}
|
|
1152
|
+
return typeof value === "string" ? value : "Browser Agent request failed";
|
|
1153
|
+
}
|
|
1154
|
+
function errorResult(tool, value, sessionId = null, replayId = null) {
|
|
1155
|
+
return structuredResult({
|
|
1156
|
+
ok: false,
|
|
1157
|
+
tool,
|
|
1158
|
+
session_id: sessionId,
|
|
1159
|
+
...replayId !== null ? { replay_id: replayId } : {},
|
|
1160
|
+
error: errorMessage(value),
|
|
1161
|
+
raw: value && typeof value === "object" ? value : { value }
|
|
1162
|
+
}, true);
|
|
1163
|
+
}
|
|
1164
|
+
function actionResult(tool, sessionId, ok, data, nextRecommendedTool = "browser_screenshot") {
|
|
1165
|
+
if (!ok) return errorResult(tool, data, sessionId);
|
|
1166
|
+
return structuredResult({
|
|
1167
|
+
ok: true,
|
|
1168
|
+
tool,
|
|
1169
|
+
session_id: sessionId,
|
|
1170
|
+
result: data && typeof data === "object" ? data : { value: data },
|
|
1171
|
+
nextRecommendedTool
|
|
1172
|
+
});
|
|
385
1173
|
}
|
|
386
1174
|
function outputBaseDir() {
|
|
387
|
-
return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0,
|
|
1175
|
+
return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path5.join)((0, import_node_os4.homedir)(), "Downloads", "mcp-scraper");
|
|
388
1176
|
}
|
|
389
1177
|
function safeFilePart(value) {
|
|
390
1178
|
return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 120) || "replay";
|
|
@@ -393,7 +1181,7 @@ function replayFilePath(sessionId, replayId, filename) {
|
|
|
393
1181
|
const requested = filename?.trim();
|
|
394
1182
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
395
1183
|
const name = requested ? safeFilePart(requested).replace(/\.mp4$/i, "") : `${stamp}-${safeFilePart(sessionId)}-${safeFilePart(replayId)}`;
|
|
396
|
-
return (0,
|
|
1184
|
+
return (0, import_node_path5.join)(outputBaseDir(), "browser-replays", `${name}.mp4`);
|
|
397
1185
|
}
|
|
398
1186
|
function annotatedReplayFilePath(sessionId, replayId, filename) {
|
|
399
1187
|
const requested = filename?.trim();
|
|
@@ -429,6 +1217,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
429
1217
|
const baseUrl2 = opts.baseUrl.replace(/\/$/, "");
|
|
430
1218
|
const consoleBase = (opts.consoleBaseUrl ?? opts.baseUrl).replace(/\/$/, "");
|
|
431
1219
|
const timeoutMs = opts.timeoutMs ?? 9e4;
|
|
1220
|
+
const localBrowser = new LocalBrowserAgentService();
|
|
432
1221
|
async function req(method, path, body) {
|
|
433
1222
|
try {
|
|
434
1223
|
const res = await fetch(`${baseUrl2}${path}`, {
|
|
@@ -457,8 +1246,8 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
457
1246
|
}
|
|
458
1247
|
const bytes = Buffer.from(await res.arrayBuffer());
|
|
459
1248
|
const filePath = replayFilePath(sessionId, replayId, filename);
|
|
460
|
-
(0,
|
|
461
|
-
(0,
|
|
1249
|
+
(0, import_node_fs3.mkdirSync)((0, import_node_path5.join)(outputBaseDir(), "browser-replays"), { recursive: true });
|
|
1250
|
+
(0, import_node_fs3.writeFileSync)(filePath, bytes);
|
|
462
1251
|
return {
|
|
463
1252
|
ok: true,
|
|
464
1253
|
data: {
|
|
@@ -480,30 +1269,211 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
480
1269
|
idempotentHint: false,
|
|
481
1270
|
openWorldHint: true
|
|
482
1271
|
});
|
|
1272
|
+
function selectChromeProfile(matches, email) {
|
|
1273
|
+
const wanted = email?.trim().toLowerCase();
|
|
1274
|
+
if (!wanted) return matches[0] ?? null;
|
|
1275
|
+
return matches.find((profile) => profile.email.toLowerCase() === wanted) ?? matches[0] ?? null;
|
|
1276
|
+
}
|
|
1277
|
+
function localReplayUnsupported(tool, sessionId = null, replayId = null) {
|
|
1278
|
+
return errorResult(tool, {
|
|
1279
|
+
error: "Local browser mode does not support replay recording yet. Use hosted mode for browser_replay_* tools.",
|
|
1280
|
+
mode: "local"
|
|
1281
|
+
}, sessionId, replayId);
|
|
1282
|
+
}
|
|
1283
|
+
server2.registerTool(
|
|
1284
|
+
"browser_profile_list",
|
|
1285
|
+
{
|
|
1286
|
+
title: "List Local Chrome Profiles",
|
|
1287
|
+
description: "List local Chrome profile metadata visible to this MCP server: account email, Chrome profile directory, local path, and a suggested managed browser profile name. This reads Chrome Local State only; it does not read cookies, passwords, browsing history, or copy local browser state. Use browser_profile_import when the user wants MCP Scraper to clone a local Chrome profile for local browser mode.",
|
|
1288
|
+
inputSchema: BrowserProfileListInputSchema,
|
|
1289
|
+
outputSchema: BrowserProfileListOutputSchema,
|
|
1290
|
+
annotations: annotations("List Local Chrome Profiles", true)
|
|
1291
|
+
},
|
|
1292
|
+
async (input) => {
|
|
1293
|
+
try {
|
|
1294
|
+
const profiles = await listLocalChromeProfiles(input.user_data_dir, input.email);
|
|
1295
|
+
return structuredResult({
|
|
1296
|
+
ok: true,
|
|
1297
|
+
tool: "browser_profile_list",
|
|
1298
|
+
session_id: null,
|
|
1299
|
+
profiles,
|
|
1300
|
+
count: profiles.length,
|
|
1301
|
+
hint: "Use browser_profile_import to clone one profile for local mode. Use browser_profile_onboard only for hosted Kernel profile setup."
|
|
1302
|
+
});
|
|
1303
|
+
} catch (err) {
|
|
1304
|
+
return errorResult("browser_profile_list", { error: err instanceof Error ? err.message : String(err) });
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
);
|
|
1308
|
+
server2.registerTool(
|
|
1309
|
+
"browser_profile_import",
|
|
1310
|
+
{
|
|
1311
|
+
title: "Import Local Chrome Profile",
|
|
1312
|
+
description: "Clone one local Chrome profile into MCP Scraper managed storage for local browser mode. This copies browser state files such as cookies, history, local storage, session storage, and password database metadata into ~/.mcp-scraper/browser-profiles by default; it skips cache and lock files and never prints cookie or password values. Use this when the user wants browser_open to run locally with their existing logged-in Chrome state.",
|
|
1313
|
+
inputSchema: BrowserProfileImportInputSchema,
|
|
1314
|
+
outputSchema: BrowserProfileImportOutputSchema,
|
|
1315
|
+
annotations: { title: "Import Local Chrome Profile", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
|
|
1316
|
+
},
|
|
1317
|
+
async (input) => {
|
|
1318
|
+
try {
|
|
1319
|
+
const result = await importChromeProfile({
|
|
1320
|
+
email: input.email,
|
|
1321
|
+
name: input.name,
|
|
1322
|
+
chromeProfileDirectory: input.chrome_profile_directory,
|
|
1323
|
+
sourceUserDataDir: input.user_data_dir,
|
|
1324
|
+
outputDir: input.output_dir,
|
|
1325
|
+
overwrite: input.overwrite,
|
|
1326
|
+
browserExecutablePath: input.browser_executable_path
|
|
1327
|
+
});
|
|
1328
|
+
return structuredResult({
|
|
1329
|
+
ok: true,
|
|
1330
|
+
tool: "browser_profile_import",
|
|
1331
|
+
session_id: null,
|
|
1332
|
+
profile: result.profile,
|
|
1333
|
+
source_profile: result.sourceProfile,
|
|
1334
|
+
next_steps: [
|
|
1335
|
+
`Set MCP_SCRAPER_BROWSER_MODE=local in the MCP server config.`,
|
|
1336
|
+
`Set MCP_SCRAPER_BROWSER_PROFILE=${result.profile.name}.`,
|
|
1337
|
+
"Restart the MCP client so it starts the local browser mode process.",
|
|
1338
|
+
"Call browser_open with the same MCP server; it will open a local Chrome window using the cloned profile."
|
|
1339
|
+
],
|
|
1340
|
+
hint: "The clone is local to this machine. Re-run browser_profile_sync after logging into new sites in normal Chrome."
|
|
1341
|
+
});
|
|
1342
|
+
} catch (err) {
|
|
1343
|
+
return errorResult("browser_profile_import", { error: err instanceof Error ? err.message : String(err) });
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
);
|
|
1347
|
+
server2.registerTool(
|
|
1348
|
+
"browser_profile_sync",
|
|
1349
|
+
{
|
|
1350
|
+
title: "Sync Local Chrome Profile",
|
|
1351
|
+
description: "Refresh an existing MCP Scraper managed local browser profile from the source Chrome profile recorded in its manifest. This overwrites the managed clone, copies browser state files again, skips cache and lock files, and never prints cookie or password values.",
|
|
1352
|
+
inputSchema: BrowserProfileSyncInputSchema,
|
|
1353
|
+
outputSchema: BrowserProfileSyncOutputSchema,
|
|
1354
|
+
annotations: { title: "Sync Local Chrome Profile", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
|
|
1355
|
+
},
|
|
1356
|
+
async (input) => {
|
|
1357
|
+
try {
|
|
1358
|
+
const result = await syncImportedChromeProfile(input.name, {
|
|
1359
|
+
outputDir: input.output_dir,
|
|
1360
|
+
browserExecutablePath: input.browser_executable_path
|
|
1361
|
+
});
|
|
1362
|
+
return structuredResult({
|
|
1363
|
+
ok: true,
|
|
1364
|
+
tool: "browser_profile_sync",
|
|
1365
|
+
session_id: null,
|
|
1366
|
+
profile: result.profile,
|
|
1367
|
+
source_profile: result.sourceProfile,
|
|
1368
|
+
hint: `Refreshed ${result.profile.name}. Restart local browser sessions that were using this profile.`
|
|
1369
|
+
});
|
|
1370
|
+
} catch (err) {
|
|
1371
|
+
return errorResult("browser_profile_sync", { error: err instanceof Error ? err.message : String(err) });
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
);
|
|
1375
|
+
server2.registerTool(
|
|
1376
|
+
"browser_profile_onboard",
|
|
1377
|
+
{
|
|
1378
|
+
title: "Onboard Hosted Browser Profile",
|
|
1379
|
+
description: "Create or load a saved hosted Kernel browser profile and open a live setup browser with profile saving enabled. Use this only for hosted browser mode. If email is provided, the tool reads local Chrome profile metadata only to suggest the Kernel profile name; it does not import local cookies. For local browser mode with real Chrome state, use browser_profile_import instead.",
|
|
1380
|
+
inputSchema: BrowserProfileOnboardInputSchema,
|
|
1381
|
+
outputSchema: BrowserProfileOnboardOutputSchema,
|
|
1382
|
+
annotations: annotations("Onboard Hosted Browser Profile")
|
|
1383
|
+
},
|
|
1384
|
+
async (input) => {
|
|
1385
|
+
let chromeProfile = null;
|
|
1386
|
+
if (input.email) {
|
|
1387
|
+
try {
|
|
1388
|
+
chromeProfile = selectChromeProfile(await listLocalChromeProfiles(input.user_data_dir, input.email), input.email);
|
|
1389
|
+
} catch (err) {
|
|
1390
|
+
return errorResult("browser_profile_onboard", { error: err instanceof Error ? err.message : String(err) });
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
const profile = input.profile?.trim() || chromeProfile?.recommendedKernelProfileName || browserServiceProfileName();
|
|
1394
|
+
if (!profile) {
|
|
1395
|
+
return errorResult("browser_profile_onboard", {
|
|
1396
|
+
error: "profile is required when no email match or BROWSER_AGENT_PROFILE_NAME is available"
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
const setupUrl = input.url ?? "https://accounts.google.com/";
|
|
1400
|
+
const open = await req("POST", "/agent/sessions", {
|
|
1401
|
+
label: input.label ?? `Profile setup: ${profile}`,
|
|
1402
|
+
profile,
|
|
1403
|
+
save_profile_changes: true,
|
|
1404
|
+
timeout_seconds: input.timeout_seconds
|
|
1405
|
+
});
|
|
1406
|
+
if (!open.ok) return errorResult("browser_profile_onboard", open.data);
|
|
1407
|
+
const session = open.data;
|
|
1408
|
+
const goto = await req("POST", `/agent/sessions/${session.session_id}/goto`, { url: setupUrl });
|
|
1409
|
+
if (!goto.ok) return errorResult("browser_profile_onboard", goto.data, session.session_id);
|
|
1410
|
+
return structuredResult({
|
|
1411
|
+
ok: true,
|
|
1412
|
+
tool: "browser_profile_onboard",
|
|
1413
|
+
session_id: session.session_id,
|
|
1414
|
+
watch_url: `${consoleBase}/console/${session.session_id}`,
|
|
1415
|
+
live_view_url: session.live_view_url ?? null,
|
|
1416
|
+
profile,
|
|
1417
|
+
setup_url: setupUrl,
|
|
1418
|
+
chrome_profile: chromeProfile,
|
|
1419
|
+
next_steps: [
|
|
1420
|
+
"Open the watch_url and complete the login manually.",
|
|
1421
|
+
"After login is complete, call browser_close with this session_id to persist cookies and local storage to the Kernel profile.",
|
|
1422
|
+
"Use BROWSER_AGENT_PROFILE_NAME or browser_open.profile with this profile name for future authenticated sessions."
|
|
1423
|
+
],
|
|
1424
|
+
raw: session
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
);
|
|
483
1428
|
server2.registerTool(
|
|
484
1429
|
"browser_open",
|
|
485
1430
|
{
|
|
486
1431
|
title: "Open Browser Session",
|
|
487
|
-
description: "Open a
|
|
1432
|
+
description: "Open a browser you can drive. By default this creates a hosted cloud browser with a watch_url; when MCP_SCRAPER_BROWSER_MODE=local it opens a local Google Chrome window against an imported MCP Scraper Chrome profile. Returns a session_id used by all other browser_* tools. In hosted mode, anti-bot stealth and automatic CAPTCHA/Cloudflare solving are on by default. Call browser_close when done.",
|
|
488
1433
|
inputSchema: BrowserOpenInputSchema,
|
|
1434
|
+
outputSchema: BrowserOpenOutputSchema,
|
|
489
1435
|
annotations: annotations("Open Browser Session")
|
|
490
1436
|
},
|
|
491
1437
|
async (input) => {
|
|
1438
|
+
if (localBrowserModeEnabled()) {
|
|
1439
|
+
try {
|
|
1440
|
+
const session2 = await localBrowser.open(input);
|
|
1441
|
+
return structuredResult({
|
|
1442
|
+
ok: true,
|
|
1443
|
+
tool: "browser_open",
|
|
1444
|
+
session_id: session2.session_id,
|
|
1445
|
+
watch_url: session2.watch_url,
|
|
1446
|
+
live_view_url: null,
|
|
1447
|
+
url: input.url ?? null,
|
|
1448
|
+
hint: "Local Chrome is open on this machine. Call browser_screenshot to see the page and click by x,y coordinates.",
|
|
1449
|
+
raw: session2
|
|
1450
|
+
});
|
|
1451
|
+
} catch (err) {
|
|
1452
|
+
return errorResult("browser_open", { error: err instanceof Error ? err.message : String(err), mode: "local" });
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
const profile = input.profile ?? browserServiceProfileName();
|
|
1456
|
+
const saveProfileChanges = input.save_profile_changes ?? browserServiceProfileSaveChanges();
|
|
492
1457
|
const open = await req("POST", "/agent/sessions", {
|
|
493
1458
|
label: input.label,
|
|
494
|
-
profile:
|
|
1459
|
+
...profile ? { profile } : {},
|
|
1460
|
+
...profile && typeof saveProfileChanges === "boolean" ? { save_profile_changes: saveProfileChanges } : {},
|
|
495
1461
|
timeout_seconds: input.timeout_seconds
|
|
496
1462
|
});
|
|
497
|
-
if (!open.ok) return
|
|
1463
|
+
if (!open.ok) return errorResult("browser_open", open.data);
|
|
498
1464
|
const session = open.data;
|
|
499
1465
|
if (input.url) {
|
|
500
1466
|
await req("POST", `/agent/sessions/${session.session_id}/goto`, { url: input.url });
|
|
501
1467
|
}
|
|
502
|
-
return
|
|
1468
|
+
return structuredResult({
|
|
1469
|
+
ok: true,
|
|
1470
|
+
tool: "browser_open",
|
|
503
1471
|
session_id: session.session_id,
|
|
504
1472
|
watch_url: `${consoleBase}/console/${session.session_id}`,
|
|
505
1473
|
live_view_url: session.live_view_url ?? null,
|
|
506
|
-
|
|
1474
|
+
url: input.url ?? null,
|
|
1475
|
+
hint: "Call browser_screenshot to see the page. Click by the x,y of an element from the snapshot.",
|
|
1476
|
+
raw: session
|
|
507
1477
|
});
|
|
508
1478
|
}
|
|
509
1479
|
);
|
|
@@ -513,19 +1483,51 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
513
1483
|
title: "See Page (Screenshot + Elements)",
|
|
514
1484
|
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.",
|
|
515
1485
|
inputSchema: BrowserSessionInputSchema,
|
|
1486
|
+
outputSchema: BrowserScreenshotOutputSchema,
|
|
516
1487
|
annotations: annotations("See Page", true)
|
|
517
1488
|
},
|
|
518
1489
|
async (input) => {
|
|
1490
|
+
if (localBrowserModeEnabled()) {
|
|
1491
|
+
try {
|
|
1492
|
+
const res2 = await localBrowser.screenshot(input.session_id);
|
|
1493
|
+
const content2 = [];
|
|
1494
|
+
if (res2.image_base64) content2.push({ type: "image", data: res2.image_base64, mimeType: res2.mime_type });
|
|
1495
|
+
const structured2 = {
|
|
1496
|
+
ok: true,
|
|
1497
|
+
tool: "browser_screenshot",
|
|
1498
|
+
session_id: input.session_id,
|
|
1499
|
+
url: res2.url ?? null,
|
|
1500
|
+
title: res2.title ?? null,
|
|
1501
|
+
text: res2.text,
|
|
1502
|
+
elements: res2.elements,
|
|
1503
|
+
screenshot: { mime_type: res2.mime_type, inline: true }
|
|
1504
|
+
};
|
|
1505
|
+
content2.push({ type: "text", text: JSON.stringify(structured2) });
|
|
1506
|
+
return { content: content2, structuredContent: structured2 };
|
|
1507
|
+
} catch (err) {
|
|
1508
|
+
return errorResult("browser_screenshot", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
519
1511
|
const res = await req("POST", `/agent/sessions/${input.session_id}/screenshot`);
|
|
520
|
-
if (!res.ok) return
|
|
1512
|
+
if (!res.ok) return errorResult("browser_screenshot", res.data, input.session_id);
|
|
521
1513
|
const { image_base64, mime_type, url, title, elements, text } = res.data;
|
|
522
1514
|
const content = [];
|
|
523
1515
|
if (image_base64) content.push({ type: "image", data: image_base64, mimeType: mime_type ?? "image/png" });
|
|
1516
|
+
const structured = {
|
|
1517
|
+
ok: true,
|
|
1518
|
+
tool: "browser_screenshot",
|
|
1519
|
+
session_id: input.session_id,
|
|
1520
|
+
url: url ?? null,
|
|
1521
|
+
title: title ?? null,
|
|
1522
|
+
text: typeof text === "string" ? text : "",
|
|
1523
|
+
elements: Array.isArray(elements) ? elements : [],
|
|
1524
|
+
screenshot: image_base64 ? { mime_type: mime_type ?? "image/png", inline: true } : null
|
|
1525
|
+
};
|
|
524
1526
|
content.push({
|
|
525
1527
|
type: "text",
|
|
526
|
-
text: JSON.stringify(
|
|
1528
|
+
text: JSON.stringify(structured)
|
|
527
1529
|
});
|
|
528
|
-
return { content };
|
|
1530
|
+
return { content, structuredContent: structured };
|
|
529
1531
|
}
|
|
530
1532
|
);
|
|
531
1533
|
server2.registerTool(
|
|
@@ -534,11 +1536,39 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
534
1536
|
title: "Read Page Text + Elements",
|
|
535
1537
|
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.",
|
|
536
1538
|
inputSchema: BrowserSessionInputSchema,
|
|
1539
|
+
outputSchema: BrowserReadOutputSchema,
|
|
537
1540
|
annotations: annotations("Read Page", true)
|
|
538
1541
|
},
|
|
539
1542
|
async (input) => {
|
|
1543
|
+
if (localBrowserModeEnabled()) {
|
|
1544
|
+
try {
|
|
1545
|
+
const res2 = await localBrowser.read(input.session_id);
|
|
1546
|
+
return structuredResult({
|
|
1547
|
+
ok: true,
|
|
1548
|
+
tool: "browser_read",
|
|
1549
|
+
session_id: input.session_id,
|
|
1550
|
+
url: res2.url ?? null,
|
|
1551
|
+
title: res2.title ?? null,
|
|
1552
|
+
text: res2.text,
|
|
1553
|
+
elements: res2.elements,
|
|
1554
|
+
raw: res2
|
|
1555
|
+
});
|
|
1556
|
+
} catch (err) {
|
|
1557
|
+
return errorResult("browser_read", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
540
1560
|
const res = await req("POST", `/agent/sessions/${input.session_id}/read`);
|
|
541
|
-
return
|
|
1561
|
+
if (!res.ok) return errorResult("browser_read", res.data, input.session_id);
|
|
1562
|
+
return structuredResult({
|
|
1563
|
+
ok: true,
|
|
1564
|
+
tool: "browser_read",
|
|
1565
|
+
session_id: input.session_id,
|
|
1566
|
+
url: res.data?.url ?? null,
|
|
1567
|
+
title: res.data?.title ?? null,
|
|
1568
|
+
text: typeof res.data?.text === "string" ? res.data.text : "",
|
|
1569
|
+
elements: Array.isArray(res.data?.elements) ? res.data.elements : [],
|
|
1570
|
+
raw: res.data
|
|
1571
|
+
});
|
|
542
1572
|
}
|
|
543
1573
|
);
|
|
544
1574
|
server2.registerTool(
|
|
@@ -547,86 +1577,161 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
547
1577
|
title: "Locate DOM Targets",
|
|
548
1578
|
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.",
|
|
549
1579
|
inputSchema: BrowserLocateInputSchema,
|
|
1580
|
+
outputSchema: BrowserLocateOutputSchema,
|
|
550
1581
|
annotations: annotations("Locate DOM Targets", true)
|
|
551
1582
|
},
|
|
552
1583
|
async (input) => {
|
|
1584
|
+
if (localBrowserModeEnabled()) {
|
|
1585
|
+
try {
|
|
1586
|
+
const res2 = await localBrowser.locate(input.session_id, input.targets);
|
|
1587
|
+
return structuredResult({
|
|
1588
|
+
ok: true,
|
|
1589
|
+
tool: "browser_locate",
|
|
1590
|
+
session_id: input.session_id,
|
|
1591
|
+
url: res2.url ?? null,
|
|
1592
|
+
title: res2.title ?? null,
|
|
1593
|
+
viewport: res2.viewport ?? null,
|
|
1594
|
+
replay: null,
|
|
1595
|
+
targets: Array.isArray(res2.targets) ? res2.targets : [],
|
|
1596
|
+
raw: res2
|
|
1597
|
+
});
|
|
1598
|
+
} catch (err) {
|
|
1599
|
+
return errorResult("browser_locate", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
553
1602
|
const res = await req("POST", `/agent/sessions/${input.session_id}/locate`, { targets: input.targets });
|
|
554
|
-
return
|
|
1603
|
+
if (!res.ok) return errorResult("browser_locate", res.data, input.session_id);
|
|
1604
|
+
return structuredResult({
|
|
1605
|
+
ok: true,
|
|
1606
|
+
tool: "browser_locate",
|
|
1607
|
+
session_id: input.session_id,
|
|
1608
|
+
url: res.data?.url ?? null,
|
|
1609
|
+
title: res.data?.title ?? null,
|
|
1610
|
+
viewport: res.data?.viewport ?? null,
|
|
1611
|
+
replay: res.data?.replay ?? null,
|
|
1612
|
+
targets: Array.isArray(res.data?.targets) ? res.data.targets : [],
|
|
1613
|
+
raw: res.data
|
|
1614
|
+
});
|
|
555
1615
|
}
|
|
556
1616
|
);
|
|
557
1617
|
server2.registerTool(
|
|
558
1618
|
"browser_goto",
|
|
559
1619
|
{
|
|
560
1620
|
title: "Navigate To URL",
|
|
561
|
-
description: "Navigate
|
|
1621
|
+
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.",
|
|
562
1622
|
inputSchema: BrowserGotoInputSchema,
|
|
1623
|
+
outputSchema: BrowserActionOutputSchema,
|
|
563
1624
|
annotations: annotations("Navigate To URL")
|
|
564
1625
|
},
|
|
565
1626
|
async (input) => {
|
|
1627
|
+
if (localBrowserModeEnabled()) {
|
|
1628
|
+
try {
|
|
1629
|
+
const res2 = await localBrowser.goto(input.session_id, input.url);
|
|
1630
|
+
return actionResult("browser_goto", input.session_id, true, res2, "browser_screenshot");
|
|
1631
|
+
} catch (err) {
|
|
1632
|
+
return errorResult("browser_goto", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
566
1635
|
const res = await req("POST", `/agent/sessions/${input.session_id}/goto`, { url: input.url });
|
|
567
|
-
return
|
|
1636
|
+
return actionResult("browser_goto", input.session_id, res.ok, res.data, "browser_screenshot");
|
|
568
1637
|
}
|
|
569
1638
|
);
|
|
570
1639
|
server2.registerTool(
|
|
571
1640
|
"browser_click",
|
|
572
1641
|
{
|
|
573
1642
|
title: "Click",
|
|
574
|
-
description: "Click
|
|
1643
|
+
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.",
|
|
575
1644
|
inputSchema: BrowserClickInputSchema,
|
|
1645
|
+
outputSchema: BrowserActionOutputSchema,
|
|
576
1646
|
annotations: annotations("Click")
|
|
577
1647
|
},
|
|
578
1648
|
async (input) => {
|
|
1649
|
+
if (localBrowserModeEnabled()) {
|
|
1650
|
+
try {
|
|
1651
|
+
const res2 = await localBrowser.click(input.session_id, input);
|
|
1652
|
+
return actionResult("browser_click", input.session_id, true, res2, "browser_screenshot");
|
|
1653
|
+
} catch (err) {
|
|
1654
|
+
return errorResult("browser_click", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
579
1657
|
const res = await req("POST", `/agent/sessions/${input.session_id}/click`, {
|
|
580
1658
|
x: input.x,
|
|
581
1659
|
y: input.y,
|
|
582
1660
|
button: input.button,
|
|
583
1661
|
num_clicks: input.num_clicks
|
|
584
1662
|
});
|
|
585
|
-
return
|
|
1663
|
+
return actionResult("browser_click", input.session_id, res.ok, res.data, "browser_screenshot");
|
|
586
1664
|
}
|
|
587
1665
|
);
|
|
588
1666
|
server2.registerTool(
|
|
589
1667
|
"browser_type",
|
|
590
1668
|
{
|
|
591
1669
|
title: "Type Text",
|
|
592
|
-
description: 'Type text
|
|
1670
|
+
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.',
|
|
593
1671
|
inputSchema: BrowserTypeInputSchema,
|
|
1672
|
+
outputSchema: BrowserActionOutputSchema,
|
|
594
1673
|
annotations: annotations("Type Text")
|
|
595
1674
|
},
|
|
596
1675
|
async (input) => {
|
|
1676
|
+
if (localBrowserModeEnabled()) {
|
|
1677
|
+
try {
|
|
1678
|
+
const res2 = await localBrowser.type(input.session_id, input);
|
|
1679
|
+
return actionResult("browser_type", input.session_id, true, res2, "browser_screenshot");
|
|
1680
|
+
} catch (err) {
|
|
1681
|
+
return errorResult("browser_type", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
597
1684
|
const res = await req("POST", `/agent/sessions/${input.session_id}/type`, { text: input.text, delay: input.delay });
|
|
598
|
-
return
|
|
1685
|
+
return actionResult("browser_type", input.session_id, res.ok, res.data, "browser_screenshot");
|
|
599
1686
|
}
|
|
600
1687
|
);
|
|
601
1688
|
server2.registerTool(
|
|
602
1689
|
"browser_scroll",
|
|
603
1690
|
{
|
|
604
1691
|
title: "Scroll",
|
|
605
|
-
description: "Scroll the page. Positive delta_y scrolls down. Follow with browser_screenshot to
|
|
1692
|
+
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.",
|
|
606
1693
|
inputSchema: BrowserScrollInputSchema,
|
|
1694
|
+
outputSchema: BrowserActionOutputSchema,
|
|
607
1695
|
annotations: annotations("Scroll")
|
|
608
1696
|
},
|
|
609
1697
|
async (input) => {
|
|
1698
|
+
if (localBrowserModeEnabled()) {
|
|
1699
|
+
try {
|
|
1700
|
+
const res2 = await localBrowser.scroll(input.session_id, input);
|
|
1701
|
+
return actionResult("browser_scroll", input.session_id, true, res2, "browser_screenshot");
|
|
1702
|
+
} catch (err) {
|
|
1703
|
+
return errorResult("browser_scroll", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
610
1706
|
const res = await req("POST", `/agent/sessions/${input.session_id}/scroll`, {
|
|
611
1707
|
delta_y: input.delta_y,
|
|
612
1708
|
delta_x: input.delta_x,
|
|
613
1709
|
x: input.x,
|
|
614
1710
|
y: input.y
|
|
615
1711
|
});
|
|
616
|
-
return
|
|
1712
|
+
return actionResult("browser_scroll", input.session_id, res.ok, res.data, "browser_screenshot");
|
|
617
1713
|
}
|
|
618
1714
|
);
|
|
619
1715
|
server2.registerTool(
|
|
620
1716
|
"browser_press",
|
|
621
1717
|
{
|
|
622
1718
|
title: "Press Keys",
|
|
623
|
-
description: 'Press keys or combinations,
|
|
1719
|
+
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.',
|
|
624
1720
|
inputSchema: BrowserPressInputSchema,
|
|
1721
|
+
outputSchema: BrowserActionOutputSchema,
|
|
625
1722
|
annotations: annotations("Press Keys")
|
|
626
1723
|
},
|
|
627
1724
|
async (input) => {
|
|
1725
|
+
if (localBrowserModeEnabled()) {
|
|
1726
|
+
try {
|
|
1727
|
+
const res2 = await localBrowser.press(input.session_id, input);
|
|
1728
|
+
return actionResult("browser_press", input.session_id, true, res2, "browser_screenshot");
|
|
1729
|
+
} catch (err) {
|
|
1730
|
+
return errorResult("browser_press", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
628
1733
|
const res = await req("POST", `/agent/sessions/${input.session_id}/press`, { keys: input.keys });
|
|
629
|
-
return
|
|
1734
|
+
return actionResult("browser_press", input.session_id, res.ok, res.data, "browser_screenshot");
|
|
630
1735
|
}
|
|
631
1736
|
);
|
|
632
1737
|
server2.registerTool(
|
|
@@ -635,11 +1740,22 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
635
1740
|
title: "Start Recording",
|
|
636
1741
|
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.",
|
|
637
1742
|
inputSchema: BrowserSessionInputSchema,
|
|
1743
|
+
outputSchema: BrowserReplayStartOutputSchema,
|
|
638
1744
|
annotations: annotations("Start Recording")
|
|
639
1745
|
},
|
|
640
1746
|
async (input) => {
|
|
1747
|
+
if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_start", input.session_id);
|
|
641
1748
|
const res = await req("POST", `/agent/sessions/${input.session_id}/replay/start`);
|
|
642
|
-
return
|
|
1749
|
+
if (!res.ok) return errorResult("browser_replay_start", res.data, input.session_id);
|
|
1750
|
+
return structuredResult({
|
|
1751
|
+
ok: true,
|
|
1752
|
+
tool: "browser_replay_start",
|
|
1753
|
+
session_id: input.session_id,
|
|
1754
|
+
replay_id: res.data?.replay_id ?? res.data?.replayId ?? null,
|
|
1755
|
+
view_url: res.data?.view_url ?? res.data?.viewUrl ?? null,
|
|
1756
|
+
download_url: res.data?.download_url ?? res.data?.downloadUrl ?? null,
|
|
1757
|
+
raw: res.data
|
|
1758
|
+
});
|
|
643
1759
|
}
|
|
644
1760
|
);
|
|
645
1761
|
server2.registerTool(
|
|
@@ -648,11 +1764,22 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
648
1764
|
title: "Stop Recording",
|
|
649
1765
|
description: "Stop a replay recording and expose the final view_url and download_url. Use browser_replay_download to save the MP4 locally.",
|
|
650
1766
|
inputSchema: BrowserReplayStopInputSchema,
|
|
1767
|
+
outputSchema: BrowserReplayStopOutputSchema,
|
|
651
1768
|
annotations: annotations("Stop Recording")
|
|
652
1769
|
},
|
|
653
1770
|
async (input) => {
|
|
1771
|
+
if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_stop", input.session_id, input.replay_id);
|
|
654
1772
|
const res = await req("POST", `/agent/sessions/${input.session_id}/replay/stop`, { replay_id: input.replay_id });
|
|
655
|
-
return
|
|
1773
|
+
if (!res.ok) return errorResult("browser_replay_stop", res.data, input.session_id, input.replay_id);
|
|
1774
|
+
return structuredResult({
|
|
1775
|
+
ok: true,
|
|
1776
|
+
tool: "browser_replay_stop",
|
|
1777
|
+
session_id: input.session_id,
|
|
1778
|
+
replay_id: res.data?.replay_id ?? res.data?.replayId ?? input.replay_id,
|
|
1779
|
+
view_url: res.data?.view_url ?? res.data?.viewUrl ?? null,
|
|
1780
|
+
download_url: res.data?.download_url ?? res.data?.downloadUrl ?? null,
|
|
1781
|
+
raw: res.data
|
|
1782
|
+
});
|
|
656
1783
|
}
|
|
657
1784
|
);
|
|
658
1785
|
server2.registerTool(
|
|
@@ -661,11 +1788,21 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
661
1788
|
title: "List Replay Videos",
|
|
662
1789
|
description: "List replay recordings for a browser session, including final view_url and authenticated download_url values when available.",
|
|
663
1790
|
inputSchema: BrowserSessionInputSchema,
|
|
1791
|
+
outputSchema: BrowserListReplaysOutputSchema,
|
|
664
1792
|
annotations: annotations("List Replay Videos", true)
|
|
665
1793
|
},
|
|
666
1794
|
async (input) => {
|
|
1795
|
+
if (localBrowserModeEnabled()) return localReplayUnsupported("browser_list_replays", input.session_id);
|
|
667
1796
|
const res = await req("GET", `/agent/sessions/${input.session_id}/replays`);
|
|
668
|
-
return
|
|
1797
|
+
if (!res.ok) return errorResult("browser_list_replays", res.data, input.session_id);
|
|
1798
|
+
const replays = Array.isArray(res.data?.replays) ? res.data.replays : [];
|
|
1799
|
+
return structuredResult({
|
|
1800
|
+
ok: true,
|
|
1801
|
+
tool: "browser_list_replays",
|
|
1802
|
+
session_id: input.session_id,
|
|
1803
|
+
replays,
|
|
1804
|
+
count: replays.length
|
|
1805
|
+
});
|
|
669
1806
|
}
|
|
670
1807
|
);
|
|
671
1808
|
server2.registerTool(
|
|
@@ -674,11 +1811,23 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
674
1811
|
title: "Download Replay MP4",
|
|
675
1812
|
description: "Download a replay recording through MCP Scraper and save the MP4 locally under MCP_SCRAPER_OUTPUT_DIR/browser-replays. Use after browser_replay_stop or browser_list_replays.",
|
|
676
1813
|
inputSchema: BrowserReplayDownloadInputSchema,
|
|
677
|
-
|
|
1814
|
+
outputSchema: BrowserReplayDownloadOutputSchema,
|
|
1815
|
+
annotations: annotations("Download Replay MP4")
|
|
678
1816
|
},
|
|
679
1817
|
async (input) => {
|
|
1818
|
+
if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_download", input.session_id, input.replay_id);
|
|
680
1819
|
const res = await downloadReplay(input.session_id, input.replay_id, input.filename);
|
|
681
|
-
return
|
|
1820
|
+
if (!res.ok) return errorResult("browser_replay_download", res.data, input.session_id, input.replay_id);
|
|
1821
|
+
return structuredResult({
|
|
1822
|
+
ok: true,
|
|
1823
|
+
tool: "browser_replay_download",
|
|
1824
|
+
session_id: input.session_id,
|
|
1825
|
+
replay_id: input.replay_id,
|
|
1826
|
+
file_path: res.data?.file_path ?? null,
|
|
1827
|
+
bytes: typeof res.data?.bytes === "number" ? res.data.bytes : null,
|
|
1828
|
+
mime_type: res.data?.mime_type ?? null,
|
|
1829
|
+
download_url: res.data?.download_url ?? null
|
|
1830
|
+
});
|
|
682
1831
|
}
|
|
683
1832
|
);
|
|
684
1833
|
server2.registerTool(
|
|
@@ -687,19 +1836,21 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
687
1836
|
title: "Mark Replay Annotation",
|
|
688
1837
|
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.",
|
|
689
1838
|
inputSchema: BrowserReplayMarkInputSchema,
|
|
1839
|
+
outputSchema: BrowserReplayMarkOutputSchema,
|
|
690
1840
|
annotations: annotations("Mark Replay Annotation", true)
|
|
691
1841
|
},
|
|
692
1842
|
async (input) => {
|
|
1843
|
+
if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_mark", input.session_id);
|
|
693
1844
|
const res = await req("POST", `/agent/sessions/${input.session_id}/locate`, { targets: [input.target] });
|
|
694
|
-
if (!res.ok) return
|
|
1845
|
+
if (!res.ok) return errorResult("browser_replay_mark", res.data, input.session_id);
|
|
695
1846
|
const target = res.data?.targets?.[0];
|
|
696
1847
|
const element = target?.element;
|
|
697
1848
|
const elapsed = res.data?.replay?.replay_elapsed_seconds;
|
|
698
1849
|
if (!target?.found || !element) {
|
|
699
|
-
return
|
|
1850
|
+
return errorResult("browser_replay_mark", { error: target?.error ?? "target not found in current viewport", target }, input.session_id);
|
|
700
1851
|
}
|
|
701
1852
|
if (!finiteNumber2(elapsed)) {
|
|
702
|
-
return
|
|
1853
|
+
return errorResult("browser_replay_mark", { error: "no active replay clock found; call browser_replay_start before browser_replay_mark" }, input.session_id);
|
|
703
1854
|
}
|
|
704
1855
|
const padded = expandElementBounds(element, res.data?.viewport, input.padding ?? 8);
|
|
705
1856
|
const start = Math.max(0, elapsed + (input.start_offset_seconds ?? -0.25));
|
|
@@ -716,7 +1867,11 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
716
1867
|
...input.color ? { color: input.color } : {},
|
|
717
1868
|
...input.thickness ? { thickness: input.thickness } : {}
|
|
718
1869
|
};
|
|
719
|
-
return
|
|
1870
|
+
return structuredResult({
|
|
1871
|
+
ok: true,
|
|
1872
|
+
tool: "browser_replay_mark",
|
|
1873
|
+
session_id: input.session_id,
|
|
1874
|
+
replay_id: res.data?.replay?.replay_id ?? res.data?.replay?.replayId ?? null,
|
|
720
1875
|
annotation,
|
|
721
1876
|
source_width: padded.sourceWidth,
|
|
722
1877
|
source_height: padded.sourceHeight,
|
|
@@ -732,16 +1887,18 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
732
1887
|
title: "Annotate Replay MP4",
|
|
733
1888
|
description: "Download a browser replay MP4, render visual annotations over it, and save a new annotated MP4 locally. Use this after browser_replay_stop when the user wants proof videos with circles, boxes, arrows, underlines, or labels. For accurate timing and placement, prefer annotations returned by browser_replay_mark while the replay is recording; otherwise use exact left/top/width/height bounds from browser_locate. If the replay video size differs from the screenshot coordinate space, pass source_width and source_height.",
|
|
734
1889
|
inputSchema: BrowserReplayAnnotateInputSchema,
|
|
735
|
-
|
|
1890
|
+
outputSchema: BrowserReplayAnnotateOutputSchema,
|
|
1891
|
+
annotations: annotations("Annotate Replay MP4")
|
|
736
1892
|
},
|
|
737
1893
|
async (input) => {
|
|
1894
|
+
if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_annotate", input.session_id, input.replay_id);
|
|
738
1895
|
const sourceName = input.filename ? `${input.filename}-source` : void 0;
|
|
739
1896
|
const downloaded = await downloadReplay(input.session_id, input.replay_id, sourceName);
|
|
740
|
-
if (!downloaded.ok) return
|
|
1897
|
+
if (!downloaded.ok) return errorResult("browser_replay_annotate", downloaded.data, input.session_id, input.replay_id);
|
|
741
1898
|
try {
|
|
742
1899
|
const sourcePath = String(downloaded.data.file_path);
|
|
743
1900
|
const outputPath = annotatedReplayFilePath(input.session_id, input.replay_id, input.filename);
|
|
744
|
-
(0,
|
|
1901
|
+
(0, import_node_fs3.mkdirSync)((0, import_node_path5.join)(outputBaseDir(), "browser-replays"), { recursive: true });
|
|
745
1902
|
const result = await annotateReplayVideo(sourcePath, outputPath, {
|
|
746
1903
|
annotations: input.annotations,
|
|
747
1904
|
sourceWidth: input.source_width,
|
|
@@ -749,7 +1906,10 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
749
1906
|
sourceLeftOffset: input.source_left_offset,
|
|
750
1907
|
sourceTopOffset: input.source_top_offset
|
|
751
1908
|
});
|
|
752
|
-
return
|
|
1909
|
+
return structuredResult({
|
|
1910
|
+
ok: true,
|
|
1911
|
+
tool: "browser_replay_annotate",
|
|
1912
|
+
session_id: input.session_id,
|
|
753
1913
|
replay_id: input.replay_id,
|
|
754
1914
|
source_file_path: sourcePath,
|
|
755
1915
|
annotated_file_path: result.filePath,
|
|
@@ -760,7 +1920,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
760
1920
|
mime_type: "video/mp4"
|
|
761
1921
|
});
|
|
762
1922
|
} catch (err) {
|
|
763
|
-
return
|
|
1923
|
+
return errorResult("browser_replay_annotate", { error: err instanceof Error ? err.message : String(err) }, input.session_id, input.replay_id);
|
|
764
1924
|
}
|
|
765
1925
|
}
|
|
766
1926
|
);
|
|
@@ -768,28 +1928,67 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
768
1928
|
"browser_close",
|
|
769
1929
|
{
|
|
770
1930
|
title: "Close Browser Session",
|
|
771
|
-
description: "Close and release
|
|
1931
|
+
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.",
|
|
772
1932
|
inputSchema: BrowserSessionInputSchema,
|
|
1933
|
+
outputSchema: BrowserCloseOutputSchema,
|
|
773
1934
|
annotations: { title: "Close Browser Session", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
|
|
774
1935
|
},
|
|
775
1936
|
async (input) => {
|
|
1937
|
+
if (localBrowserModeEnabled()) {
|
|
1938
|
+
try {
|
|
1939
|
+
const res2 = await localBrowser.close(input.session_id);
|
|
1940
|
+
return structuredResult({
|
|
1941
|
+
ok: true,
|
|
1942
|
+
tool: "browser_close",
|
|
1943
|
+
session_id: input.session_id,
|
|
1944
|
+
closed: true,
|
|
1945
|
+
raw: res2
|
|
1946
|
+
});
|
|
1947
|
+
} catch (err) {
|
|
1948
|
+
return errorResult("browser_close", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
776
1951
|
const res = await req("DELETE", `/agent/sessions/${input.session_id}`);
|
|
777
|
-
return
|
|
1952
|
+
if (!res.ok) return errorResult("browser_close", res.data, input.session_id);
|
|
1953
|
+
return structuredResult({
|
|
1954
|
+
ok: true,
|
|
1955
|
+
tool: "browser_close",
|
|
1956
|
+
session_id: input.session_id,
|
|
1957
|
+
closed: true,
|
|
1958
|
+
raw: res.data
|
|
1959
|
+
});
|
|
778
1960
|
}
|
|
779
1961
|
);
|
|
780
1962
|
server2.registerTool(
|
|
781
1963
|
"browser_list_sessions",
|
|
782
1964
|
{
|
|
783
1965
|
title: "List Browser Sessions",
|
|
784
|
-
description: "List
|
|
1966
|
+
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.",
|
|
785
1967
|
inputSchema: BrowserListInputSchema,
|
|
1968
|
+
outputSchema: BrowserListSessionsOutputSchema,
|
|
786
1969
|
annotations: annotations("List Browser Sessions", true)
|
|
787
1970
|
},
|
|
788
1971
|
async (input) => {
|
|
1972
|
+
if (localBrowserModeEnabled()) {
|
|
1973
|
+
const sessions2 = localBrowser.list(input.include_closed);
|
|
1974
|
+
return structuredResult({
|
|
1975
|
+
ok: true,
|
|
1976
|
+
tool: "browser_list_sessions",
|
|
1977
|
+
session_id: null,
|
|
1978
|
+
sessions: sessions2,
|
|
1979
|
+
count: sessions2.length
|
|
1980
|
+
});
|
|
1981
|
+
}
|
|
789
1982
|
const res = await req("GET", `/agent/sessions${input.include_closed ? "?all=1" : ""}`);
|
|
790
|
-
if (!res.ok) return
|
|
1983
|
+
if (!res.ok) return errorResult("browser_list_sessions", res.data);
|
|
791
1984
|
const sessions = (res.data.sessions ?? []).map((s) => ({ ...s, watch_url: `${consoleBase}/console/${s.session_id}` }));
|
|
792
|
-
return
|
|
1985
|
+
return structuredResult({
|
|
1986
|
+
ok: true,
|
|
1987
|
+
tool: "browser_list_sessions",
|
|
1988
|
+
session_id: null,
|
|
1989
|
+
sessions,
|
|
1990
|
+
count: sessions.length
|
|
1991
|
+
});
|
|
793
1992
|
}
|
|
794
1993
|
);
|
|
795
1994
|
}
|
|
@@ -797,10 +1996,10 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
797
1996
|
// bin/browser-agent-stdio-server.ts
|
|
798
1997
|
function readApiKeyFile() {
|
|
799
1998
|
const explicitPath = process.env.MCP_SCRAPER_KEY_PATH?.trim();
|
|
800
|
-
const paths = [explicitPath, (0,
|
|
1999
|
+
const paths = [explicitPath, (0, import_node_path6.join)((0, import_node_os5.homedir)(), ".mcp-scraper-key")].filter(Boolean);
|
|
801
2000
|
for (const path of paths) {
|
|
802
2001
|
try {
|
|
803
|
-
const value = (0,
|
|
2002
|
+
const value = (0, import_node_fs4.readFileSync)(path, "utf8").trim();
|
|
804
2003
|
if (value) return value;
|
|
805
2004
|
} catch {
|
|
806
2005
|
}
|