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 +7 -0
- package/bridge/bridge-client.js +27 -0
- package/browser/browser-manager.js +50 -0
- package/index.js +11 -0
- package/models/TextInputModel.js +61 -40
- package/package.json +1 -1
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
|
package/bridge/bridge-client.js
CHANGED
|
@@ -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
|
|
package/models/TextInputModel.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
el.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
50
|
-
|
|
60
|
+
// Small delay to ensure focus is established
|
|
61
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
51
62
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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.
|
|
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",
|