chrometools-mcp 3.4.0 → 3.4.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
@@ -2,6 +2,13 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [3.4.1] - 2026-02-12
6
+
7
+ ### Fixed
8
+ - **Extension "not connected" after install** — Bridge connection now auto-retries after Chrome launches (previously MCP exhausted all reconnect attempts before Chrome even started)
9
+ - **Bridge auto-install on first run** — Native Messaging Host is automatically registered on MCP startup if not yet installed, eliminating the need to manually run `--install-bridge`
10
+ - **Browser autocomplete corrupting text input** — Temporarily suppresses autocomplete during typing to prevent stale data from overwriting input fields
11
+
5
12
  ## [3.4.0] - 2026-02-08
6
13
 
7
14
  ### Added
@@ -156,6 +156,33 @@ function scheduleReconnect() {
156
156
  }, RECONNECT_DELAY);
157
157
  }
158
158
 
159
+ /**
160
+ * Reset reconnect state and retry connection to Bridge.
161
+ * Call this after Chrome launches (bridge-service may have just started).
162
+ */
163
+ export async function retryBridgeConnection() {
164
+ if (isConnected && ws?.readyState === WebSocket.OPEN) {
165
+ return true; // Already connected
166
+ }
167
+
168
+ // Clean up any stale connection
169
+ if (ws) {
170
+ try { ws.close(); } catch (e) {}
171
+ ws = null;
172
+ }
173
+ isConnected = false;
174
+
175
+ // Reset reconnect counter so we get fresh attempts
176
+ reconnectAttempts = 0;
177
+ if (reconnectTimer) {
178
+ clearTimeout(reconnectTimer);
179
+ reconnectTimer = null;
180
+ }
181
+
182
+ logToFile('retryBridgeConnection: resetting and reconnecting');
183
+ return await connectToBridge();
184
+ }
185
+
159
186
  /**
160
187
  * Disconnect from Bridge
161
188
  */
@@ -23,6 +23,9 @@ let chromeProcess = null;
23
23
  // Track if we connected to existing Chrome (extension won't be auto-loaded)
24
24
  let connectedToExistingChrome = false;
25
25
 
26
+ // Ensure bridge reconnect is only triggered once per Chrome launch
27
+ let bridgeReconnectScheduled = false;
28
+
26
29
  // Track pages we've already seen to avoid double-handling
27
30
  const knownTargets = new WeakSet();
28
31
 
