chrome-devtools-mcp-for-extension 0.12.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 +86 -3
- package/build/src/browser-connection-manager.js +356 -0
- package/build/src/main.js +16 -3
- package/package.json +1 -1
package/build/src/McpContext.js
CHANGED
|
@@ -10,6 +10,7 @@ import { NetworkCollector, PageCollector } from './PageCollector.js';
|
|
|
10
10
|
import { listPages } from './tools/pages.js';
|
|
11
11
|
import { CLOSE_PAGE_ERROR } from './tools/ToolDefinition.js';
|
|
12
12
|
import { WaitForHelper } from './WaitForHelper.js';
|
|
13
|
+
import { BrowserConnectionManager, } from './browser-connection-manager.js';
|
|
13
14
|
const DEFAULT_TIMEOUT = 5_000;
|
|
14
15
|
const NAVIGATION_TIMEOUT = 10_000;
|
|
15
16
|
function getNetworkMultiplierFromString(condition) {
|
|
@@ -29,6 +30,7 @@ function getNetworkMultiplierFromString(condition) {
|
|
|
29
30
|
export class McpContext {
|
|
30
31
|
browser;
|
|
31
32
|
logger;
|
|
33
|
+
connectionManager;
|
|
32
34
|
// The most recent page state.
|
|
33
35
|
#pages = [];
|
|
34
36
|
#selectedPageIdx = 0;
|
|
@@ -42,9 +44,14 @@ export class McpContext {
|
|
|
42
44
|
#dialog;
|
|
43
45
|
#nextSnapshotId = 1;
|
|
44
46
|
#traceResults = [];
|
|
45
|
-
constructor(browser, logger) {
|
|
47
|
+
constructor(browser, logger, browserFactory, connectionOptions) {
|
|
46
48
|
this.browser = browser;
|
|
47
49
|
this.logger = logger;
|
|
50
|
+
this.connectionManager = new BrowserConnectionManager(connectionOptions);
|
|
51
|
+
// Set up browser instance and factory for reconnection
|
|
52
|
+
if (browserFactory) {
|
|
53
|
+
this.connectionManager.setBrowser(browser, browserFactory);
|
|
54
|
+
}
|
|
48
55
|
this.#networkCollector = new NetworkCollector(this.browser, (page, collect) => {
|
|
49
56
|
page.on('request', request => {
|
|
50
57
|
collect(request);
|
|
@@ -65,11 +72,87 @@ export class McpContext {
|
|
|
65
72
|
await this.#networkCollector.init();
|
|
66
73
|
await this.#consoleCollector.init();
|
|
67
74
|
}
|
|
68
|
-
static async from(browser, logger) {
|
|
69
|
-
const context = new McpContext(browser, logger);
|
|
75
|
+
static async from(browser, logger, browserFactory, connectionOptions) {
|
|
76
|
+
const context = new McpContext(browser, logger, browserFactory, connectionOptions);
|
|
70
77
|
await context.#init();
|
|
71
78
|
return context;
|
|
72
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
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Update browser instance after reconnection
|
|
141
|
+
*/
|
|
142
|
+
async updateBrowser(newBrowser) {
|
|
143
|
+
this.browser = newBrowser;
|
|
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();
|
|
148
|
+
// Reinitialize collectors for new browser
|
|
149
|
+
await this.#networkCollector.init();
|
|
150
|
+
await this.#consoleCollector.init();
|
|
151
|
+
// Recreate pages snapshot
|
|
152
|
+
await this.createPagesSnapshot();
|
|
153
|
+
this.setSelectedPageIdx(0);
|
|
154
|
+
this.logger('Browser instance updated after reconnection');
|
|
155
|
+
}
|
|
73
156
|
getNetworkRequests() {
|
|
74
157
|
const page = this.getSelectedPage();
|
|
75
158
|
return this.#networkCollector.getData(page);
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import { ProtocolError, TimeoutError } from 'puppeteer';
|
|
7
|
+
/**
|
|
8
|
+
* Default connection manager options
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULT_OPTIONS = {
|
|
11
|
+
maxReconnectAttempts: 3,
|
|
12
|
+
initialRetryDelay: 1000, // 1 second
|
|
13
|
+
maxRetryDelay: 10000, // 10 seconds
|
|
14
|
+
enableLogging: true,
|
|
15
|
+
onReconnect: undefined,
|
|
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 = {}));
|
|
26
|
+
/**
|
|
27
|
+
* Browser Connection Manager
|
|
28
|
+
*
|
|
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
|
|
34
|
+
*/
|
|
35
|
+
export class BrowserConnectionManager {
|
|
36
|
+
reconnectAttempts = 0;
|
|
37
|
+
options;
|
|
38
|
+
browser = null;
|
|
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;
|
|
46
|
+
constructor(options = {}) {
|
|
47
|
+
this.options = { ...DEFAULT_OPTIONS, ...options };
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
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
|
|
54
|
+
*/
|
|
55
|
+
setBrowser(browser, factory) {
|
|
56
|
+
this.browser = browser;
|
|
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');
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Execute an operation with automatic retry on CDP connection errors
|
|
69
|
+
*/
|
|
70
|
+
async executeWithRetry(operation, operationName) {
|
|
71
|
+
try {
|
|
72
|
+
return await operation();
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (this.isCDPConnectionError(error)) {
|
|
76
|
+
this.log(`CDP connection error in ${operationName}, attempting reconnect...`);
|
|
77
|
+
return await this.retryWithReconnect(operation, operationName);
|
|
78
|
+
}
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
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
|
|
95
|
+
*/
|
|
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() {
|
|
134
|
+
const maxAttempts = this.options.maxReconnectAttempts ?? 3;
|
|
135
|
+
const initialDelay = this.options.initialRetryDelay ?? 1000;
|
|
136
|
+
const maxDelay = this.options.maxRetryDelay ?? 10000;
|
|
137
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
138
|
+
this.reconnectAttempts++;
|
|
139
|
+
const attemptNum = attempt + 1;
|
|
140
|
+
this.log(`Reconnect attempt ${attemptNum}/${maxAttempts}...`);
|
|
141
|
+
// Exponential backoff with max delay
|
|
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...`);
|
|
147
|
+
await this.sleep(delay);
|
|
148
|
+
try {
|
|
149
|
+
await this.reconnectBrowser();
|
|
150
|
+
this.log(`Reconnection successful`);
|
|
151
|
+
return; // Success!
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
if (attempt === maxAttempts - 1) {
|
|
155
|
+
// Last attempt failed
|
|
156
|
+
throw this.createReconnectionFailedError(attemptNum, error);
|
|
157
|
+
}
|
|
158
|
+
this.log(`Reconnect attempt ${attemptNum} failed: ${error}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
throw new Error('Reconnection logic error'); // Should never reach here
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
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
|
|
171
|
+
*/
|
|
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
|
|
178
|
+
const errorMessage = error?.message || '';
|
|
179
|
+
return (errorMessage.includes('Target closed') ||
|
|
180
|
+
errorMessage.includes('Protocol error') ||
|
|
181
|
+
errorMessage.includes('Session closed') ||
|
|
182
|
+
errorMessage.includes('Connection closed') ||
|
|
183
|
+
errorMessage.includes('WebSocket is not open'));
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
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
|
|
192
|
+
*/
|
|
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() {
|
|
215
|
+
if (!this.browserFactory) {
|
|
216
|
+
throw new Error('Browser factory not set. Cannot reconnect.');
|
|
217
|
+
}
|
|
218
|
+
this.setState(ConnectionState.RECONNECTING);
|
|
219
|
+
try {
|
|
220
|
+
// Close old browser if still connected
|
|
221
|
+
if (this.browser?.isConnected()) {
|
|
222
|
+
await this.browser.close().catch(() => {
|
|
223
|
+
// Ignore errors during close
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
// Ignore errors during cleanup
|
|
229
|
+
}
|
|
230
|
+
// Create new browser instance
|
|
231
|
+
const newBrowser = await this.browserFactory();
|
|
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);
|
|
239
|
+
this.log('Browser reconnected successfully');
|
|
240
|
+
// Notify callback if provided
|
|
241
|
+
if (this.options.onReconnect) {
|
|
242
|
+
await this.options.onReconnect(newBrowser);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Create user-friendly error for reconnection failure
|
|
247
|
+
*/
|
|
248
|
+
createReconnectionFailedError(attempts, lastError) {
|
|
249
|
+
const message = `
|
|
250
|
+
❌ Chrome DevTools接続エラー
|
|
251
|
+
|
|
252
|
+
${attempts}回の再接続を試みましたが、Chrome DevToolsとの接続を回復できませんでした。
|
|
253
|
+
|
|
254
|
+
📋 最後のエラー:
|
|
255
|
+
${lastError?.message || 'Unknown error'}
|
|
256
|
+
|
|
257
|
+
🔧 解決方法:
|
|
258
|
+
1. Claude Codeを再起動してください
|
|
259
|
+
2. Chromeブラウザを完全に終了して再起動してください
|
|
260
|
+
3. chrome://extensions でChrome DevTools拡張機能を確認してください
|
|
261
|
+
|
|
262
|
+
詳細: docs/troubleshooting.md#cdp-connection-error
|
|
263
|
+
`.trim();
|
|
264
|
+
const error = new Error(message);
|
|
265
|
+
error.name = 'CDPReconnectionError';
|
|
266
|
+
return error;
|
|
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
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Log message if logging is enabled
|
|
281
|
+
*
|
|
282
|
+
* @param message - Message to log
|
|
283
|
+
*/
|
|
284
|
+
log(message) {
|
|
285
|
+
if (this.options.enableLogging !== false) {
|
|
286
|
+
console.log(`[ConnectionManager] ${message}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Sleep for specified milliseconds
|
|
291
|
+
*/
|
|
292
|
+
sleep(ms) {
|
|
293
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Get current browser instance
|
|
297
|
+
*/
|
|
298
|
+
getBrowser() {
|
|
299
|
+
return this.browser;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Get total reconnection attempts made
|
|
303
|
+
*/
|
|
304
|
+
getReconnectAttempts() {
|
|
305
|
+
return this.reconnectAttempts;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Reset reconnection attempt counter
|
|
309
|
+
*/
|
|
310
|
+
resetReconnectAttempts() {
|
|
311
|
+
this.reconnectAttempts = 0;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Check if browser is currently connected
|
|
315
|
+
*
|
|
316
|
+
* @returns true if browser is connected
|
|
317
|
+
*/
|
|
318
|
+
isConnected() {
|
|
319
|
+
return this.browser?.isConnected() ?? false;
|
|
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
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Global connection manager instance
|
|
340
|
+
*/
|
|
341
|
+
let globalConnectionManager = null;
|
|
342
|
+
/**
|
|
343
|
+
* Get or create global connection manager
|
|
344
|
+
*/
|
|
345
|
+
export function getConnectionManager(options) {
|
|
346
|
+
if (!globalConnectionManager) {
|
|
347
|
+
globalConnectionManager = new BrowserConnectionManager(options);
|
|
348
|
+
}
|
|
349
|
+
return globalConnectionManager;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Reset global connection manager (for testing)
|
|
353
|
+
*/
|
|
354
|
+
export function resetConnectionManager() {
|
|
355
|
+
globalConnectionManager = null;
|
|
356
|
+
}
|
package/build/src/main.js
CHANGED
|
@@ -60,7 +60,7 @@ server.server.setRequestHandler(SetLevelRequestSchema, () => {
|
|
|
60
60
|
let context;
|
|
61
61
|
let uiHealthCheckRun = false; // Track if UI health check has been run
|
|
62
62
|
async function getContext() {
|
|
63
|
-
const
|
|
63
|
+
const browserOptions = {
|
|
64
64
|
browserUrl: args.browserUrl,
|
|
65
65
|
headless: args.headless,
|
|
66
66
|
executablePath: args.executablePath,
|
|
@@ -73,10 +73,23 @@ async function getContext() {
|
|
|
73
73
|
chromeProfile: args.chromeProfile,
|
|
74
74
|
userDataDir: args.userDataDir,
|
|
75
75
|
logFile,
|
|
76
|
-
}
|
|
76
|
+
};
|
|
77
|
+
const browser = await resolveBrowser(browserOptions);
|
|
78
|
+
// Browser factory function for reconnection
|
|
79
|
+
const browserFactory = async () => {
|
|
80
|
+
logger('Reconnecting browser...');
|
|
81
|
+
return await resolveBrowser(browserOptions);
|
|
82
|
+
};
|
|
77
83
|
// Always recreate context if browser reference changed or context doesn't exist
|
|
78
84
|
if (!context || context.browser !== browser) {
|
|
79
|
-
|
|
85
|
+
// Connection manager options with reconnect callback
|
|
86
|
+
const connectionOptions = {
|
|
87
|
+
onReconnect: async (newBrowser) => {
|
|
88
|
+
logger('Updating context with reconnected browser...');
|
|
89
|
+
await context.updateBrowser(newBrowser);
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
context = await McpContext.from(browser, logger, browserFactory, connectionOptions);
|
|
80
93
|
// Run UI health check only once per browser instance
|
|
81
94
|
if (!uiHealthCheckRun) {
|
|
82
95
|
uiHealthCheckRun = true;
|
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",
|