chrome-devtools-mcp-for-extension 0.13.0 → 0.14.0
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/build/src/McpContext.js
CHANGED
|
@@ -77,12 +77,74 @@ export class McpContext {
|
|
|
77
77
|
await context.#init();
|
|
78
78
|
return context;
|
|
79
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Reinitialize CDP protocol domains after reconnection
|
|
82
|
+
* This ensures proper CDP event handling after browser restart
|
|
83
|
+
*/
|
|
84
|
+
async reinitializeCDP() {
|
|
85
|
+
try {
|
|
86
|
+
// Get CDP client from the browser
|
|
87
|
+
const client = await this.browser._client();
|
|
88
|
+
if (!client) {
|
|
89
|
+
this.logger('Warning: Unable to get CDP client for reinitialization');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
// 1. Enable Target discovery and auto-attach
|
|
93
|
+
// This is critical for multi-target scenarios (iframes, workers, etc.)
|
|
94
|
+
try {
|
|
95
|
+
await client.send('Target.setDiscoverTargets', { discover: true });
|
|
96
|
+
this.logger('CDP: Target discovery enabled');
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
this.logger('Warning: Failed to enable target discovery:', err);
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
await client.send('Target.setAutoAttach', {
|
|
103
|
+
autoAttach: true,
|
|
104
|
+
waitForDebuggerOnStart: false,
|
|
105
|
+
flatten: true
|
|
106
|
+
});
|
|
107
|
+
this.logger('CDP: Auto-attach configured');
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
this.logger('Warning: Failed to configure auto-attach:', err);
|
|
111
|
+
}
|
|
112
|
+
// 2. Enable essential CDP domains for each page
|
|
113
|
+
const pages = await this.browser.pages();
|
|
114
|
+
for (const page of pages) {
|
|
115
|
+
try {
|
|
116
|
+
const pageClient = page._client();
|
|
117
|
+
if (pageClient) {
|
|
118
|
+
// Enable Network domain for request interception
|
|
119
|
+
await pageClient.send('Network.enable');
|
|
120
|
+
// Enable Runtime domain for console messages and exceptions
|
|
121
|
+
await pageClient.send('Runtime.enable');
|
|
122
|
+
// Enable Log domain for browser logs
|
|
123
|
+
await pageClient.send('Log.enable');
|
|
124
|
+
this.logger(`CDP domains enabled for page: ${page.url()}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
this.logger(`Warning: Failed to enable CDP domains for page ${page.url()}:`, err);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
this.logger('CDP protocol reinitialization completed');
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
this.logger('Error during CDP reinitialization:', err);
|
|
135
|
+
// Don't throw - this is a best-effort operation
|
|
136
|
+
// The browser should still be usable even if CDP setup partially fails
|
|
137
|
+
}
|
|
138
|
+
}
|
|
80
139
|
/**
|
|
81
140
|
* Update browser instance after reconnection
|
|
82
141
|
*/
|
|
83
142
|
async updateBrowser(newBrowser) {
|
|
84
143
|
this.browser = newBrowser;
|
|
85
144
|
this.connectionManager.setBrowser(newBrowser, async () => newBrowser);
|
|
145
|
+
// Reinitialize CDP protocol domains BEFORE collectors
|
|
146
|
+
// This ensures CDP events are properly set up
|
|
147
|
+
await this.reinitializeCDP();
|
|
86
148
|
// Reinitialize collectors for new browser
|
|
87
149
|
await this.#networkCollector.init();
|
|
88
150
|
await this.#consoleCollector.init();
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Copyright 2025 Google LLC
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
|
+
import { ProtocolError, TimeoutError } from 'puppeteer';
|
|
6
7
|
/**
|
|
7
8
|
* Default connection manager options
|
|
8
9
|
*/
|
|
@@ -13,25 +14,55 @@ const DEFAULT_OPTIONS = {
|
|
|
13
14
|
enableLogging: true,
|
|
14
15
|
onReconnect: undefined,
|
|
15
16
|
};
|
|
17
|
+
/**
|
|
18
|
+
* Connection state enum
|
|
19
|
+
*/
|
|
20
|
+
var ConnectionState;
|
|
21
|
+
(function (ConnectionState) {
|
|
22
|
+
ConnectionState["CONNECTED"] = "CONNECTED";
|
|
23
|
+
ConnectionState["RECONNECTING"] = "RECONNECTING";
|
|
24
|
+
ConnectionState["CLOSED"] = "CLOSED";
|
|
25
|
+
})(ConnectionState || (ConnectionState = {}));
|
|
16
26
|
/**
|
|
17
27
|
* Browser Connection Manager
|
|
18
28
|
*
|
|
19
|
-
* Provides automatic reconnection for CDP operations
|
|
29
|
+
* Provides automatic reconnection for CDP operations with:
|
|
30
|
+
* - Single-flight pattern to prevent concurrent reconnections
|
|
31
|
+
* - Event-driven detection via browser 'disconnected' event
|
|
32
|
+
* - State machine tracking (CONNECTED | RECONNECTING | CLOSED)
|
|
33
|
+
* - Exponential backoff with jitter to prevent thundering herd
|
|
20
34
|
*/
|
|
21
35
|
export class BrowserConnectionManager {
|
|
22
36
|
reconnectAttempts = 0;
|
|
23
37
|
options;
|
|
24
38
|
browser = null;
|
|
25
39
|
browserFactory = null;
|
|
40
|
+
/** Single-flight pattern: prevents concurrent reconnection attempts */
|
|
41
|
+
reconnectInFlight = null;
|
|
42
|
+
/** Shared reconnection sequence for multiple operations */
|
|
43
|
+
reconnectSequenceInFlight = null;
|
|
44
|
+
/** Current connection state */
|
|
45
|
+
state = ConnectionState.CLOSED;
|
|
26
46
|
constructor(options = {}) {
|
|
27
47
|
this.options = { ...DEFAULT_OPTIONS, ...options };
|
|
28
48
|
}
|
|
29
49
|
/**
|
|
30
50
|
* Set browser instance and factory for reconnection
|
|
51
|
+
*
|
|
52
|
+
* @param browser - Browser instance to manage
|
|
53
|
+
* @param factory - Factory function to create new browser instances on reconnection
|
|
31
54
|
*/
|
|
32
55
|
setBrowser(browser, factory) {
|
|
33
56
|
this.browser = browser;
|
|
34
57
|
this.browserFactory = factory;
|
|
58
|
+
this.state = ConnectionState.CONNECTED;
|
|
59
|
+
// Event-driven detection: hook into browser 'disconnected' event
|
|
60
|
+
this.browser.on('disconnected', () => {
|
|
61
|
+
this.log('Browser disconnected event fired');
|
|
62
|
+
this.setState(ConnectionState.CLOSED);
|
|
63
|
+
// Trigger immediate reconnection (will be handled by next operation)
|
|
64
|
+
});
|
|
65
|
+
this.log('Browser instance set, state: CONNECTED');
|
|
35
66
|
}
|
|
36
67
|
/**
|
|
37
68
|
* Execute an operation with automatic retry on CDP connection errors
|
|
@@ -49,23 +80,75 @@ export class BrowserConnectionManager {
|
|
|
49
80
|
}
|
|
50
81
|
}
|
|
51
82
|
/**
|
|
52
|
-
* Retry operation with exponential backoff
|
|
83
|
+
* Retry operation with exponential backoff and jitter
|
|
84
|
+
*
|
|
85
|
+
* Implements single-flight pattern at the sequence level:
|
|
86
|
+
* - Multiple concurrent operations share the same reconnection sequence
|
|
87
|
+
* - Only one reconnection sequence runs at a time
|
|
88
|
+
* - All waiting operations retry after the shared sequence completes
|
|
89
|
+
*
|
|
90
|
+
* Adds ±20% randomness to retry delays to prevent thundering herd problem.
|
|
91
|
+
*
|
|
92
|
+
* @param operation - Operation to retry
|
|
93
|
+
* @param operationName - Name of operation for logging
|
|
94
|
+
* @returns Result of operation
|
|
53
95
|
*/
|
|
54
96
|
async retryWithReconnect(operation, operationName) {
|
|
97
|
+
// Single-flight pattern: if a reconnection sequence is already running,
|
|
98
|
+
// wait for it to complete, then try the operation once
|
|
99
|
+
if (this.reconnectSequenceInFlight) {
|
|
100
|
+
this.log(`${operationName}: Waiting for ongoing reconnection sequence...`);
|
|
101
|
+
try {
|
|
102
|
+
await this.reconnectSequenceInFlight;
|
|
103
|
+
this.log(`${operationName}: Reconnection sequence completed, retrying operation...`);
|
|
104
|
+
return await operation();
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
// Reconnection sequence failed, propagate the error
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Start a new reconnection sequence
|
|
112
|
+
this.reconnectSequenceInFlight = this._runReconnectionSequence();
|
|
113
|
+
try {
|
|
114
|
+
await this.reconnectSequenceInFlight;
|
|
115
|
+
this.log(`${operationName}: Reconnection sequence completed, retrying operation...`);
|
|
116
|
+
return await operation();
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
// Clear the in-flight sequence
|
|
123
|
+
this.reconnectSequenceInFlight = null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Run a full reconnection sequence with exponential backoff
|
|
128
|
+
*
|
|
129
|
+
* This is the actual retry loop that multiple operations can share.
|
|
130
|
+
*
|
|
131
|
+
* @private
|
|
132
|
+
*/
|
|
133
|
+
async _runReconnectionSequence() {
|
|
55
134
|
const maxAttempts = this.options.maxReconnectAttempts ?? 3;
|
|
56
135
|
const initialDelay = this.options.initialRetryDelay ?? 1000;
|
|
57
136
|
const maxDelay = this.options.maxRetryDelay ?? 10000;
|
|
58
137
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
59
138
|
this.reconnectAttempts++;
|
|
60
139
|
const attemptNum = attempt + 1;
|
|
61
|
-
this.log(`Reconnect attempt ${attemptNum}/${maxAttempts}
|
|
140
|
+
this.log(`Reconnect attempt ${attemptNum}/${maxAttempts}...`);
|
|
62
141
|
// Exponential backoff with max delay
|
|
63
|
-
const
|
|
142
|
+
const baseDelay = Math.min(initialDelay * Math.pow(2, attempt), maxDelay);
|
|
143
|
+
// Add jitter: ±20% randomness to prevent thundering herd
|
|
144
|
+
const jitter = baseDelay * 0.2 * (Math.random() * 2 - 1);
|
|
145
|
+
const delay = Math.max(0, baseDelay + jitter);
|
|
146
|
+
this.log(`Waiting ${delay.toFixed(0)}ms before reconnect attempt...`);
|
|
64
147
|
await this.sleep(delay);
|
|
65
148
|
try {
|
|
66
149
|
await this.reconnectBrowser();
|
|
67
|
-
this.log(`Reconnection successful
|
|
68
|
-
return
|
|
150
|
+
this.log(`Reconnection successful`);
|
|
151
|
+
return; // Success!
|
|
69
152
|
}
|
|
70
153
|
catch (error) {
|
|
71
154
|
if (attempt === maxAttempts - 1) {
|
|
@@ -79,8 +162,19 @@ export class BrowserConnectionManager {
|
|
|
79
162
|
}
|
|
80
163
|
/**
|
|
81
164
|
* Check if error is a CDP connection error
|
|
165
|
+
*
|
|
166
|
+
* Uses type-safe error detection with instanceof checks and falls back
|
|
167
|
+
* to string matching for compatibility.
|
|
168
|
+
*
|
|
169
|
+
* @param error - Error to check
|
|
170
|
+
* @returns true if error is a CDP connection error
|
|
82
171
|
*/
|
|
83
172
|
isCDPConnectionError(error) {
|
|
173
|
+
// Type-safe detection using instanceof
|
|
174
|
+
if (error instanceof ProtocolError || error instanceof TimeoutError) {
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
// Fallback: string matching for error messages
|
|
84
178
|
const errorMessage = error?.message || '';
|
|
85
179
|
return (errorMessage.includes('Target closed') ||
|
|
86
180
|
errorMessage.includes('Protocol error') ||
|
|
@@ -89,12 +183,39 @@ export class BrowserConnectionManager {
|
|
|
89
183
|
errorMessage.includes('WebSocket is not open'));
|
|
90
184
|
}
|
|
91
185
|
/**
|
|
92
|
-
* Reconnect browser instance
|
|
186
|
+
* Reconnect browser instance with single-flight pattern
|
|
187
|
+
*
|
|
188
|
+
* Ensures only one reconnection attempt happens at a time.
|
|
189
|
+
* Multiple concurrent calls will wait for the same reconnection promise.
|
|
190
|
+
*
|
|
191
|
+
* @returns Promise that resolves when reconnection is complete
|
|
93
192
|
*/
|
|
94
193
|
async reconnectBrowser() {
|
|
194
|
+
// Single-flight pattern: return existing promise if reconnection is already in progress
|
|
195
|
+
if (this.reconnectInFlight) {
|
|
196
|
+
this.log('Reconnection already in progress, waiting...');
|
|
197
|
+
return this.reconnectInFlight;
|
|
198
|
+
}
|
|
199
|
+
// Create new reconnection promise
|
|
200
|
+
this.reconnectInFlight = this._doReconnect();
|
|
201
|
+
try {
|
|
202
|
+
await this.reconnectInFlight;
|
|
203
|
+
}
|
|
204
|
+
finally {
|
|
205
|
+
// Clear in-flight promise when complete
|
|
206
|
+
this.reconnectInFlight = null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Internal reconnection logic
|
|
211
|
+
*
|
|
212
|
+
* @private
|
|
213
|
+
*/
|
|
214
|
+
async _doReconnect() {
|
|
95
215
|
if (!this.browserFactory) {
|
|
96
216
|
throw new Error('Browser factory not set. Cannot reconnect.');
|
|
97
217
|
}
|
|
218
|
+
this.setState(ConnectionState.RECONNECTING);
|
|
98
219
|
try {
|
|
99
220
|
// Close old browser if still connected
|
|
100
221
|
if (this.browser?.isConnected()) {
|
|
@@ -109,6 +230,12 @@ export class BrowserConnectionManager {
|
|
|
109
230
|
// Create new browser instance
|
|
110
231
|
const newBrowser = await this.browserFactory();
|
|
111
232
|
this.browser = newBrowser;
|
|
233
|
+
// Re-attach disconnected event handler
|
|
234
|
+
this.browser.on('disconnected', () => {
|
|
235
|
+
this.log('Browser disconnected event fired');
|
|
236
|
+
this.setState(ConnectionState.CLOSED);
|
|
237
|
+
});
|
|
238
|
+
this.setState(ConnectionState.CONNECTED);
|
|
112
239
|
this.log('Browser reconnected successfully');
|
|
113
240
|
// Notify callback if provided
|
|
114
241
|
if (this.options.onReconnect) {
|
|
@@ -138,8 +265,21 @@ ${lastError?.message || 'Unknown error'}
|
|
|
138
265
|
error.name = 'CDPReconnectionError';
|
|
139
266
|
return error;
|
|
140
267
|
}
|
|
268
|
+
/**
|
|
269
|
+
* Set connection state and log transition
|
|
270
|
+
*
|
|
271
|
+
* @param newState - New connection state
|
|
272
|
+
*/
|
|
273
|
+
setState(newState) {
|
|
274
|
+
if (this.state !== newState) {
|
|
275
|
+
this.log(`State transition: ${this.state} -> ${newState}`);
|
|
276
|
+
this.state = newState;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
141
279
|
/**
|
|
142
280
|
* Log message if logging is enabled
|
|
281
|
+
*
|
|
282
|
+
* @param message - Message to log
|
|
143
283
|
*/
|
|
144
284
|
log(message) {
|
|
145
285
|
if (this.options.enableLogging !== false) {
|
|
@@ -172,10 +312,28 @@ ${lastError?.message || 'Unknown error'}
|
|
|
172
312
|
}
|
|
173
313
|
/**
|
|
174
314
|
* Check if browser is currently connected
|
|
315
|
+
*
|
|
316
|
+
* @returns true if browser is connected
|
|
175
317
|
*/
|
|
176
318
|
isConnected() {
|
|
177
319
|
return this.browser?.isConnected() ?? false;
|
|
178
320
|
}
|
|
321
|
+
/**
|
|
322
|
+
* Get current connection state
|
|
323
|
+
*
|
|
324
|
+
* @returns Current connection state (CONNECTED | RECONNECTING | CLOSED)
|
|
325
|
+
*/
|
|
326
|
+
getState() {
|
|
327
|
+
return this.state;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Check if reconnection is currently in progress
|
|
331
|
+
*
|
|
332
|
+
* @returns true if reconnection is in progress
|
|
333
|
+
*/
|
|
334
|
+
isReconnecting() {
|
|
335
|
+
return this.state === ConnectionState.RECONNECTING;
|
|
336
|
+
}
|
|
179
337
|
}
|
|
180
338
|
/**
|
|
181
339
|
* Global connection manager instance
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrome-devtools-mcp-for-extension",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "MCP server for Chrome extension development with Web Store automation. Fork of chrome-devtools-mcp with extension-specific tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": "./build/src/index.js",
|