@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.
@@ -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
- let isOpening = !this.#contextPromise;
52
- if (isOpening) {
53
- this.#everStarted = true;
54
- this.#startedAt = performance.now();
55
- this.#contextPromise = this.#open();
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.#contextPromise = this.#open();
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 toolText(value) {
168
- const versioned = value && typeof value === "object" && !Array.isArray(value)
169
- ? { ...value, server_version: packageManifest.version }
170
- : { result: value, server_version: packageManifest.version };
171
- return { content: [{ type: "text", text: JSON.stringify(versioned) }] };
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(restart ? await browserPool.restart() : await browserPool.health()),
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(await chooseNextAction({ flow, pageSnapshot, actions, completedSteps, localOnly, settings, fetchImpl }));
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegosouzacdv/jev-browser-mcp",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "Portable MCP server for bounded Playwright screen flows selected by Jev",
5
5
  "license": "MIT",
6
6
  "repository": {