codex-control-chrome-mcp 1.3.0 → 1.3.1

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/CHANGELOG.md CHANGED
@@ -6,7 +6,11 @@ This project follows semantic versioning.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
- ## [1.3.0] - 2026-07-03
9
+ ## [1.3.1] - 2026-07-04
10
+
11
+ ### Fixed
12
+
13
+ - `chrome_evaluate`, `chrome_navigate`, and `chrome_screenshot` now self-heal a stale debugger attachment instead of dead-ending on `Debugger is not attached`. Each tool already attaches before sending CDP commands, but the extension tracks attachments in memory: when Chrome tears an attachment down out-of-band (a competing debugger client, DevTools, or a renderer/process swap), that flag can go stale, so a fresh `attach` short-circuits and reports success without actually re-attaching and the next command fails with `Debugger is not attached`. The tab then stayed wedged — `attach` kept reporting `ok` while every command failed — and only a new tab recovered. These tools now retry once on that specific error by issuing a `detach` (which clears the stale flag; it may itself report "not attached", which is expected and ignored) followed by a real `attach`, so a previously wedged tab recovers automatically. The self-heal wrapper is covered by regression tests. This addresses recovery in the bridge only; the underlying churn comes from multiple debugger clients contending for the same tab, so reducing competing browser-control stacks still helps.
10
14
 
11
15
  ### Changed
12
16
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-control-chrome-mcp",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "MCP server and native host bridge for controlling Chrome through the Codex Chrome extension.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/mcp-server.js CHANGED
@@ -229,15 +229,15 @@ function registerTools(server, runtime) {
229
229
  url: z.string().url(),
230
230
  },
231
231
  },
232
- async ({ tabId, url }) => {
233
- await runtime.request("attach", { tabId });
234
- await executeCdp(runtime, { tabId, method: "Page.enable", params: {} });
235
- return toolResult(await executeCdp(runtime, {
236
- tabId,
237
- method: "Page.navigate",
238
- params: { url },
239
- }));
240
- },
232
+ async ({ tabId, url }) =>
233
+ toolResult(await withReattach(runtime, tabId, async () => {
234
+ await executeCdp(runtime, { tabId, method: "Page.enable", params: {} });
235
+ return await executeCdp(runtime, {
236
+ tabId,
237
+ method: "Page.navigate",
238
+ params: { url },
239
+ });
240
+ })),
241
241
  );
242
242
 
243
243
  server.registerTool(
@@ -250,19 +250,19 @@ function registerTools(server, runtime) {
250
250
  awaitPromise: z.boolean().default(true),
251
251
  },
252
252
  },
253
- async ({ tabId, expression, awaitPromise }) => {
254
- await runtime.request("attach", { tabId });
255
- await executeCdp(runtime, { tabId, method: "Runtime.enable", params: {} });
256
- return toolResult(await executeCdp(runtime, {
257
- tabId,
258
- method: "Runtime.evaluate",
259
- params: {
260
- expression,
261
- awaitPromise,
262
- returnByValue: true,
263
- },
264
- }));
265
- },
253
+ async ({ tabId, expression, awaitPromise }) =>
254
+ toolResult(await withReattach(runtime, tabId, async () => {
255
+ await executeCdp(runtime, { tabId, method: "Runtime.enable", params: {} });
256
+ return await executeCdp(runtime, {
257
+ tabId,
258
+ method: "Runtime.evaluate",
259
+ params: {
260
+ expression,
261
+ awaitPromise,
262
+ returnByValue: true,
263
+ },
264
+ });
265
+ })),
266
266
  );
267
267
 
268
268
  server.registerTool(
@@ -274,16 +274,16 @@ function registerTools(server, runtime) {
274
274
  format: z.enum(["png", "jpeg", "webp"]).default("png"),
275
275
  },
276
276
  },
277
- async ({ tabId, format }) => {
278
- await runtime.request("attach", { tabId });
279
- await executeCdp(runtime, { tabId, method: "Page.enable", params: {} });
280
- return toolResult(await executeCdp(runtime, {
281
- tabId,
282
- method: "Page.captureScreenshot",
283
- params: { format, captureBeyondViewport: true },
284
- timeoutMs: 30_000,
285
- }));
286
- },
277
+ async ({ tabId, format }) =>
278
+ toolResult(await withReattach(runtime, tabId, async () => {
279
+ await executeCdp(runtime, { tabId, method: "Page.enable", params: {} });
280
+ return await executeCdp(runtime, {
281
+ tabId,
282
+ method: "Page.captureScreenshot",
283
+ params: { format, captureBeyondViewport: true },
284
+ timeoutMs: 30_000,
285
+ });
286
+ })),
287
287
  );
288
288
 
289
289
  server.registerTool(
@@ -319,6 +319,38 @@ function registerTools(server, runtime) {
319
319
  );
320
320
  }
321
321
 
322
+ /**
323
+ * Attach the debugger, run a CDP sequence, and self-heal a stale-attach state.
324
+ *
325
+ * The Codex Chrome extension tracks debugger attachments in memory. When Chrome
326
+ * tears an attachment down out-of-band (a competing debugger client, DevTools, a
327
+ * renderer/process swap), that flag can go stale: a fresh `attach` short-circuits
328
+ * and reports success without actually re-attaching, so the next CDP command
329
+ * fails with "Debugger is not attached". A `detach` clears the stale flag (it may
330
+ * itself throw "not attached" — expected, and ignored), after which `attach`
331
+ * re-attaches for real. We retry the sequence once on that specific error.
332
+ */
333
+ export async function withReattach(runtime, tabId, run) {
334
+ await runtime.request("attach", { tabId });
335
+ try {
336
+ return await run();
337
+ } catch (error) {
338
+ if (!isNotAttachedError(error)) throw error;
339
+ try {
340
+ await runtime.request("detach", { tabId });
341
+ } catch {
342
+ // Detach throws when the debugger is already gone; we only need its side
343
+ // effect of clearing the extension's stale attach flag.
344
+ }
345
+ await runtime.request("attach", { tabId });
346
+ return await run();
347
+ }
348
+ }
349
+
350
+ export function isNotAttachedError(error) {
351
+ return /not attached/i.test(error?.message ?? String(error ?? ""));
352
+ }
353
+
322
354
  async function executeCdp(runtime, { tabId, method, params = {}, timeoutMs }) {
323
355
  return await runtime.request(
324
356
  "executeCdp",