@mcp-b/chrome-devtools-mcp 1.6.2 → 1.6.3

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.
@@ -167,8 +167,10 @@ export class McpContext {
167
167
  }
168
168
  await page.evaluate(WEB_MCP_BRIDGE_SCRIPT);
169
169
  }
170
- catch {
171
- // Page might not be ready or accessible, ignore
170
+ catch (err) {
171
+ // Page might not be ready or accessible - log for debugging
172
+ const message = err instanceof Error ? err.message : String(err);
173
+ this.logger(`Bridge injection skipped for ${page.url()}: ${message}`);
172
174
  }
173
175
  }
174
176
  this.logger('WebMCP bridge injected into existing pages');
@@ -191,7 +193,10 @@ export class McpContext {
191
193
  this.#toolHub = hub;
192
194
  // Trigger auto-detection for all existing pages asynchronously
193
195
  this.#setupWebMCPAutoDetectionForAllPages().catch(err => {
194
- this.logger('Error setting up WebMCP auto-detection:', err);
196
+ const message = err instanceof Error ? err.message : String(err);
197
+ this.logger(`WebMCP auto-detection setup failed: ${message}`);
198
+ // Note: This is non-fatal - individual page connections may still work
199
+ // when explicitly requested via getWebMCPClient()
195
200
  });
196
201
  }
197
202
  /**
@@ -202,11 +207,23 @@ export class McpContext {
202
207
  }
203
208
  /**
204
209
  * Get or create a browser-level CDP session for window operations.
210
+ * Recreates the session if it has become invalid.
205
211
  */
206
212
  async #getBrowserCdpSession() {
207
- if (!this.#browserCdpSession) {
208
- this.#browserCdpSession = await this.browser.target().createCDPSession();
213
+ if (this.#browserCdpSession) {
214
+ // Verify session is still valid by attempting a simple operation
215
+ try {
216
+ await this.#browserCdpSession.send('Browser.getVersion');
217
+ return this.#browserCdpSession;
218
+ }
219
+ catch (err) {
220
+ // Session is stale, recreate it
221
+ const message = err instanceof Error ? err.message : String(err);
222
+ this.logger(`Browser CDP session stale (${message}), recreating...`);
223
+ this.#browserCdpSession = undefined;
224
+ }
209
225
  }
226
+ this.#browserCdpSession = await this.browser.target().createCDPSession();
210
227
  return this.#browserCdpSession;
211
228
  }