@@ -118,6 +121,9 @@ export async function getBrowser() {
118
121
  // Set up new tab tracking
119
122
  setupNewTabTracking(browser);
120
123
 
124
+ // Existing Chrome may already have bridge running — try to connect
125
+ scheduleBridgeReconnect();
126
+
121
127
  return browser;
122
128
  } catch (connectError) {
123
129
  debugLog("No existing Chrome found, launching new instance...");
@@ -173,6 +179,10 @@ export async function getBrowser() {
173
179
  // Set up new tab tracking
174
180
  setupNewTabTracking(browser);
175
181
 
182
+ // Extension needs time to load and start the bridge service.
183
+ // Schedule bridge reconnection after Chrome is ready.
184
+ scheduleBridgeReconnect();
185
+
176
186
  return browser;
177
187
  } catch (error) {
178
188
  // Check if it's a display-related error in WSL
@@ -221,6 +231,46 @@ This requires an X server to display the browser GUI.
221
231
  return await browserPromise;
222
232
  }
223
233
 
234
+ /**
235
+ * Schedule bridge reconnection after Chrome launches.
236
+ * Extension needs ~2-3s to load and start the bridge service via Native Messaging.
237
+ * Retries a few times in case the bridge takes longer to start.
238
+ */
239
+ function scheduleBridgeReconnect() {
240
+ if (bridgeReconnectScheduled) return;
241
+ bridgeReconnectScheduled = true;
242
+
243
+ const delays = [2000, 3000, 5000]; // retry schedule in ms
244
+ let attempt = 0;
245
+
246
+ async function tryConnect() {
247
+ try {
248
+ const { retryBridgeConnection, isBridgeConnected } = await import('../bridge/bridge-client.js');
249
+ if (isBridgeConnected()) {
250
+ debugLog('Bridge already connected, skipping reconnect');
251
+ return;
252
+ }
253
+ debugLog(`Bridge reconnect attempt ${attempt + 1}/${delays.length}`);
254
+ const connected = await retryBridgeConnection();
255
+ if (connected) {
256
+ console.error('[chrometools-mcp] Bridge connected after Chrome launch');
257
+ return;
258
+ }
259
+ } catch (e) {
260
+ debugLog('Bridge reconnect error:', e.message);
261
+ }
262
+
263
+ attempt++;
264
+ if (attempt < delays.length) {
265
+ setTimeout(tryConnect, delays[attempt]);
266
+ } else {
267
+ console.error('[chrometools-mcp] Bridge not available after Chrome launch. Run: npx chrometools-mcp --install-bridge');
268
+ }
269
+ }
270
+
271
+ setTimeout(tryConnect, delays[0]);
272
+ }
273
+
224
274
  /**
225
275
  * Setup tracking for new tabs opened via window.open, target="_blank", etc.
226
276
  * @param {Browser} browser - Puppeteer browser instance
package/index.js CHANGED
@@ -3998,6 +3998,17 @@ async function main() {
3998
3998
  console.error("[chrometools-mcp] GUI mode requires X server (DISPLAY=" + (process.env.DISPLAY || "not set") + ")");
3999
3999
  }
4000
4000
 
4001
+ // Auto-install bridge if not yet registered (required for extension <-> MCP communication)
4002
+ try {
4003
+ const { isBridgeInstalled, installBridge } = await import('./bridge/install.js');
4004
+ if (!isBridgeInstalled()) {
4005
+ console.error('[chrometools-mcp] Bridge not installed. Auto-installing...');
4006
+ await installBridge({ silent: true });
4007
+ }
4008
+ } catch (e) {
4009
+ console.error('[chrometools-mcp] Bridge auto-install failed:', e.message);
4010
+ }
4011
+
4001
4012
  // Connect to Bridge Service (if running)
4002
4013
  await startWebSocketServer();
4003
4014
 
@@ -31,52 +31,73 @@ export class TextInputModel extends BaseInputModel {
31
31
  const { delay = 0, clearFirst = true } = options;
32
32
  const opTimeout = 5000; // 5s timeout per operation
33
33
 
34
- // Method 1: Try Puppeteer typing (works for most cases)
34
+ // Suppress browser autocomplete to prevent field corruption
35
+ // (autocomplete can fire between focus and typing, filling the field with stale data)
36
+ const savedAutocomplete = await this.element.evaluate(el => {
37
+ const prev = el.getAttribute('autocomplete');
38
+ el.setAttribute('autocomplete', 'new-password');
39
+ return prev;
40
+ });
41
+
35
42
  try {
36
- // Focus and clear using JS (most reliable)
37
- await withTimeout(
38
- () => this.element.evaluate((el, shouldClear) => {
39
- el.focus();
40
- el.click();
41
- if (shouldClear) {
42
- el.select(); // Select all text
43
- }
44
- }, clearFirst),
45
- opTimeout,
46
- 'focus-and-select'
47
- );
43
+ // Method 1: Try Puppeteer typing (works for most cases)
44
+ try {
45
+ // Focus and clear using JS (most reliable)
46
+ await withTimeout(
47
+ () => this.element.evaluate((el, shouldClear) => {
48
+ el.focus();
49
+ el.click();
50
+ if (shouldClear) {
51
+ el.value = '';
52
+ el.dispatchEvent(new Event('input', { bubbles: true }));
53
+ el.dispatchEvent(new Event('change', { bubbles: true }));
54
+ }
55
+ }, clearFirst),
56
+ opTimeout,
57
+ 'focus-and-clear'
58
+ );
48
59
 
49
- // Small delay to ensure focus is established
50
- await new Promise(resolve => setTimeout(resolve, 50));
60
+ // Small delay to ensure focus is established
61
+ await new Promise(resolve => setTimeout(resolve, 50));
51
62
 
52
- // Type the new value
53
- const typeTimeout = Math.max(opTimeout, value.length * delay + 5000);
54
- await withTimeout(
55
- () => this.element.type(value, { delay }),
56
- typeTimeout,
57
- 'type'
58
- );
63
+ // Type the new value
64
+ const typeTimeout = Math.max(opTimeout, value.length * delay + 5000);
65
+ await withTimeout(
66
+ () => this.element.type(value, { delay }),
67
+ typeTimeout,
68
+ 'type'
69
+ );
59
70
 
60
- // Verify the value was set
61
- const actualValue = await this.element.evaluate(el => el.value);
62
- if (actualValue.includes(value)) {
63
- return; // Success
71
+ // Verify the value was set (exact match to catch autocomplete corruption)
72
+ const actualValue = await this.element.evaluate(el => el.value);
73
+ if (actualValue === value) {
74
+ return; // Success
75
+ }
76
+ } catch (e) {
77
+ // Fall through to JS method
64
78
  }
65
- } catch (e) {
66
- // Fall through to JS method
67
- }
68
79
 
69
- // Method 2: Fallback to direct JS value setting
70
- await withTimeout(
71
- () => this.element.evaluate((el, newValue) => {
72
- el.focus();
73
- el.value = newValue;
74
- el.dispatchEvent(new Event('input', { bubbles: true }));
75
- el.dispatchEvent(new Event('change', { bubbles: true }));
76
- }, value),
77
- opTimeout,
78
- 'js-set-value'
79
- );
80
+ // Method 2: Fallback to direct JS value setting
81
+ await withTimeout(
82
+ () => this.element.evaluate((el, newValue) => {
83
+ el.focus();
84
+ el.value = newValue;
85
+ el.dispatchEvent(new Event('input', { bubbles: true }));
86
+ el.dispatchEvent(new Event('change', { bubbles: true }));
87
+ }, value),
88
+ opTimeout,
89
+ 'js-set-value'
90
+ );
91
+ } finally {
92
+ // Restore original autocomplete attribute
93
+ await this.element.evaluate((el, orig) => {
94
+ if (orig === null) {
95
+ el.removeAttribute('autocomplete');
96
+ } else {
97
+ el.setAttribute('autocomplete', orig);
98
+ }
99
+ }, savedAutocomplete).catch(() => {});
100
+ }
80
101
  }
81
102
 
82
103
  getActionDescription(value, identifier) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrometools-mcp",
3
- "version": "3.4.0",
3
+ "version": "3.4.1",
4
4
  "description": "MCP (Model Context Protocol) server for Chrome automation using Puppeteer. Persistent browser sessions, UI framework detection (MUI, Ant Design, etc.), Page Object support, visual testing, Figma comparison. Works seamlessly in WSL, Linux, macOS, and Windows.",
5
5
  "type": "module",
6
6
  "main": "index.js",