@mcp-b/chrome-devtools-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.
@@ -43,6 +43,8 @@ export class WebMCPClientTransport {
43
43
  _page;
44
44
  _cdpSession = null;
45
45
  _started = false;
46
+ /** Guards against concurrent start() calls. */
47
+ _starting = false;
46
48
  _closed = false;
47
49
  _readyTimeout;
48
50
  _requireWebMCP;
@@ -50,9 +52,17 @@ export class WebMCPClientTransport {
50
52
  _serverReadyPromise;
51
53
  _serverReadyResolve;
52
54
  _serverReadyReject;
53
- // Transport callbacks
55
+ /** Tracks if promise was rejected to prevent double rejection. */
56
+ _serverReadyRejected = false;
57
+ /** Bound handler for frame navigation events (stored for cleanup). */
58
+ _frameNavigatedHandler = null;
59
+ /** Bound handler for CDP binding calls (stored for cleanup). */
60
+ _bindingCalledHandler = null;
61
+ /** Callback invoked when transport is closed. */
54
62
  onclose;
63
+ /** Callback invoked when an error occurs. */
55
64
  onerror;
65
+ /** Callback invoked when a JSON-RPC message is received. */
56
66
  onmessage;
57
67
  /**
58
68
  * Check if the transport has been closed.
@@ -74,21 +84,43 @@ export class WebMCPClientTransport {
74
84
  this._page = options.page;
75
85
  this._readyTimeout = options.readyTimeout ?? 10000;
76
86
  this._requireWebMCP = options.requireWebMCP ?? true;
77
- // Set up server ready promise
87
+ // Set up server ready promise with attached rejection handler
88
+ // to prevent unhandled rejection if close() is called before start()
78
89
  this._serverReadyPromise = new Promise((resolve, reject) => {
79
90
  this._serverReadyResolve = resolve;
80
- this._serverReadyReject = reject;
91
+ this._serverReadyReject = (err) => {
92
+ this._serverReadyRejected = true;
93
+ reject(err);
94
+ };
95
+ });
96
+ // Attach a no-op catch to prevent unhandled rejection warnings
97
+ // The actual error will be propagated when the promise is awaited
98
+ this._serverReadyPromise.catch(() => {
99
+ // Intentionally empty - errors are handled where the promise is awaited
81
100
  });
82
101
  }
83
102
  /**
84
- * Check if WebMCP is available on the page
103
+ * Check if WebMCP is available on the page.
104
+ *
105
+ * Returns `available: false` with an `error` field if the check failed due to
106
+ * page-level issues (closed, navigating, etc.) vs WebMCP simply not being present.
85
107
  */
86
108
  async checkWebMCPAvailable() {
87
109
  try {
88
110
  const result = await this._page.evaluate(CHECK_WEBMCP_AVAILABLE_SCRIPT);
89
111
  return result;
90
112
  }
91
- catch {
113
+ catch (err) {
114
+ const message = err instanceof Error ? err.message : String(err);
115
+ // Distinguish between "page is broken" vs "WebMCP not present"
116
+ const isPageError = message.includes('Execution context was destroyed') ||
117
+ message.includes('Target closed') ||
118
+ message.includes('Session closed') ||
119
+ message.includes('Protocol error');
120
+ if (isPageError) {
121
+ return { available: false, error: `Page error: ${message}` };
122
+ }
123
+ // Other errors (e.g., script threw) mean WebMCP is not available
92
124
  return { available: false };
93
125
  }
94
126
  }
@@ -106,76 +138,103 @@ export class WebMCPClientTransport {
106
138
  if (this._started) {
107
139
  throw new Error('WebMCPClientTransport already started');
108
140
  }
141
+ if (this._starting) {
142
+ throw new Error('WebMCPClientTransport start already in progress');
143
+ }
109
144
  if (this._closed) {
110
145
  throw new Error('WebMCPClientTransport has been closed');
111
146
  }
112
- // Check if WebMCP is available
113
- if (this._requireWebMCP) {
114
- const check = await this.checkWebMCPAvailable();
115
- if (!check.available) {
116
- throw new Error('WebMCP not detected on this page. ' +
117
- 'Ensure @mcp-b/global is loaded and initialized.');
147
+ this._starting = true;
148
+ try {
149
+ // Check if WebMCP is available
150
+ if (this._requireWebMCP) {
151
+ const check = await this.checkWebMCPAvailable();
152
+ if (!check.available) {
153
+ const errorDetail = check.error ? ` (${check.error})` : '';
154
+ throw new Error(`WebMCP not detected on this page${errorDetail}. ` +
155
+ 'Ensure @mcp-b/global is loaded and initialized.');
156
+ }
118
157
  }
119
- }
120
- // Create CDP session for this page
121
- this._cdpSession = await this._page.createCDPSession();
122
- // Enable Runtime domain for bindings and evaluation
123
- await this._cdpSession.send('Runtime.enable');
124
- // Set up binding for receiving messages from the bridge
125
- // When the bridge calls window.__mcpBridgeToClient(msg), we receive it here
126
- await this._cdpSession.send('Runtime.addBinding', {
127
- name: '__mcpBridgeToClient',
128
- });
129
- // Listen for binding calls (messages from bridge → this transport)
130
- this._cdpSession.on('Runtime.bindingCalled', event => {
131
- if (event.name !== '__mcpBridgeToClient')
132
- return;
158
+ // Create CDP session for this page
159
+ this._cdpSession = await this._page.createCDPSession();
160
+ // Enable Runtime domain for bindings and evaluation
161
+ await this._cdpSession.send('Runtime.enable');
162
+ // Set up binding for receiving messages from the bridge
163
+ // When the bridge calls window.__mcpBridgeToClient(msg), we receive it here
164
+ await this._cdpSession.send('Runtime.addBinding', {
165
+ name: '__mcpBridgeToClient',
166
+ });
167
+ // Create bound handlers so we can remove them later
168
+ this._bindingCalledHandler = (event) => {
169
+ if (event.name !== '__mcpBridgeToClient')
170
+ return;
171
+ // Guard against processing messages after close
172
+ if (this._closed)
173
+ return;
174
+ try {
175
+ const payload = JSON.parse(event.payload);
176
+ this._handlePayload(payload);
177
+ }
178
+ catch (err) {
179
+ this.onerror?.(new Error(`Failed to parse message from bridge: ${err}`));
180
+ }
181
+ };
182
+ this._frameNavigatedHandler = (frame) => {
183
+ if (frame === this._page.mainFrame()) {
184
+ // Main frame navigated, bridge is gone
185
+ this._handleNavigation();
186
+ }
187
+ };
188
+ // Listen for binding calls (messages from bridge → this transport)
189
+ this._cdpSession.on('Runtime.bindingCalled', this._bindingCalledHandler);
190
+ // Listen for page navigation to detect when bridge is lost
191
+ this._page.on('framenavigated', this._frameNavigatedHandler);
192
+ // Inject the bridge script
193
+ const result = await this._page.evaluate(WEB_MCP_BRIDGE_SCRIPT);
194
+ if (!result.success && !result.alreadyInjected) {
195
+ throw new Error('Failed to inject WebMCP bridge script');
196
+ }
197
+ this._started = true;
198
+ // Initiate server-ready handshake
199
+ await this._page.evaluate(() => {
200
+ window.__mcpBridge?.checkReady();
201
+ });
202
+ // Wait for server ready with timeout
203
+ const timeoutPromise = new Promise((_, reject) => {
204
+ const timer = setTimeout(() => {
205
+ reject(new Error(`WebMCP server did not respond within ${this._readyTimeout}ms. ` +
206
+ 'Ensure TabServerTransport is running on the page.'));
207
+ }, this._readyTimeout);
208
+ // Clear timeout if promise resolves
209
+ this._serverReadyPromise.then(() => clearTimeout(timer)).catch(() => clearTimeout(timer));
210
+ });
133
211
  try {
134
- const payload = JSON.parse(event.payload);
135
- this._handlePayload(payload);
212
+ await Promise.race([this._serverReadyPromise, timeoutPromise]);
136
213
  }
137
214
  catch (err) {
138
- this.onerror?.(new Error(`Failed to parse message from bridge: ${err}`));
139
- }
140
- });
141
- // Listen for page navigation to detect when bridge is lost
142
- this._page.on('framenavigated', frame => {
143
- if (frame === this._page.mainFrame()) {
144
- // Main frame navigated, bridge is gone
145
- this._handleNavigation();
215
+ await this.close();
216
+ throw err;
146
217
  }
147
- });
148
- // Inject the bridge script
149
- const result = await this._page.evaluate(WEB_MCP_BRIDGE_SCRIPT);
150
- if (!result.success && !result.alreadyInjected) {
151
- throw new Error('Failed to inject WebMCP bridge script');
152
- }
153
- this._started = true;
154
- // Initiate server-ready handshake
155
- await this._page.evaluate(() => {
156
- window.__mcpBridge?.checkReady();
157
- });
158
- // Wait for server ready with timeout
159
- const timeoutPromise = new Promise((_, reject) => {
160
- const timer = setTimeout(() => {
161
- reject(new Error(`WebMCP server did not respond within ${this._readyTimeout}ms. ` +
162
- 'Ensure TabServerTransport is running on the page.'));
163
- }, this._readyTimeout);
164
- // Clear timeout if promise resolves
165
- this._serverReadyPromise.then(() => clearTimeout(timer)).catch(() => clearTimeout(timer));
166
- });
167
- try {
168
- await Promise.race([this._serverReadyPromise, timeoutPromise]);
169
218
  }
170
219
  catch (err) {
171
- await this.close();
220
+ // Clean up on any error during start
221
+ this._starting = false;
222
+ if (!this._closed) {
223
+ await this._cleanup();
224
+ }
172
225
  throw err;
173
226
  }
227
+ finally {
228
+ this._starting = false;
229
+ }
174
230
  }
175
231
  /**
176
232
  * Handle a payload received from the bridge
177
233
  */
178
234
  _handlePayload(payload) {
235
+ // Guard against processing messages after close
236
+ if (this._closed)
237
+ return;
179
238
  // Handle special string payloads (handshake signals)
180
239
  if (typeof payload === 'string') {
181
240
  if (payload === 'mcp-server-ready') {
@@ -207,25 +266,32 @@ export class WebMCPClientTransport {
207
266
  * Handle page navigation - bridge is lost
208
267
  *
209
268
  * Navigation is a normal lifecycle event in browsers, not a fatal error.
210
- * We close the connection gracefully and allow the client to reconnect if needed.
269
+ * We perform full teardown (detach CDP session, remove listeners) and
270
+ * allow the client to reconnect by creating a new transport instance.
211
271
  */
212
272
  _handleNavigation() {
213
273
  if (this._closed)
214
274
  return;
215
275
  this._serverReady = false;
216
- // Mark as closed to prevent further operations
217
276
  this._closed = true;
218
277
  this._started = false;
219
- // Reject any pending server ready promise
220
- this._serverReadyReject(new Error('Page navigated, connection lost'));
278
+ // Reject any pending server ready promise (safe - has attached catch handler)
279
+ if (!this._serverReadyRejected) {
280
+ this._serverReadyReject(new Error('Page navigated, connection lost'));
281
+ }
282
+ // Full teardown - detach CDP session and remove listeners
283
+ this._cleanup().catch(() => {
284
+ // Ignore cleanup errors during navigation
285
+ });
221
286
  // Signal clean disconnection (not an error)
222
- // The client can reconnect by creating a new transport instance
223
287
  this.onclose?.();
224
288
  }
225
289
  /**
226
290
  * Handle server stopped signal
227
291
  */
228
292
  _handleServerStopped() {
293
+ if (this._closed)
294
+ return;
229
295
  this._serverReady = false;
230
296
  this.onerror?.(new Error('WebMCP server stopped'));
231
297
  }
@@ -242,8 +308,9 @@ export class WebMCPClientTransport {
242
308
  // Wait for server to be ready before sending
243
309
  await this._serverReadyPromise;
244
310
  // Send via CDP → bridge → postMessage → TabServer
245
- const messageJson = JSON.stringify(message);
246
311
  try {
312
+ // JSON.stringify inside try block to catch non-serializable messages
313
+ const messageJson = JSON.stringify(message);
247
314
  await this._page.evaluate((msg) => {
248
315
  const bridge = window.__mcpBridge;
249
316
  if (!bridge) {
@@ -262,14 +329,20 @@ export class WebMCPClientTransport {
262
329
  }
263
330
  }
264
331
  /**
265
- * Close the transport and clean up resources
332
+ * Internal cleanup method - removes listeners and detaches CDP session.
333
+ * Does not set _closed flag or call onclose (caller handles those).
266
334
  */
267
- async close() {
268
- if (this._closed)
269
- return;
270
- this._closed = true;
271
- this._started = false;
272
- this._serverReady = false;
335
+ async _cleanup() {
336
+ // Remove page event listener
337
+ if (this._frameNavigatedHandler) {
338
+ this._page.off('framenavigated', this._frameNavigatedHandler);
339
+ this._frameNavigatedHandler = null;
340
+ }
341
+ // Remove CDP event listener
342
+ if (this._cdpSession && this._bindingCalledHandler) {
343
+ this._cdpSession.off('Runtime.bindingCalled', this._bindingCalledHandler);
344
+ this._bindingCalledHandler = null;
345
+ }
273
346
  // Dispose the bridge script if possible
274
347
  try {
275
348
  await this._page.evaluate(() => {
@@ -277,7 +350,7 @@ export class WebMCPClientTransport {
277
350
  });
278
351
  }
279
352
  catch {
280
- // Ignore errors during cleanup
353
+ // Ignore errors during cleanup (page might be closed/navigated)
281
354
  }
282
355
  // Detach CDP session
283
356
  if (this._cdpSession) {
@@ -289,8 +362,22 @@ export class WebMCPClientTransport {
289
362
  }
290
363
  this._cdpSession = null;
291
364
  }
292
- // Reject any pending server ready promise
293
- this._serverReadyReject(new Error('Transport closed'));
365
+ }
366
+ /**
367
+ * Close the transport and clean up resources
368
+ */
369
+ async close() {
370
+ if (this._closed)
371
+ return;
372
+ this._closed = true;
373
+ this._started = false;
374
+ this._serverReady = false;
375
+ // Full cleanup
376
+ await this._cleanup();
377
+ // Reject any pending server ready promise (safe - has attached catch handler)
378
+ if (!this._serverReadyRejected) {
379
+ this._serverReadyReject(new Error('Transport closed'));
380
+ }
294
381
  this.onclose?.();
295
382
  }
296
383
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcp-b/chrome-devtools-mcp",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "MCP server for Chrome DevTools with WebMCP integration for connecting to website MCP tools",
5
5
  "keywords": [
6
6
  "mcp",
@@ -48,6 +48,7 @@
48
48
  "!*.tsbuildinfo"
49
49
  ],
50
50
  "dependencies": {
51
+ "@composio/json-schema-to-zod": "^0.1.17",
51
52
  "@modelcontextprotocol/sdk": "1.24.1",
52
53
  "core-js": "3.47.0",
53
54
  "debug": "4.4.3",