mcp-scraper 0.2.20 → 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 +34 -3
- package/dist/bin/api-server.cjs +52 -1
- package/dist/bin/api-server.cjs.map +1 -1
- package/dist/bin/api-server.js +2 -2
- package/dist/bin/browser-agent-stdio-server.cjs +960 -20
- package/dist/bin/browser-agent-stdio-server.cjs.map +1 -1
- package/dist/bin/browser-agent-stdio-server.js +3 -1
- package/dist/bin/browser-agent-stdio-server.js.map +1 -1
- package/dist/bin/mcp-scraper-cli.cjs +342 -47
- package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
- package/dist/bin/mcp-scraper-cli.js +105 -7
- package/dist/bin/mcp-scraper-cli.js.map +1 -1
- package/dist/bin/mcp-scraper-combined-stdio-server.cjs +976 -36
- package/dist/bin/mcp-scraper-combined-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-scraper-combined-stdio-server.js +3 -1
- package/dist/bin/mcp-scraper-combined-stdio-server.js.map +1 -1
- package/dist/bin/paa-harvest.cjs.map +1 -1
- package/dist/bin/paa-harvest.js +4 -2
- package/dist/bin/paa-harvest.js.map +1 -1
- package/dist/chunk-DUEW4EOO.js +214 -0
- package/dist/chunk-DUEW4EOO.js.map +1 -0
- package/dist/{chunk-L4OWOUGR.js → chunk-HS6OKELW.js} +737 -3
- package/dist/chunk-HS6OKELW.js.map +1 -0
- package/dist/{chunk-XGUDTDZ2.js → chunk-IPW4LFOT.js} +5 -13
- package/dist/chunk-IPW4LFOT.js.map +1 -0
- package/dist/chunk-WN7PBKMV.js +28 -0
- package/dist/chunk-WN7PBKMV.js.map +1 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/{server-7NE45K2A.js → server-24XIQ6W6.js} +40 -6
- package/dist/server-24XIQ6W6.js.map +1 -0
- package/dist/{worker-MIYG2B2I.js → worker-SLQ375UG.js} +5 -3
- package/dist/{worker-MIYG2B2I.js.map → worker-SLQ375UG.js.map} +1 -1
- package/docs/mcp-tool-manifest.generated.json +132 -4
- package/package.json +1 -1
- package/dist/chunk-L4OWOUGR.js.map +0 -1
- package/dist/chunk-XGUDTDZ2.js.map +0 -1
- package/dist/server-7NE45K2A.js.map +0 -1
|
@@ -2,16 +2,589 @@
|
|
|
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
590
|
var PACKAGE_VERSION = "0.2.20";
|
|
@@ -34,8 +607,35 @@ var BrowserOpenInputSchema = {
|
|
|
34
607
|
label: import_zod.z.string().optional().describe("Optional human label for this session, shown in the watch console."),
|
|
35
608
|
url: import_zod.z.string().url().optional().describe("Optional URL to navigate to immediately after opening."),
|
|
36
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."),
|
|
37
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.")
|
|
38
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
|
+
};
|
|
39
639
|
var BrowserSessionInputSchema = {
|
|
40
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.")
|
|
41
641
|
};
|
|
@@ -140,6 +740,43 @@ var BrowserOpenOutputSchema = {
|
|
|
140
740
|
hint: import_zod.z.string(),
|
|
141
741
|
raw: BrowserRawObject.optional()
|
|
142
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
|
+
};
|
|
143
780
|
var BrowserScreenshotOutputSchema = {
|
|
144
781
|
...BrowserBaseOutput,
|
|
145
782
|
tool: import_zod.z.literal("browser_screenshot"),
|
|
@@ -240,9 +877,9 @@ var BrowserListSessionsOutputSchema = {
|
|
|
240
877
|
|
|
241
878
|
// src/mcp/replay-annotator.ts
|
|
242
879
|
var import_node_child_process = require("child_process");
|
|
243
|
-
var
|
|
244
|
-
var
|
|
245
|
-
var
|
|
880
|
+
var import_promises3 = require("fs/promises");
|
|
881
|
+
var import_node_os3 = require("os");
|
|
882
|
+
var import_node_path4 = require("path");
|
|
246
883
|
var import_node_util = require("util");
|
|
247
884
|
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
248
885
|
function finiteNumber(value) {
|
|
@@ -465,10 +1102,10 @@ function ffmpegFilterPath(path) {
|
|
|
465
1102
|
async function annotateReplayVideo(inputFilePath, outputFilePath, options) {
|
|
466
1103
|
if (!options.annotations.length) throw new Error("annotations must include at least one item");
|
|
467
1104
|
const size = await videoSize(inputFilePath);
|
|
468
|
-
const tmp = await (0,
|
|
469
|
-
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");
|
|
470
1107
|
try {
|
|
471
|
-
await (0,
|
|
1108
|
+
await (0, import_promises3.writeFile)(assPath, buildAssSubtitle(options, size), "utf8");
|
|
472
1109
|
await execFileAsync("ffmpeg", [
|
|
473
1110
|
"-y",
|
|
474
1111
|
"-i",
|
|
@@ -485,7 +1122,7 @@ async function annotateReplayVideo(inputFilePath, outputFilePath, options) {
|
|
|
485
1122
|
"copy",
|
|
486
1123
|
outputFilePath
|
|
487
1124
|
], { maxBuffer: 1024 * 1024 * 20 });
|
|
488
|
-
const out = await (0,
|
|
1125
|
+
const out = await (0, import_promises3.stat)(outputFilePath);
|
|
489
1126
|
return {
|
|
490
1127
|
filePath: outputFilePath,
|
|
491
1128
|
bytes: out.size,
|
|
@@ -494,7 +1131,7 @@ async function annotateReplayVideo(inputFilePath, outputFilePath, options) {
|
|
|
494
1131
|
annotationCount: options.annotations.length
|
|
495
1132
|
};
|
|
496
1133
|
} finally {
|
|
497
|
-
await (0,
|
|
1134
|
+
await (0, import_promises3.rm)(tmp, { recursive: true, force: true });
|
|
498
1135
|
}
|
|
499
1136
|
}
|
|
500
1137
|
|
|
@@ -535,7 +1172,7 @@ function actionResult(tool, sessionId, ok, data, nextRecommendedTool = "browser_
|
|
|
535
1172
|
});
|
|
536
1173
|
}
|
|
537
1174
|
function outputBaseDir() {
|
|
538
|
-
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");
|
|
539
1176
|
}
|
|
540
1177
|
function safeFilePart(value) {
|
|
541
1178
|
return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 120) || "replay";
|
|
@@ -544,7 +1181,7 @@ function replayFilePath(sessionId, replayId, filename) {
|
|
|
544
1181
|
const requested = filename?.trim();
|
|
545
1182
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
546
1183
|
const name = requested ? safeFilePart(requested).replace(/\.mp4$/i, "") : `${stamp}-${safeFilePart(sessionId)}-${safeFilePart(replayId)}`;
|
|
547
|
-
return (0,
|
|
1184
|
+
return (0, import_node_path5.join)(outputBaseDir(), "browser-replays", `${name}.mp4`);
|
|
548
1185
|
}
|
|
549
1186
|
function annotatedReplayFilePath(sessionId, replayId, filename) {
|
|
550
1187
|
const requested = filename?.trim();
|
|
@@ -580,6 +1217,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
580
1217
|
const baseUrl2 = opts.baseUrl.replace(/\/$/, "");
|
|
581
1218
|
const consoleBase = (opts.consoleBaseUrl ?? opts.baseUrl).replace(/\/$/, "");
|
|
582
1219
|
const timeoutMs = opts.timeoutMs ?? 9e4;
|
|
1220
|
+
const localBrowser = new LocalBrowserAgentService();
|
|
583
1221
|
async function req(method, path, body) {
|
|
584
1222
|
try {
|
|
585
1223
|
const res = await fetch(`${baseUrl2}${path}`, {
|
|
@@ -608,8 +1246,8 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
608
1246
|
}
|
|
609
1247
|
const bytes = Buffer.from(await res.arrayBuffer());
|
|
610
1248
|
const filePath = replayFilePath(sessionId, replayId, filename);
|
|
611
|
-
(0,
|
|
612
|
-
(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);
|
|
613
1251
|
return {
|
|
614
1252
|
ok: true,
|
|
615
1253
|
data: {
|
|
@@ -631,19 +1269,195 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
631
1269
|
idempotentHint: false,
|
|
632
1270
|
openWorldHint: true
|
|
633
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
|
+
);
|
|
634
1428
|
server2.registerTool(
|
|
635
1429
|
"browser_open",
|
|
636
1430
|
{
|
|
637
1431
|
title: "Open Browser Session",
|
|
638
|
-
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.",
|
|
639
1433
|
inputSchema: BrowserOpenInputSchema,
|
|
640
1434
|
outputSchema: BrowserOpenOutputSchema,
|
|
641
1435
|
annotations: annotations("Open Browser Session")
|
|
642
1436
|
},
|
|
643
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();
|
|
644
1457
|
const open = await req("POST", "/agent/sessions", {
|
|
645
1458
|
label: input.label,
|
|
646
|
-
profile:
|
|
1459
|
+
...profile ? { profile } : {},
|
|
1460
|
+
...profile && typeof saveProfileChanges === "boolean" ? { save_profile_changes: saveProfileChanges } : {},
|
|
647
1461
|
timeout_seconds: input.timeout_seconds
|
|
648
1462
|
});
|
|
649
1463
|
if (!open.ok) return errorResult("browser_open", open.data);
|
|
@@ -673,6 +1487,27 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
673
1487
|
annotations: annotations("See Page", true)
|
|
674
1488
|
},
|
|
675
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
|
+
}
|
|
676
1511
|
const res = await req("POST", `/agent/sessions/${input.session_id}/screenshot`);
|
|
677
1512
|
if (!res.ok) return errorResult("browser_screenshot", res.data, input.session_id);
|
|
678
1513
|
const { image_base64, mime_type, url, title, elements, text } = res.data;
|
|
@@ -705,6 +1540,23 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
705
1540
|
annotations: annotations("Read Page", true)
|
|
706
1541
|
},
|
|
707
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
|
+
}
|
|
708
1560
|
const res = await req("POST", `/agent/sessions/${input.session_id}/read`);
|
|
709
1561
|
if (!res.ok) return errorResult("browser_read", res.data, input.session_id);
|
|
710
1562
|
return structuredResult({
|
|
@@ -729,6 +1581,24 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
729
1581
|
annotations: annotations("Locate DOM Targets", true)
|
|
730
1582
|
},
|
|
731
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
|
+
}
|
|
732
1602
|
const res = await req("POST", `/agent/sessions/${input.session_id}/locate`, { targets: input.targets });
|
|
733
1603
|
if (!res.ok) return errorResult("browser_locate", res.data, input.session_id);
|
|
734
1604
|
return structuredResult({
|
|
@@ -754,6 +1624,14 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
754
1624
|
annotations: annotations("Navigate To URL")
|
|
755
1625
|
},
|
|
756
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
|
+
}
|
|
757
1635
|
const res = await req("POST", `/agent/sessions/${input.session_id}/goto`, { url: input.url });
|
|
758
1636
|
return actionResult("browser_goto", input.session_id, res.ok, res.data, "browser_screenshot");
|
|
759
1637
|
}
|
|
@@ -768,6 +1646,14 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
768
1646
|
annotations: annotations("Click")
|
|
769
1647
|
},
|
|
770
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
|
+
}
|
|
771
1657
|
const res = await req("POST", `/agent/sessions/${input.session_id}/click`, {
|
|
772
1658
|
x: input.x,
|
|
773
1659
|
y: input.y,
|
|
@@ -787,6 +1673,14 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
787
1673
|
annotations: annotations("Type Text")
|
|
788
1674
|
},
|
|
789
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
|
+
}
|
|
790
1684
|
const res = await req("POST", `/agent/sessions/${input.session_id}/type`, { text: input.text, delay: input.delay });
|
|
791
1685
|
return actionResult("browser_type", input.session_id, res.ok, res.data, "browser_screenshot");
|
|
792
1686
|
}
|
|
@@ -801,6 +1695,14 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
801
1695
|
annotations: annotations("Scroll")
|
|
802
1696
|
},
|
|
803
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
|
+
}
|
|
804
1706
|
const res = await req("POST", `/agent/sessions/${input.session_id}/scroll`, {
|
|
805
1707
|
delta_y: input.delta_y,
|
|
806
1708
|
delta_x: input.delta_x,
|
|
@@ -820,6 +1722,14 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
820
1722
|
annotations: annotations("Press Keys")
|
|
821
1723
|
},
|
|
822
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
|
+
}
|
|
823
1733
|
const res = await req("POST", `/agent/sessions/${input.session_id}/press`, { keys: input.keys });
|
|
824
1734
|
return actionResult("browser_press", input.session_id, res.ok, res.data, "browser_screenshot");
|
|
825
1735
|
}
|
|
@@ -834,6 +1744,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
834
1744
|
annotations: annotations("Start Recording")
|
|
835
1745
|
},
|
|
836
1746
|
async (input) => {
|
|
1747
|
+
if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_start", input.session_id);
|
|
837
1748
|
const res = await req("POST", `/agent/sessions/${input.session_id}/replay/start`);
|
|
838
1749
|
if (!res.ok) return errorResult("browser_replay_start", res.data, input.session_id);
|
|
839
1750
|
return structuredResult({
|
|
@@ -857,6 +1768,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
857
1768
|
annotations: annotations("Stop Recording")
|
|
858
1769
|
},
|
|
859
1770
|
async (input) => {
|
|
1771
|
+
if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_stop", input.session_id, input.replay_id);
|
|
860
1772
|
const res = await req("POST", `/agent/sessions/${input.session_id}/replay/stop`, { replay_id: input.replay_id });
|
|
861
1773
|
if (!res.ok) return errorResult("browser_replay_stop", res.data, input.session_id, input.replay_id);
|
|
862
1774
|
return structuredResult({
|
|
@@ -880,6 +1792,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
880
1792
|
annotations: annotations("List Replay Videos", true)
|
|
881
1793
|
},
|
|
882
1794
|
async (input) => {
|
|
1795
|
+
if (localBrowserModeEnabled()) return localReplayUnsupported("browser_list_replays", input.session_id);
|
|
883
1796
|
const res = await req("GET", `/agent/sessions/${input.session_id}/replays`);
|
|
884
1797
|
if (!res.ok) return errorResult("browser_list_replays", res.data, input.session_id);
|
|
885
1798
|
const replays = Array.isArray(res.data?.replays) ? res.data.replays : [];
|
|
@@ -902,6 +1815,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
902
1815
|
annotations: annotations("Download Replay MP4")
|
|
903
1816
|
},
|
|
904
1817
|
async (input) => {
|
|
1818
|
+
if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_download", input.session_id, input.replay_id);
|
|
905
1819
|
const res = await downloadReplay(input.session_id, input.replay_id, input.filename);
|
|
906
1820
|
if (!res.ok) return errorResult("browser_replay_download", res.data, input.session_id, input.replay_id);
|
|
907
1821
|
return structuredResult({
|
|
@@ -926,6 +1840,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
926
1840
|
annotations: annotations("Mark Replay Annotation", true)
|
|
927
1841
|
},
|
|
928
1842
|
async (input) => {
|
|
1843
|
+
if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_mark", input.session_id);
|
|
929
1844
|
const res = await req("POST", `/agent/sessions/${input.session_id}/locate`, { targets: [input.target] });
|
|
930
1845
|
if (!res.ok) return errorResult("browser_replay_mark", res.data, input.session_id);
|
|
931
1846
|
const target = res.data?.targets?.[0];
|
|
@@ -976,13 +1891,14 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
976
1891
|
annotations: annotations("Annotate Replay MP4")
|
|
977
1892
|
},
|
|
978
1893
|
async (input) => {
|
|
1894
|
+
if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_annotate", input.session_id, input.replay_id);
|
|
979
1895
|
const sourceName = input.filename ? `${input.filename}-source` : void 0;
|
|
980
1896
|
const downloaded = await downloadReplay(input.session_id, input.replay_id, sourceName);
|
|
981
1897
|
if (!downloaded.ok) return errorResult("browser_replay_annotate", downloaded.data, input.session_id, input.replay_id);
|
|
982
1898
|
try {
|
|
983
1899
|
const sourcePath = String(downloaded.data.file_path);
|
|
984
1900
|
const outputPath = annotatedReplayFilePath(input.session_id, input.replay_id, input.filename);
|
|
985
|
-
(0,
|
|
1901
|
+
(0, import_node_fs3.mkdirSync)((0, import_node_path5.join)(outputBaseDir(), "browser-replays"), { recursive: true });
|
|
986
1902
|
const result = await annotateReplayVideo(sourcePath, outputPath, {
|
|
987
1903
|
annotations: input.annotations,
|
|
988
1904
|
sourceWidth: input.source_width,
|
|
@@ -1018,6 +1934,20 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
1018
1934
|
annotations: { title: "Close Browser Session", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
|
|
1019
1935
|
},
|
|
1020
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
|
+
}
|
|
1021
1951
|
const res = await req("DELETE", `/agent/sessions/${input.session_id}`);
|
|
1022
1952
|
if (!res.ok) return errorResult("browser_close", res.data, input.session_id);
|
|
1023
1953
|
return structuredResult({
|
|
@@ -1039,6 +1969,16 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
1039
1969
|
annotations: annotations("List Browser Sessions", true)
|
|
1040
1970
|
},
|
|
1041
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
|
+
}
|
|
1042
1982
|
const res = await req("GET", `/agent/sessions${input.include_closed ? "?all=1" : ""}`);
|
|
1043
1983
|
if (!res.ok) return errorResult("browser_list_sessions", res.data);
|
|
1044
1984
|
const sessions = (res.data.sessions ?? []).map((s) => ({ ...s, watch_url: `${consoleBase}/console/${s.session_id}` }));
|
|
@@ -1056,10 +1996,10 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
1056
1996
|
// bin/browser-agent-stdio-server.ts
|
|
1057
1997
|
function readApiKeyFile() {
|
|
1058
1998
|
const explicitPath = process.env.MCP_SCRAPER_KEY_PATH?.trim();
|
|
1059
|
-
const paths = [explicitPath, (0,
|
|
1999
|
+
const paths = [explicitPath, (0, import_node_path6.join)((0, import_node_os5.homedir)(), ".mcp-scraper-key")].filter(Boolean);
|
|
1060
2000
|
for (const path of paths) {
|
|
1061
2001
|
try {
|
|
1062
|
-
const value = (0,
|
|
2002
|
+
const value = (0, import_node_fs4.readFileSync)(path, "utf8").trim();
|
|
1063
2003
|
if (value) return value;
|
|
1064
2004
|
} catch {
|
|
1065
2005
|
}
|