@diegosouzacdv/jev-browser-mcp 0.6.1 → 0.7.0
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 +385 -138
- package/config/ui-testing.json +12 -4
- package/docs/jev-browser-mcp.md +385 -138
- package/mcp_servers/jev-browser-npm/src/accessibility.mjs +18 -9
- package/mcp_servers/jev-browser-npm/src/config.mjs +55 -19
- package/mcp_servers/jev-browser-npm/src/flow.mjs +1610 -339
- package/mcp_servers/jev-browser-npm/src/server.mjs +250 -49
- package/package.json +1 -1
|
@@ -7,21 +7,101 @@ import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
|
7
7
|
import { chromium } from "playwright";
|
|
8
8
|
import * as z from "zod/v4";
|
|
9
9
|
import packageManifest from "../../../package.json" with { type: "json" };
|
|
10
|
-
import { loadSettings, JevBrowserError } from "./config.mjs";
|
|
11
|
-
import { chooseNextAction, createMutationAuthorizationStore, executeBrowserFlow } from "./flow.mjs";
|
|
12
|
-
|
|
13
|
-
const
|
|
10
|
+
import { loadSettings, JevBrowserError } from "./config.mjs";
|
|
11
|
+
import { chooseNextAction, createMutationAuthorizationStore, describeBrowserActions, executeBrowserFlow } from "./flow.mjs";
|
|
12
|
+
|
|
13
|
+
const browserContract = describeBrowserActions();
|
|
14
|
+
const actionNames = Object.keys(browserContract.actions);
|
|
15
|
+
const jsonObject = z.record(z.string(), z.unknown());
|
|
16
|
+
function browserStepFieldSchema(field) {
|
|
17
|
+
if (["mutating", "screenshot", "expect_download", "expect_popup", "blur", "sensitive", "full_page"].includes(field)) return z.boolean();
|
|
18
|
+
if (["timeout_ms", "timeout_seconds"].includes(field)) return z.number().positive();
|
|
19
|
+
if (["index", "context_index", "ms", "duration_ms", "status", "close_code"].includes(field)) return z.number().int();
|
|
20
|
+
if (field === "file_paths") return z.union([z.string(), z.array(z.string())]);
|
|
21
|
+
if (["near", "within", "if_visible", "json"].includes(field)) return jsonObject;
|
|
22
|
+
if (field === "name_match") return z.enum(["exact", "contains", "regex"]);
|
|
23
|
+
if (field === "condition") return z.enum(["network_idle", "angular_idle", "url", "hidden"]);
|
|
24
|
+
if (field === "standard") return z.enum(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa", "section508"]);
|
|
25
|
+
return z.string();
|
|
26
|
+
}
|
|
27
|
+
function browserStepVariant(action, contract, aliases = []) {
|
|
28
|
+
const required = new Set(contract.required);
|
|
29
|
+
const optional = new Set([...contract.optional, "timeout_ms", "mutating", "screenshot", ...aliases]);
|
|
30
|
+
if (action === "wait") { required.delete("ms"); optional.add("ms"); }
|
|
31
|
+
if (action === "wait_for_text") { required.delete("text"); optional.add("text"); }
|
|
32
|
+
if (action === "assert_text" || action === "assert_value") { required.delete("expected"); optional.add("expected"); }
|
|
33
|
+
if (action === "upload_file") { required.delete("file_paths"); optional.add("file_paths"); }
|
|
34
|
+
if (action === "wait") optional.add("duration_ms");
|
|
35
|
+
if (action === "wait_for_text" || action === "assert_text" || action === "assert_value") optional.add("value");
|
|
36
|
+
if (action === "upload_file") optional.add("file_path");
|
|
37
|
+
if (action === "confirm_modal") {
|
|
38
|
+
required.delete("button");
|
|
39
|
+
optional.add("button");
|
|
40
|
+
optional.add("trigger");
|
|
41
|
+
optional.add("confirm");
|
|
42
|
+
}
|
|
43
|
+
const shape = { action: z.literal(action) };
|
|
44
|
+
for (const key of new Set([...required, ...optional])) {
|
|
45
|
+
const field = browserStepFieldSchema(key);
|
|
46
|
+
shape[key] = required.has(key) ? field : field.optional();
|
|
47
|
+
}
|
|
48
|
+
return z.object(shape).strict();
|
|
49
|
+
}
|
|
50
|
+
const browserStepVariants = Object.entries(browserContract.actions).map(([action, contract]) => browserStepVariant(action, contract));
|
|
51
|
+
browserStepVariants.push(
|
|
52
|
+
browserStepVariant("fill", browserContract.actions.type, ["value"]),
|
|
53
|
+
browserStepVariant("press_key", browserContract.actions.press),
|
|
54
|
+
);
|
|
55
|
+
const browserStepSchema = z.discriminatedUnion("action", browserStepVariants);
|
|
56
|
+
const candidatePlanSchema = z.object({
|
|
57
|
+
description: z.string(),
|
|
58
|
+
steps: z.array(browserStepSchema).min(1),
|
|
59
|
+
comment: z.string().optional(),
|
|
60
|
+
when: z.object({ visible: z.string() }).strict().optional(),
|
|
61
|
+
}).strict();
|
|
62
|
+
const browserOptionsShape = Object.fromEntries(browserContract.options.map((key) => {
|
|
63
|
+
const value = key === "return_snapshot" ? z.enum(["diff", "full", "none"])
|
|
64
|
+
: key === "reuse_page_match" ? z.enum(["url", "path"])
|
|
65
|
+
: key === "color_scheme" ? z.enum(["light", "dark", "no-preference"])
|
|
66
|
+
: key === "permissions" ? z.array(z.enum(["microphone", "geolocation"]))
|
|
67
|
+
: key === "console_levels" ? z.array(z.enum(["log", "info", "debug", "warn", "error"]))
|
|
68
|
+
: key === "ready" ? z.union([z.string(), z.record(z.string(), z.unknown()), z.array(z.union([z.string(), z.record(z.string(), z.unknown())]))])
|
|
69
|
+
: ["allow_mutations", "block_trackers", "capture_console_errors", "capture_network_error_bodies", "capture_network_errors", "auto_angular_idle", "block_fonts", "continue_from_current_page", "dry_run", "fast_path", "local_only", "ready_network_idle", "reuse_page", "screenshot_on_failure", "screenshot_on_success", "record_video", "mobile", "snapshot_include_hidden", "include_unnamed_controls", "stop_on_expected", "trace_on_failure", "trace_on_success", "fresh_context", "clear_storage"].includes(key) ? z.boolean()
|
|
70
|
+
: ["max_flow_steps", "ready_stable_ms", "ready_timeout_seconds", "step_timeout_seconds"].includes(key) ? z.number().positive()
|
|
71
|
+
: ["busy_selectors", "login_text", "login_url_contains"].includes(key) ? z.array(z.string())
|
|
72
|
+
: ["viewport", "geolocation", "wait_for_http", "fake_media"].includes(key) ? z.union([z.string(), jsonObject])
|
|
73
|
+
: z.string();
|
|
74
|
+
return [key, value.optional()];
|
|
75
|
+
}));
|
|
76
|
+
const browserOptionsSchema = z.object(browserOptionsShape).passthrough();
|
|
77
|
+
const expectedOutcomeSchema = z.union([
|
|
78
|
+
z.string(),
|
|
79
|
+
z.object({
|
|
80
|
+
text: z.string().optional(),
|
|
81
|
+
request: z.string().optional(),
|
|
82
|
+
method: z.string().optional(),
|
|
83
|
+
status: z.number().int().min(100).max(599).optional(),
|
|
84
|
+
message_contains: z.string().optional(),
|
|
85
|
+
}).strict().refine((value) => Boolean(value.text || value.request), "expected_outcome needs text or request"),
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
const browserPools = new Set();
|
|
14
89
|
|
|
15
90
|
class BrowserPool {
|
|
16
91
|
#settings;
|
|
17
92
|
#contextPromise;
|
|
18
93
|
#context;
|
|
19
|
-
#browser;
|
|
94
|
+
#browser;
|
|
95
|
+
#extraContexts = new Set();
|
|
96
|
+
#extraBrowsers = new Set();
|
|
97
|
+
#contextBrowsers = new Map();
|
|
20
98
|
#page;
|
|
21
99
|
#startedAt;
|
|
22
100
|
#everStarted = false;
|
|
23
101
|
#navigationError;
|
|
24
|
-
#flowQueue = Promise.resolve();
|
|
102
|
+
#flowQueue = Promise.resolve();
|
|
103
|
+
#configurationKey;
|
|
104
|
+
#lastConfiguration = {};
|
|
25
105
|
|
|
26
106
|
constructor(settings) {
|
|
27
107
|
this.#settings = settings;
|
|
@@ -47,12 +127,17 @@ class BrowserPool {
|
|
|
47
127
|
}
|
|
48
128
|
}
|
|
49
129
|
|
|
50
|
-
async getPage() {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
130
|
+
async getPage(configuration) {
|
|
131
|
+
if (configuration === undefined) configuration = this.#lastConfiguration;
|
|
132
|
+
else this.#lastConfiguration = configuration;
|
|
133
|
+
const configurationKey = JSON.stringify(configuration);
|
|
134
|
+
if (this.#contextPromise && this.#configurationKey !== configurationKey) await this.close();
|
|
135
|
+
let isOpening = !this.#contextPromise;
|
|
136
|
+
if (isOpening) {
|
|
137
|
+
this.#everStarted = true;
|
|
138
|
+
this.#startedAt = performance.now();
|
|
139
|
+
this.#configurationKey = configurationKey;
|
|
140
|
+
this.#contextPromise = this.#open(configuration);
|
|
56
141
|
}
|
|
57
142
|
try {
|
|
58
143
|
await this.#contextPromise;
|
|
@@ -64,7 +149,8 @@ class BrowserPool {
|
|
|
64
149
|
if (!isOpening) {
|
|
65
150
|
isOpening = true;
|
|
66
151
|
this.#startedAt = performance.now();
|
|
67
|
-
this.#
|
|
152
|
+
this.#configurationKey = configurationKey;
|
|
153
|
+
this.#contextPromise = this.#open(configuration);
|
|
68
154
|
await this.#contextPromise;
|
|
69
155
|
} else {
|
|
70
156
|
throw error;
|
|
@@ -93,18 +179,61 @@ class BrowserPool {
|
|
|
93
179
|
}
|
|
94
180
|
}
|
|
95
181
|
|
|
96
|
-
async restart() {
|
|
182
|
+
async restart() {
|
|
97
183
|
await this.close();
|
|
98
184
|
this.#everStarted = true;
|
|
99
185
|
this.#startedAt = performance.now();
|
|
100
|
-
this.#contextPromise = this.#open();
|
|
186
|
+
this.#contextPromise = this.#open(this.#lastConfiguration);
|
|
101
187
|
try {
|
|
102
188
|
await this.#contextPromise;
|
|
103
189
|
return { status: "connected", connected: true, recovered: true, restarted: true, current_url: this.#page.url() };
|
|
104
190
|
} catch (error) {
|
|
105
191
|
return { status: "disconnected", connected: false, restarted: true, reason: error?.message || error?.name || "browser unavailable" };
|
|
106
192
|
}
|
|
107
|
-
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async replaceClosedPage(page) {
|
|
196
|
+
if (this.#page !== page || !this.#context) return;
|
|
197
|
+
this.#page = this.#context.pages()[0] || await this.#context.newPage();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async createIsolatedContext(contextOptions = {}, launchArgs = []) {
|
|
201
|
+
let browser = this.#browser;
|
|
202
|
+
let ownsBrowser = false;
|
|
203
|
+
if (!browser) {
|
|
204
|
+
browser = await chromium.launch({
|
|
205
|
+
channel: this.#settings.browser.channel,
|
|
206
|
+
headless: this.#settings.browser.mode !== "computer",
|
|
207
|
+
args: launchArgs,
|
|
208
|
+
});
|
|
209
|
+
ownsBrowser = true;
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
const context = await browser.newContext(contextOptions);
|
|
213
|
+
this.#extraContexts.add(context);
|
|
214
|
+
this.#contextBrowsers.set(context, { browser, ownsBrowser });
|
|
215
|
+
context.once("close", () => this.#extraContexts.delete(context));
|
|
216
|
+
if (ownsBrowser) {
|
|
217
|
+
this.#extraBrowsers.add(browser);
|
|
218
|
+
browser.once("disconnected", () => this.#extraBrowsers.delete(browser));
|
|
219
|
+
}
|
|
220
|
+
return context;
|
|
221
|
+
} catch (error) {
|
|
222
|
+
if (ownsBrowser) await browser.close().catch(() => {});
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async releaseContext(context) {
|
|
228
|
+
const ownership = this.#contextBrowsers.get(context);
|
|
229
|
+
await context?.close().catch(() => {});
|
|
230
|
+
this.#extraContexts.delete(context);
|
|
231
|
+
this.#contextBrowsers.delete(context);
|
|
232
|
+
if (ownership?.ownsBrowser) {
|
|
233
|
+
await ownership.browser.close().catch(() => {});
|
|
234
|
+
this.#extraBrowsers.delete(ownership.browser);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
108
237
|
|
|
109
238
|
#watchContext(context, browser) {
|
|
110
239
|
context.once("close", () => {
|
|
@@ -115,19 +244,22 @@ class BrowserPool {
|
|
|
115
244
|
});
|
|
116
245
|
}
|
|
117
246
|
|
|
118
|
-
async #open() {
|
|
119
|
-
const { mode, channel
|
|
247
|
+
async #open({ contextOptions = {}, launchArgs = [], profileDir: configuredProfileDir } = {}) {
|
|
248
|
+
const { mode, channel } = this.#settings.browser;
|
|
249
|
+
const profileDir = configuredProfileDir || this.#settings.browser.profileDir;
|
|
120
250
|
try {
|
|
121
251
|
if (mode === "computer") {
|
|
122
|
-
this.#context = await chromium.launchPersistentContext(profileDir, {
|
|
123
|
-
channel,
|
|
124
|
-
headless: false,
|
|
125
|
-
|
|
252
|
+
this.#context = await chromium.launchPersistentContext(profileDir, {
|
|
253
|
+
channel,
|
|
254
|
+
headless: false,
|
|
255
|
+
args: launchArgs,
|
|
256
|
+
...contextOptions,
|
|
257
|
+
});
|
|
126
258
|
this.#watchContext(this.#context);
|
|
127
259
|
this.#page = this.#context.pages()[0] || await this.#context.newPage();
|
|
128
260
|
} else {
|
|
129
|
-
this.#browser = await chromium.launch({ channel, headless: true });
|
|
130
|
-
this.#context = await this.#browser.newContext();
|
|
261
|
+
this.#browser = await chromium.launch({ channel, headless: true, args: launchArgs });
|
|
262
|
+
this.#context = await this.#browser.newContext(contextOptions);
|
|
131
263
|
this.#watchContext(this.#context, this.#browser);
|
|
132
264
|
this.#page = await this.#context.newPage();
|
|
133
265
|
}
|
|
@@ -144,14 +276,20 @@ class BrowserPool {
|
|
|
144
276
|
}
|
|
145
277
|
}
|
|
146
278
|
|
|
147
|
-
async close() {
|
|
279
|
+
async close() {
|
|
280
|
+
await Promise.all([...this.#extraContexts].map((context) => this.releaseContext(context)));
|
|
281
|
+
await Promise.all([...this.#extraBrowsers].map((browser) => browser.close().catch(() => {})));
|
|
282
|
+
this.#extraContexts.clear();
|
|
283
|
+
this.#extraBrowsers.clear();
|
|
284
|
+
this.#contextBrowsers.clear();
|
|
148
285
|
if (this.#context) await this.#context.close().catch(() => {});
|
|
149
286
|
if (this.#browser) await this.#browser.close().catch(() => {});
|
|
150
287
|
this.#context = undefined;
|
|
151
288
|
this.#browser = undefined;
|
|
152
289
|
this.#page = undefined;
|
|
153
|
-
this.#contextPromise = undefined;
|
|
154
|
-
this.#navigationError = undefined;
|
|
290
|
+
this.#contextPromise = undefined;
|
|
291
|
+
this.#navigationError = undefined;
|
|
292
|
+
this.#configurationKey = undefined;
|
|
155
293
|
}
|
|
156
294
|
}
|
|
157
295
|
|
|
@@ -164,11 +302,46 @@ function toolError(error, settings) {
|
|
|
164
302
|
return { isError: true, content: [{ type: "text", text: `[jev-browser ${packageManifest.version}] ${message}` }] };
|
|
165
303
|
}
|
|
166
304
|
|
|
167
|
-
function
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
305
|
+
export function fitToolResponse(value, maxBytes) {
|
|
306
|
+
const clone = structuredClone(value && typeof value === "object" ? value : { result: value });
|
|
307
|
+
const measure = () => Buffer.byteLength(JSON.stringify(clone), "utf8");
|
|
308
|
+
if (measure() <= maxBytes) return clone;
|
|
309
|
+
clone.output_truncated = true;
|
|
310
|
+
delete clone.unnamed_controls;
|
|
311
|
+
if (typeof clone.final_snapshot === "string") clone.final_snapshot = "[snapshot omitted to fit the configured MCP response limit]";
|
|
312
|
+
for (const key of ["unnamed_controls_initial", "unnamed_controls_final"]) {
|
|
313
|
+
if (Array.isArray(clone[key])) clone[key] = clone[key].map(({ role, index, nearest_label }) => ({ role, index, nearest_label }));
|
|
314
|
+
}
|
|
315
|
+
for (const key of ["console_errors", "network_failures", "warnings"]) {
|
|
316
|
+
if (Array.isArray(clone[key])) clone[key] = clone[key].slice(-10);
|
|
317
|
+
}
|
|
318
|
+
for (const step of clone.steps || []) {
|
|
319
|
+
delete step.html;
|
|
320
|
+
if (typeof step.error === "string") step.error = step.error.slice(0, 300);
|
|
321
|
+
}
|
|
322
|
+
while (measure() > maxBytes) {
|
|
323
|
+
if (Array.isArray(clone.steps) && clone.steps.length > 4) clone.steps.splice(1, clone.steps.length - 4);
|
|
324
|
+
else if (Array.isArray(clone.network_failures) && clone.network_failures.length) clone.network_failures.shift();
|
|
325
|
+
else if (Array.isArray(clone.console_errors) && clone.console_errors.length) clone.console_errors.shift();
|
|
326
|
+
else if (Array.isArray(clone.warnings) && clone.warnings.length) clone.warnings.shift();
|
|
327
|
+
else break;
|
|
328
|
+
}
|
|
329
|
+
if (measure() <= maxBytes) return clone;
|
|
330
|
+
return {
|
|
331
|
+
...(clone.server_version ? { server_version: clone.server_version } : {}),
|
|
332
|
+
status: clone.status || "failed",
|
|
333
|
+
output_truncated: true,
|
|
334
|
+
...(clone.reason ? { reason: String(clone.reason).slice(0, 300) } : {}),
|
|
335
|
+
...(Number.isSafeInteger(clone.steps_executed) ? { steps_executed: clone.steps_executed } : {}),
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function toolText(value, maxBytes) {
|
|
340
|
+
const versioned = value && typeof value === "object" && !Array.isArray(value)
|
|
341
|
+
? { ...value, server_version: packageManifest.version }
|
|
342
|
+
: { result: value, server_version: packageManifest.version };
|
|
343
|
+
const bounded = maxBytes ? fitToolResponse(versioned, maxBytes) : versioned;
|
|
344
|
+
return { content: [{ type: "text", text: JSON.stringify(bounded) }] };
|
|
172
345
|
}
|
|
173
346
|
|
|
174
347
|
export function profileOwnerPidFromProcessList(processes, profileDir, platform = process.platform) {
|
|
@@ -242,8 +415,33 @@ export function createJevBrowserServer({ env = process.env, configPath, fetchImp
|
|
|
242
415
|
description: "Check the managed browser connection; set restart=true to close and restart only the browser session owned by this MCP process.",
|
|
243
416
|
inputSchema: { restart: z.boolean().optional() },
|
|
244
417
|
},
|
|
245
|
-
async ({ restart = false } = {}) =>
|
|
246
|
-
|
|
418
|
+
async ({ restart = false } = {}) => {
|
|
419
|
+
const health = restart ? await browserPool.restart() : await browserPool.health();
|
|
420
|
+
return toolText({
|
|
421
|
+
...health,
|
|
422
|
+
server_version: packageManifest.version,
|
|
423
|
+
browser_mode: settings.browser.mode,
|
|
424
|
+
session_id: settings.browser.sessionId,
|
|
425
|
+
capabilities: {
|
|
426
|
+
actions: Object.keys(describeBrowserActions().actions),
|
|
427
|
+
options: describeBrowserActions().options,
|
|
428
|
+
media_permissions: ["microphone", "geolocation"],
|
|
429
|
+
fake_media: "WAV audio input",
|
|
430
|
+
storage_state: "named account snapshots",
|
|
431
|
+
contexts: ["current", "new isolated context"],
|
|
432
|
+
},
|
|
433
|
+
}, settings.browser.maxToolResponseBytes);
|
|
434
|
+
},
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
server.registerTool(
|
|
438
|
+
"describe_actions",
|
|
439
|
+
{
|
|
440
|
+
description: "Return the live JSON contract for every supported Jev Browser MCP action, option, alias, timeout and placeholder rule.",
|
|
441
|
+
inputSchema: {},
|
|
442
|
+
},
|
|
443
|
+
async () => toolText({ server_version: packageManifest.version, ...describeBrowserActions() }, settings.browser.maxToolResponseBytes),
|
|
444
|
+
);
|
|
247
445
|
|
|
248
446
|
server.registerTool(
|
|
249
447
|
"choose_next_action",
|
|
@@ -259,29 +457,32 @@ export function createJevBrowserServer({ env = process.env, configPath, fetchImp
|
|
|
259
457
|
},
|
|
260
458
|
async ({ flow, page_snapshot: pageSnapshot, actions, completed_steps: completedSteps, local_only: localOnly }) => {
|
|
261
459
|
try {
|
|
262
|
-
return toolText(
|
|
460
|
+
return toolText(
|
|
461
|
+
await chooseNextAction({ flow, pageSnapshot, actions, completedSteps, localOnly, settings, fetchImpl }),
|
|
462
|
+
settings.browser.maxToolResponseBytes,
|
|
463
|
+
);
|
|
263
464
|
} catch (error) {
|
|
264
465
|
return toolError(error, settings);
|
|
265
466
|
}
|
|
266
467
|
},
|
|
267
468
|
);
|
|
268
469
|
|
|
269
|
-
server.registerTool(
|
|
270
|
-
"run_browser_flow",
|
|
271
|
-
{
|
|
272
|
-
description: "Run a bounded screen flow in
|
|
273
|
-
inputSchema: {
|
|
274
|
-
flow: z.string(),
|
|
275
|
-
initial_url: z.string(),
|
|
276
|
-
expected_outcome:
|
|
277
|
-
candidate_plans: z.record(z.string(),
|
|
278
|
-
params: z.record(z.string(), z.unknown()).optional(),
|
|
279
|
-
options:
|
|
280
|
-
},
|
|
470
|
+
server.registerTool(
|
|
471
|
+
"run_browser_flow",
|
|
472
|
+
{
|
|
473
|
+
description: "Run a bounded screen flow in an isolated per-server browser profile. Continues the current SPA page by default. Supports media fakes, tabs and contexts, read-only page inspection, conditional plans, HTTP/network/WebSocket assertions, named storage state, guarded mutation confirmation, per-step timeouts and evidence. expected_outcome accepts descriptive text or structured text/request assertions. Use describe_actions for required and optional fields and the exact options contract. Jev chooses among eligible supplied plans; a single plan skips the decision call when fast_path is enabled.",
|
|
474
|
+
inputSchema: {
|
|
475
|
+
flow: z.string(),
|
|
476
|
+
initial_url: z.string().optional(),
|
|
477
|
+
expected_outcome: expectedOutcomeSchema.optional(),
|
|
478
|
+
candidate_plans: z.record(z.string(), candidatePlanSchema),
|
|
479
|
+
params: z.record(z.string(), z.unknown()).optional(),
|
|
480
|
+
options: browserOptionsSchema.optional(),
|
|
481
|
+
},
|
|
281
482
|
},
|
|
282
483
|
async ({ flow, initial_url: initialUrl, expected_outcome: expectedOutcome, candidate_plans: candidatePlans, params, options }) => {
|
|
283
484
|
try {
|
|
284
|
-
return toolText(await browserPool.runFlow(async () => {
|
|
485
|
+
return toolText(await browserPool.runFlow(async () => {
|
|
285
486
|
const args = {
|
|
286
487
|
flow,
|
|
287
488
|
initialUrl,
|
|
@@ -302,7 +503,7 @@ export function createJevBrowserServer({ env = process.env, configPath, fetchImp
|
|
|
302
503
|
if (!recovery.connected) return { ...first, browser_restart: recovery, retry_count: 0 };
|
|
303
504
|
const retry = await executeBrowserFlow({ ...args, options: { ...options, reuse_page: false } });
|
|
304
505
|
return { ...retry, browser_restart: recovery, retry_count: 1 };
|
|
305
|
-
}));
|
|
506
|
+
}), settings.browser.maxToolResponseBytes);
|
|
306
507
|
} catch (error) {
|
|
307
508
|
return toolError(error, settings);
|
|
308
509
|
}
|