212
229
  /**
@@ -235,6 +252,44 @@ export class McpContext {
235
252
  getSessionWindowId() {
236
253
  return this.#sessionWindowId;
237
254
  }
255
+ /**
256
+ * Close all pages in the session's window.
257
+ * Called during cleanup when the MCP server is shutting down.
258
+ */
259
+ async closeSessionWindow() {
260
+ if (this.#sessionWindowId === undefined) {
261
+ this.logger('No session window to close');
262
+ return;
263
+ }
264
+ this.logger(`Closing session window ${this.#sessionWindowId}...`);
265
+ // Get all pages in this session's window and close them
266
+ const pagesToClose = [...this.#pages];
267
+ for (const page of pagesToClose) {
268
+ try {
269
+ if (!page.isClosed()) {
270
+ await page.close();
271
+ }
272
+ }
273
+ catch (err) {
274
+ // Ignore errors during cleanup - page might already be closed
275
+ const message = err instanceof Error ? err.message : String(err);
276
+ this.logger(`Error closing page during cleanup: ${message}`);
277
+ }
278
+ }
279
+ // Clean up the CDP session
280
+ if (this.#browserCdpSession) {
281
+ try {
282
+ await this.#browserCdpSession.detach();
283
+ }
284
+ catch (err) {
285
+ // Detach errors during cleanup are expected - log for debugging
286
+ const message = err instanceof Error ? err.message : String(err);
287
+ this.logger(`CDP detach during cleanup: ${message}`);
288
+ }
289
+ this.#browserCdpSession = undefined;
290
+ }
291
+ this.logger('Session window closed');
292
+ }
238
293
  /**
239
294
  * Set up automatic WebMCP detection for a page.
240
295
  * This installs listeners that detect WebMCP after navigation and sync tools.
@@ -270,7 +325,17 @@ export class McpContext {
270
325
  // Immediately try to connect - no polling needed
271
326
  // If no WebMCP, connection will timeout gracefully
272
327
  this.#tryConnectWebMCP(page).catch(err => {
273
- this.logger('WebMCP connection attempt failed (expected if page has no WebMCP):', err);
328
+ const message = err instanceof Error ? err.message : String(err);
329
+ // Differentiate expected timeouts from unexpected errors
330
+ const isExpected = message.includes('timeout') ||
331
+ message.includes('WebMCP not detected') ||
332
+ message.includes('server did not respond');
333
+ if (isExpected) {
334
+ this.logger(`No WebMCP detected after navigation: ${page.url()}`);
335
+ }
336
+ else {
337
+ this.logger(`Unexpected WebMCP connection error for ${page.url()}: ${message}`);
338
+ }
274
339
  });
275
340
  };
276
341
  page.on('framenavigated', onFrameNavigated);
@@ -324,7 +389,17 @@ export class McpContext {
324
389
  this.#setupWebMCPAutoDetection(page);
325
390
  // Try to connect immediately (don't await - run in parallel for all pages)
326
391
  this.#tryConnectWebMCP(page).catch(err => {
327
- this.logger('Initial WebMCP connection attempt failed (expected):', err);
392
+ const message = err instanceof Error ? err.message : String(err);
393
+ // Differentiate expected timeouts from unexpected errors
394
+ const isExpected = message.includes('timeout') ||
395
+ message.includes('WebMCP not detected') ||
396
+ message.includes('server did not respond');
397
+ if (isExpected) {
398
+ this.logger(`No WebMCP on page during initial scan: ${page.url()}`);
399
+ }
400
+ else {
401
+ this.logger(`Unexpected error during initial WebMCP scan for ${page.url()}: ${message}`);
402
+ }
328
403
  });
329
404
  }
330
405
  }
@@ -412,8 +487,10 @@ export class McpContext {
412
487
  await existingPage.bringToFront();
413
488
  }
414
489
  }
415
- catch {
490
+ catch (err) {
416
491
  // Best effort - focus might fail if page is closing
492
+ const message = err instanceof Error ? err.message : String(err);
493
+ this.logger(`Window focus failed (non-critical): ${message}`);
417
494
  }
418
495
  }
419
496
  const page = await this.browser.newPage();
@@ -427,8 +504,10 @@ export class McpContext {
427
504
  `Tab may not be visible in list_pages.`);
428
505
  }
429
506
  }
430
- catch {
507
+ catch (err) {
431
508
  // Failed to get windowId - page might be in an unexpected state
509
+ const message = err instanceof Error ? err.message : String(err);
510
+ this.logger(`Window ID check failed (non-critical): ${message}`);
432
511
  }
433
512
  }
434
513
  await this.createPagesSnapshot();
@@ -631,6 +710,10 @@ export class McpContext {
631
710
  */
632
711
  async createPagesSnapshot() {
633
712
  const allPages = await this.browser.pages(this.#options.experimentalIncludeAllPages);
713
+ this.logger(`createPagesSnapshot: found ${allPages.length} total pages`);
714
+ for (const page of allPages) {
715
+ this.logger(` - ${page.url()}`);
716
+ }
634
717
  // First filter: DevTools pages (unless experimental mode is enabled)
635
718
  let filteredPages = allPages.filter(page => {
636
719
  return (this.#options.experimentalDevToolsDebugging ||
@@ -642,23 +725,50 @@ export class McpContext {
642
725
  const windowFilteredPages = [];
643
726
  // Check window IDs in parallel for better performance
644
727
  const windowIdResults = await Promise.allSettled(filteredPages.map(async (page) => {
645
- try {
646
- const windowId = await this.getWindowIdForPage(page);
647
- return { page, windowId };
728
+ // Try to get windowId with retry for pages that might be in transitional state
729
+ let windowId = null;
730
+ let lastError = null;
731
+ for (let attempt = 0; attempt < 3; attempt++) {
732
+ try {
733
+ windowId = await this.getWindowIdForPage(page);
734
+ break; // Success, exit retry loop
735
+ }
736
+ catch (err) {
737
+ lastError = err instanceof Error ? err.message : String(err);
738
+ if (attempt < 2) {
739
+ // Wait before retry (50ms, then 100ms)
740
+ await new Promise(r => setTimeout(r, 50 * (attempt + 1)));
741
+ }
742
+ }
648
743
  }
649
- catch {
650
- // Page might be closing, exclude it
651
- return null;
744
+ if (windowId !== null) {
745
+ return { page, windowId, url: page.url() };
652
746
  }
747
+ // All retries failed
748
+ this.logger(`Failed to get windowId for page ${page.url()} after 3 attempts: ${lastError}`);
749
+ return null;
653
750
  }));
654
751
  for (const result of windowIdResults) {
655
- if (result.status === 'fulfilled' &&
656
- result.value &&
657
- result.value.windowId === this.#sessionWindowId) {
658
- windowFilteredPages.push(result.value.page);
752
+ if (result.status === 'fulfilled' && result.value) {
753
+ if (result.value.windowId === this.#sessionWindowId) {
754
+ windowFilteredPages.push(result.value.page);
755
+ }
756
+ else {
757
+ this.logger(`Excluding page ${result.value.url} (windowId ${result.value.windowId} != session ${this.#sessionWindowId})`);
758
+ }
659
759
  }
660
760
  }
661
761
  filteredPages = windowFilteredPages;
762
+ this.logger(`Window filter: ${windowFilteredPages.length} pages in session window ${this.#sessionWindowId}`);
763
+ // Always include the explicitly selected page even if window filtering excluded it
764
+ // This handles edge cases where windowId lookup fails after cross-origin navigation
765
+ if (this.#pageExplicitlySelected &&
766
+ this.#selectedPage &&
767
+ !this.#selectedPage.isClosed() &&
768
+ !filteredPages.includes(this.#selectedPage)) {
769
+ this.logger(`Re-adding explicitly selected page that was filtered out: ${this.#selectedPage.url()}`);
770
+ filteredPages.unshift(this.#selectedPage);
771
+ }
662
772
  }
663
773
  this.#pages = filteredPages;
664
774
  // Only auto-select pages[0] if:
@@ -709,7 +819,8 @@ export class McpContext {
709
819
  }
710
820
  }
711
821
  catch (error) {
712
- this.logger('Issue occurred while trying to find DevTools', error);
822
+ const message = error instanceof Error ? error.message : String(error);
823
+ this.logger(`DevTools detection failed for ${devToolsPage.url()}: ${message}`);
713
824
  }
714
825
  }
715
826
  }
@@ -879,8 +990,10 @@ export class McpContext {
879
990
  try {
880
991
  await conn.client.close();
881
992
  }
882
- catch {
883
- // Ignore close errors
993
+ catch (err) {
994
+ // Close errors during reconnection are expected - log for debugging
995
+ const message = err instanceof Error ? err.message : String(err);
996
+ this.logger(`WebMCP client close during reconnection: ${message}`);
884
997
  }
885
998
  this.#webMCPConnections.delete(targetPage);
886
999
  }
package/build/src/main.js CHANGED
@@ -27,6 +27,39 @@ const VERSION = '1.5.6';
27
27
  process.on('unhandledRejection', (reason, promise) => {
28
28
  logger('Unhandled promise rejection', promise, reason);
29
29
  });
30
+ /**
31
+ * Cleanup handler for graceful shutdown.
32
+ * Closes the session window when the MCP server is killed.
33
+ */
34
+ let isCleaningUp = false;
35
+ async function cleanup() {
36
+ if (isCleaningUp)
37
+ return;
38
+ isCleaningUp = true;
39
+ logger('Shutting down MCP server...');
40
+ if (context) {
41
+ try {
42
+ await context.closeSessionWindow();
43
+ }
44
+ catch (err) {
45
+ logger('Error during cleanup:', err);
46
+ }
47
+ }
48
+ process.exit(0);
49
+ }
50
+ // Handle various termination signals
51
+ process.on('SIGINT', () => {
52
+ logger('Received SIGINT');
53
+ cleanup();
54
+ });
55
+ process.on('SIGTERM', () => {
56
+ logger('Received SIGTERM');
57
+ cleanup();
58
+ });
59
+ process.on('SIGHUP', () => {
60
+ logger('Received SIGHUP');
61
+ cleanup();
62
+ });
30
63
  export const args = parseArguments(VERSION);
31
64
  const logFile = args.logFile ? saveLogsToFile(args.logFile) : undefined;
32
65
  logger(`Starting Chrome DevTools MCP Server v${VERSION}`);
@@ -79,8 +79,10 @@ export class WebMCPToolHub {
79
79
  return this.#applyToolChanges(page, tools);
80
80
  }
81
81
  catch (err) {
82
- this.#logger('Failed to sync WebMCP tools:', err);
83
- return { synced: 0, removed: 0, updated: 0 };
82
+ const message = err instanceof Error ? err.message : String(err);
83
+ this.#logger(`Failed to sync WebMCP tools: ${message}`);
84
+ // Return error indicator so callers know sync failed vs "no tools found"
85
+ return { synced: 0, removed: 0, updated: 0, error: message };
84
86
  }
85
87
  finally {
86
88
  this.#syncInProgress.delete(page);
@@ -305,7 +307,12 @@ export function extractDomain(url) {
305
307
  : hostname;
306
308
  return sanitizeName(domain);
307
309
  }
308
- catch {
310
+ catch (err) {
311
+ // Log at debug level for troubleshooting malformed URLs
312
+ if (typeof console !== 'undefined' && console.debug) {
313
+ const message = err instanceof Error ? err.message : String(err);
314
+ console.debug(`[WebMCPToolHub] Failed to extract domain from URL "${url}": ${message}`);
315
+ }
309
316
  return 'unknown';
310
317
  }
311
318
  }
@@ -368,7 +368,29 @@ export const callWebMCPTool = defineTool({
368
368
  }
369
369
  }
370
370
  }
371
- response.appendResponseLine(`Failed to call tool: ${errorMessage}`);
371
+ // Classify errors and provide actionable guidance
372
+ if (errorMessage.includes('timeout') || errorMessage.includes('Timeout')) {
373
+ response.appendResponseLine(`Tool call timed out: ${name}`);
374
+ response.appendResponseLine('');
375
+ response.appendResponseLine('The page may be unresponsive. Try:');
376
+ response.appendResponseLine(' 1. Refresh the page with navigate_page({ type: "reload" })');
377
+ response.appendResponseLine(' 2. Check list_console_messages for JavaScript errors');
378
+ }
379
+ else if (errorMessage.includes('validation') || errorMessage.includes('schema')) {
380
+ response.appendResponseLine(`Tool argument validation failed: ${errorMessage}`);
381
+ response.appendResponseLine('');
382
+ response.appendResponseLine('Check the tool schema with list_webmcp_tools to see expected arguments.');
383
+ }
384
+ else if (errorMessage.includes('not found') || errorMessage.includes('No such tool')) {
385
+ response.appendResponseLine(`Tool not found: ${name}`);
386
+ response.appendResponseLine('');
387
+ response.appendResponseLine('The tool may have been unregistered. Use list_webmcp_tools to see available tools.');
388
+ }
389
+ else {
390
+ response.appendResponseLine(`Failed to call tool: ${errorMessage}`);
391
+ response.appendResponseLine('');
392
+ response.appendResponseLine('For debugging, check list_console_messages for page errors.');
393
+ }
372
394
  response.setIsError(true);
373
395
  }
374
396
  },
@@ -59,6 +59,8 @@ export class WebMCPClientTransport {
59
59
  _frameNavigatedHandler = null;
60
60
  /** Bound handler for CDP binding calls (stored for cleanup). */
61
61
  _bindingCalledHandler = null;
62
+ /** Guards against concurrent navigation handling. */
63
+ _navigationInProgress = false;
62
64
  /** Callback invoked when transport is closed. */
63
65
  onclose;
64
66
  /** Callback invoked when an error occurs. */
@@ -190,8 +192,13 @@ export class WebMCPClientTransport {
190
192
  };
191
193
  this._frameNavigatedHandler = (frame) => {
192
194
  if (frame === this._page.mainFrame()) {
193
- // Main frame navigated, bridge is gone
194
- this._handleNavigation();
195
+ // Main frame navigated - check if bridge survived (SPA navigation)
196
+ this._handleNavigation().catch((err) => {
197
+ // Log navigation handling errors for debugging
198
+ const message = err instanceof Error ? err.message : String(err);
199
+ const logFn = console.debug || console.log;
200
+ logFn('[WebMCPClientTransport] Navigation handling error:', message);
201
+ });
195
202
  }
196
203
  };
197
204
  // Listen for binding calls (messages from bridge → this transport)
@@ -272,31 +279,72 @@ export class WebMCPClientTransport {
272
279
  }
273
280
  }
274
281
  /**
275
- * Handle page navigation - bridge is lost
282
+ * Handle page navigation - check if OUR BRIDGE survived (SPA navigation)
276
283
  *
277
- * Navigation is a normal lifecycle event in browsers, not a fatal error.
278
- * We perform full teardown (detach CDP session, remove listeners) and
279
- * allow the client to reconnect by creating a new transport instance.
284
+ * The `framenavigated` event fires for BOTH:
285
+ * 1. Full page navigations (where the bridge IS destroyed)
286
+ * 2. Client-side SPA navigations via History API (where the bridge survives)
287
+ *
288
+ * For SPA navigations (React Router, TanStack Router, Next.js, etc.),
289
+ * the page doesn't actually reload - only the URL changes.
290
+ * The injected bridge script remains intact and functional.
291
+ *
292
+ * IMPORTANT: We check for OUR bridge (window.__mcpCdpBridge), not just the
293
+ * page's WebMCP availability. A full navigation to another WebMCP page would
294
+ * have a new TabServerTransport but our bridge would be destroyed.
280
295
  */
281
- _handleNavigation() {
282
- if (this._closed)
296
+ async _handleNavigation() {
297
+ // Guard against concurrent navigation handling
298
+ if (this._closed || this._navigationInProgress)
283
299
  return;
284
- this._serverReady = false;
285
- this._closed = true;
286
- this._started = false;
287
- // Reject any pending server ready promise (safe - has attached catch handler)
288
- if (!this._serverReadyRejected) {
289
- this._serverReadyReject(new Error('Page navigated, connection lost'));
300
+ this._navigationInProgress = true;
301
+ try {
302
+ // Give the page a moment to settle after navigation
303
+ // This helps with rapid SPA navigations
304
+ await new Promise(resolve => setTimeout(resolve, 50));
305
+ // Re-check closed state after the wait (could have been closed during delay)
306
+ if (this._closed)
307
+ return;
308
+ // Check if OUR BRIDGE survived the navigation (not just the page's WebMCP)
309
+ // This distinguishes SPA navigation from full navigation to another WebMCP page
310
+ try {
311
+ const bridgeAlive = await this._page.evaluate((bridgeProp) => {
312
+ return !!window[bridgeProp];
313
+ }, CDP_BRIDGE_WINDOW_PROPERTY);
314
+ if (bridgeAlive) {
315
+ // Our bridge survived - this was an SPA navigation, don't close
316
+ const logFn = console.debug || console.log;
317
+ logFn('[WebMCPClientTransport] Bridge survived navigation (SPA), keeping connection open');
318
+ return;
319
+ }
320
+ }
321
+ catch {
322
+ // Evaluation failed - page context is gone, proceed with close
323
+ }
324
+ // Re-check closed state (could have been closed during bridge check)
325
+ if (this._closed)
326
+ return;
327
+ // Bridge is lost - full navigation occurred
328
+ this._serverReady = false;
329
+ this._closed = true;
330
+ this._started = false;
331
+ // Reject any pending server ready promise (safe - has attached catch handler)
332
+ if (!this._serverReadyRejected) {
333
+ this._serverReadyReject(new Error('Page navigated, connection lost'));
334
+ }
335
+ // Full teardown - detach CDP session and remove listeners
336
+ this._cleanup().catch((err) => {
337
+ // Cleanup errors during navigation are expected and non-critical
338
+ const message = err instanceof Error ? err.message : String(err);
339
+ const logFn = console.debug || console.log;
340
+ logFn('[WebMCPClientTransport] Cleanup error during navigation (non-critical):', message);
341
+ });
342
+ // Signal clean disconnection (not an error)
343
+ this.onclose?.();
344
+ }
345
+ finally {
346
+ this._navigationInProgress = false;
290
347
  }
291
- // Full teardown - detach CDP session and remove listeners
292
- this._cleanup().catch((err) => {
293
- // Cleanup errors during navigation are expected and non-critical
294
- const message = err instanceof Error ? err.message : String(err);
295
- const logFn = console.debug || console.log;
296
- logFn('[WebMCPClientTransport] Cleanup error during navigation (non-critical):', message);
297
- });
298
- // Signal clean disconnection (not an error)
299
- this.onclose?.();
300
348
  }
301
349
  /**
302
350
  * Handle server stopped signal
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcp-b/chrome-devtools-mcp",
3
- "version": "1.6.2",
3
+ "version": "1.6.3",
4
4
  "description": "MCP server for Chrome DevTools with WebMCP integration for connecting to website MCP tools",
5
5
  "keywords": [
6
6
  "mcp",