@diegosouzacdv/jev-browser-mcp 0.6.1 → 0.6.2
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 +126 -55
- package/config/ui-testing.json +10 -2
- package/docs/jev-browser-mcp.md +126 -55
- package/mcp_servers/jev-browser-npm/src/accessibility.mjs +18 -9
- package/mcp_servers/jev-browser-npm/src/config.mjs +28 -7
- package/mcp_servers/jev-browser-npm/src/flow.mjs +736 -314
- package/mcp_servers/jev-browser-npm/src/server.mjs +82 -28
- package/package.json +1 -1
|
@@ -21,7 +21,8 @@ class BrowserPool {
|
|
|
21
21
|
#startedAt;
|
|
22
22
|
#everStarted = false;
|
|
23
23
|
#navigationError;
|
|
24
|
-
#flowQueue = Promise.resolve();
|
|
24
|
+
#flowQueue = Promise.resolve();
|
|
25
|
+
#configurationKey;
|
|
25
26
|
|
|
26
27
|
constructor(settings) {
|
|
27
28
|
this.#settings = settings;
|
|
@@ -47,12 +48,15 @@ class BrowserPool {
|
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
async getPage() {
|
|
51
|
-
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
this.#
|
|
51
|
+
async getPage(configuration = {}) {
|
|
52
|
+
const configurationKey = JSON.stringify(configuration);
|
|
53
|
+
if (this.#contextPromise && this.#configurationKey !== configurationKey) await this.close();
|
|
54
|
+
let isOpening = !this.#contextPromise;
|
|
55
|
+
if (isOpening) {
|
|
56
|
+
this.#everStarted = true;
|
|
57
|
+
this.#startedAt = performance.now();
|
|
58
|
+
this.#configurationKey = configurationKey;
|
|
59
|
+
this.#contextPromise = this.#open(configuration);
|
|
56
60
|
}
|
|
57
61
|
try {
|
|
58
62
|
await this.#contextPromise;
|
|
@@ -64,7 +68,8 @@ class BrowserPool {
|
|
|
64
68
|
if (!isOpening) {
|
|
65
69
|
isOpening = true;
|
|
66
70
|
this.#startedAt = performance.now();
|
|
67
|
-
this.#
|
|
71
|
+
this.#configurationKey = configurationKey;
|
|
72
|
+
this.#contextPromise = this.#open(configuration);
|
|
68
73
|
await this.#contextPromise;
|
|
69
74
|
} else {
|
|
70
75
|
throw error;
|
|
@@ -93,7 +98,7 @@ class BrowserPool {
|
|
|
93
98
|
}
|
|
94
99
|
}
|
|
95
100
|
|
|
96
|
-
async restart() {
|
|
101
|
+
async restart() {
|
|
97
102
|
await this.close();
|
|
98
103
|
this.#everStarted = true;
|
|
99
104
|
this.#startedAt = performance.now();
|
|
@@ -104,7 +109,12 @@ class BrowserPool {
|
|
|
104
109
|
} catch (error) {
|
|
105
110
|
return { status: "disconnected", connected: false, restarted: true, reason: error?.message || error?.name || "browser unavailable" };
|
|
106
111
|
}
|
|
107
|
-
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async replaceClosedPage(page) {
|
|
115
|
+
if (this.#page !== page || !this.#context) return;
|
|
116
|
+
this.#page = this.#context.pages()[0] || await this.#context.newPage();
|
|
117
|
+
}
|
|
108
118
|
|
|
109
119
|
#watchContext(context, browser) {
|
|
110
120
|
context.once("close", () => {
|
|
@@ -115,19 +125,21 @@ class BrowserPool {
|
|
|
115
125
|
});
|
|
116
126
|
}
|
|
117
127
|
|
|
118
|
-
async #open() {
|
|
128
|
+
async #open({ contextOptions = {}, launchArgs = [] } = {}) {
|
|
119
129
|
const { mode, channel, profileDir } = this.#settings.browser;
|
|
120
130
|
try {
|
|
121
131
|
if (mode === "computer") {
|
|
122
|
-
this.#context = await chromium.launchPersistentContext(profileDir, {
|
|
123
|
-
channel,
|
|
124
|
-
headless: false,
|
|
125
|
-
|
|
132
|
+
this.#context = await chromium.launchPersistentContext(profileDir, {
|
|
133
|
+
channel,
|
|
134
|
+
headless: false,
|
|
135
|
+
args: launchArgs,
|
|
136
|
+
...contextOptions,
|
|
137
|
+
});
|
|
126
138
|
this.#watchContext(this.#context);
|
|
127
139
|
this.#page = this.#context.pages()[0] || await this.#context.newPage();
|
|
128
140
|
} else {
|
|
129
|
-
this.#browser = await chromium.launch({ channel, headless: true });
|
|
130
|
-
this.#context = await this.#browser.newContext();
|
|
141
|
+
this.#browser = await chromium.launch({ channel, headless: true, args: launchArgs });
|
|
142
|
+
this.#context = await this.#browser.newContext(contextOptions);
|
|
131
143
|
this.#watchContext(this.#context, this.#browser);
|
|
132
144
|
this.#page = await this.#context.newPage();
|
|
133
145
|
}
|
|
@@ -150,8 +162,9 @@ class BrowserPool {
|
|
|
150
162
|
this.#context = undefined;
|
|
151
163
|
this.#browser = undefined;
|
|
152
164
|
this.#page = undefined;
|
|
153
|
-
this.#contextPromise = undefined;
|
|
154
|
-
this.#navigationError = undefined;
|
|
165
|
+
this.#contextPromise = undefined;
|
|
166
|
+
this.#navigationError = undefined;
|
|
167
|
+
this.#configurationKey = undefined;
|
|
155
168
|
}
|
|
156
169
|
}
|
|
157
170
|
|
|
@@ -164,11 +177,46 @@ function toolError(error, settings) {
|
|
|
164
177
|
return { isError: true, content: [{ type: "text", text: `[jev-browser ${packageManifest.version}] ${message}` }] };
|
|
165
178
|
}
|
|
166
179
|
|
|
167
|
-
function
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
180
|
+
export function fitToolResponse(value, maxBytes) {
|
|
181
|
+
const clone = structuredClone(value && typeof value === "object" ? value : { result: value });
|
|
182
|
+
const measure = () => Buffer.byteLength(JSON.stringify(clone), "utf8");
|
|
183
|
+
if (measure() <= maxBytes) return clone;
|
|
184
|
+
clone.output_truncated = true;
|
|
185
|
+
delete clone.unnamed_controls;
|
|
186
|
+
if (typeof clone.final_snapshot === "string") clone.final_snapshot = "[snapshot omitted to fit the configured MCP response limit]";
|
|
187
|
+
for (const key of ["unnamed_controls_initial", "unnamed_controls_final"]) {
|
|
188
|
+
if (Array.isArray(clone[key])) clone[key] = clone[key].map(({ role, index, nearest_label }) => ({ role, index, nearest_label }));
|
|
189
|
+
}
|
|
190
|
+
for (const key of ["console_errors", "network_failures", "warnings"]) {
|
|
191
|
+
if (Array.isArray(clone[key])) clone[key] = clone[key].slice(-10);
|
|
192
|
+
}
|
|
193
|
+
for (const step of clone.steps || []) {
|
|
194
|
+
delete step.html;
|
|
195
|
+
if (typeof step.error === "string") step.error = step.error.slice(0, 300);
|
|
196
|
+
}
|
|
197
|
+
while (measure() > maxBytes) {
|
|
198
|
+
if (Array.isArray(clone.steps) && clone.steps.length > 4) clone.steps.splice(1, clone.steps.length - 4);
|
|
199
|
+
else if (Array.isArray(clone.network_failures) && clone.network_failures.length) clone.network_failures.shift();
|
|
200
|
+
else if (Array.isArray(clone.console_errors) && clone.console_errors.length) clone.console_errors.shift();
|
|
201
|
+
else if (Array.isArray(clone.warnings) && clone.warnings.length) clone.warnings.shift();
|
|
202
|
+
else break;
|
|
203
|
+
}
|
|
204
|
+
if (measure() <= maxBytes) return clone;
|
|
205
|
+
return {
|
|
206
|
+
...(clone.server_version ? { server_version: clone.server_version } : {}),
|
|
207
|
+
status: clone.status || "failed",
|
|
208
|
+
output_truncated: true,
|
|
209
|
+
...(clone.reason ? { reason: String(clone.reason).slice(0, 300) } : {}),
|
|
210
|
+
...(Number.isSafeInteger(clone.steps_executed) ? { steps_executed: clone.steps_executed } : {}),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function toolText(value, maxBytes) {
|
|
215
|
+
const versioned = value && typeof value === "object" && !Array.isArray(value)
|
|
216
|
+
? { ...value, server_version: packageManifest.version }
|
|
217
|
+
: { result: value, server_version: packageManifest.version };
|
|
218
|
+
const bounded = maxBytes ? fitToolResponse(versioned, maxBytes) : versioned;
|
|
219
|
+
return { content: [{ type: "text", text: JSON.stringify(bounded) }] };
|
|
172
220
|
}
|
|
173
221
|
|
|
174
222
|
export function profileOwnerPidFromProcessList(processes, profileDir, platform = process.platform) {
|
|
@@ -242,7 +290,10 @@ export function createJevBrowserServer({ env = process.env, configPath, fetchImp
|
|
|
242
290
|
description: "Check the managed browser connection; set restart=true to close and restart only the browser session owned by this MCP process.",
|
|
243
291
|
inputSchema: { restart: z.boolean().optional() },
|
|
244
292
|
},
|
|
245
|
-
async ({ restart = false } = {}) => toolText(
|
|
293
|
+
async ({ restart = false } = {}) => toolText(
|
|
294
|
+
restart ? await browserPool.restart() : await browserPool.health(),
|
|
295
|
+
settings.browser.maxToolResponseBytes,
|
|
296
|
+
),
|
|
246
297
|
);
|
|
247
298
|
|
|
248
299
|
server.registerTool(
|
|
@@ -259,7 +310,10 @@ export function createJevBrowserServer({ env = process.env, configPath, fetchImp
|
|
|
259
310
|
},
|
|
260
311
|
async ({ flow, page_snapshot: pageSnapshot, actions, completed_steps: completedSteps, local_only: localOnly }) => {
|
|
261
312
|
try {
|
|
262
|
-
return toolText(
|
|
313
|
+
return toolText(
|
|
314
|
+
await chooseNextAction({ flow, pageSnapshot, actions, completedSteps, localOnly, settings, fetchImpl }),
|
|
315
|
+
settings.browser.maxToolResponseBytes,
|
|
316
|
+
);
|
|
263
317
|
} catch (error) {
|
|
264
318
|
return toolError(error, settings);
|
|
265
319
|
}
|
|
@@ -281,7 +335,7 @@ export function createJevBrowserServer({ env = process.env, configPath, fetchImp
|
|
|
281
335
|
},
|
|
282
336
|
async ({ flow, initial_url: initialUrl, expected_outcome: expectedOutcome, candidate_plans: candidatePlans, params, options }) => {
|
|
283
337
|
try {
|
|
284
|
-
return toolText(await browserPool.runFlow(async () => {
|
|
338
|
+
return toolText(await browserPool.runFlow(async () => {
|
|
285
339
|
const args = {
|
|
286
340
|
flow,
|
|
287
341
|
initialUrl,
|
|
@@ -302,7 +356,7 @@ export function createJevBrowserServer({ env = process.env, configPath, fetchImp
|
|
|
302
356
|
if (!recovery.connected) return { ...first, browser_restart: recovery, retry_count: 0 };
|
|
303
357
|
const retry = await executeBrowserFlow({ ...args, options: { ...options, reuse_page: false } });
|
|
304
358
|
return { ...retry, browser_restart: recovery, retry_count: 1 };
|
|
305
|
-
}));
|
|
359
|
+
}), settings.browser.maxToolResponseBytes);
|
|
306
360
|
} catch (error) {
|
|
307
361
|
return toolError(error, settings);
|
|
308
362
|
}
|