@mcp-b/chrome-devtools-mcp 1.3.1 → 1.5.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/README.md CHANGED
@@ -123,7 +123,7 @@ The AI can see the actual response, fix any bugs, and repeat until it works perf
123
123
  This creates a tight feedback loop where your AI assistant can:
124
124
  - **Write** WebMCP tools in your codebase
125
125
  - **Deploy** them automatically via hot-reload
126
- - **Discover** them through `diff_webmcp_tools`
126
+ - **Discover** them through `list_webmcp_tools`
127
127
  - **Test** them by calling tools directly by their prefixed names (e.g., `webmcp_localhost_3000_page0_search_products`)
128
128
  - **Debug** issues using console messages and snapshots
129
129
  - **Iterate** until the tool works correctly
@@ -476,7 +476,7 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles
476
476
  - [`take_screenshot`](docs/tool-reference.md#take_screenshot)
477
477
  - [`take_snapshot`](docs/tool-reference.md#take_snapshot)
478
478
  - **Website MCP Tools** (1 tool)
479
- - [`diff_webmcp_tools`](docs/tool-reference.md#diff_webmcp_tools) - List available website tools across all pages (with diff)
479
+ - [`list_webmcp_tools`](docs/tool-reference.md#list_webmcp_tools) - List available website tools across all pages (with diff)
480
480
 
481
481
  <!-- END AUTO GENERATED TOOLS -->
482
482
 
@@ -814,7 +814,7 @@ Navigate to https://example.com/app
814
814
  What tools are available on this website?
815
815
  ```
816
816
 
817
- The AI agent will use `diff_webmcp_tools` to show you what functionality the
817
+ The AI agent will use `list_webmcp_tools` to show you what functionality the
818
818
  website exposes. This automatically connects to the page's WebMCP server.
819
819
 
820
820
  **3. Use the tools directly**
@@ -840,13 +840,13 @@ By default, WebMCP tools are automatically registered as first-class MCP tools w
840
840
  | Claude Code | Yes | Full support for `tools/list_changed` |
841
841
  | GitHub Copilot | Yes | Supports list changed notifications |
842
842
  | Gemini CLI | Yes | Recently added support |
843
- | Cursor | No | Use `diff_webmcp_tools` to poll manually |
844
- | Cline | Partial | May need manual polling with `diff_webmcp_tools` |
845
- | Continue | Unknown | Use `diff_webmcp_tools` if tools don't appear |
843
+ | Cursor | No | Use `list_webmcp_tools` to poll manually |
844
+ | Cline | Partial | May need manual polling with `list_webmcp_tools` |
845
+ | Continue | Unknown | Use `list_webmcp_tools` if tools don't appear |
846
846
 
847
847
  **For clients without dynamic tool support:**
848
848
 
849
- If your MCP client doesn't support `tools/list_changed` notifications, use `diff_webmcp_tools` to manually see which tools are available, then call them directly by their prefixed names. The `diff_webmcp_tools` tool is diff-aware to reduce context pollution:
849
+ If your MCP client doesn't support `tools/list_changed` notifications, use `list_webmcp_tools` to manually see which tools are available, then call them directly by their prefixed names. The `list_webmcp_tools` tool is diff-aware to reduce context pollution:
850
850
  - First call returns the full tool list
851
851
  - Subsequent calls return only added/removed tools
852
852
  - Use `full: true` to force the complete list
@@ -872,9 +872,9 @@ Call the website's form submission tool to fill out the contact form
872
872
  - **"WebMCP not detected"**: The current webpage doesn't have `@mcp-b/global`
873
873
  installed or no tools are registered. The page needs the WebMCP polyfill loaded.
874
874
  - **Tool call fails**: Check the tool's input schema matches your parameters.
875
- Use `diff_webmcp_tools` to see the expected input format.
875
+ Use `list_webmcp_tools` to see the expected input format.
876
876
  - **Tools not appearing after navigation**: WebMCP auto-reconnects when you
877
- navigate. If the new page has different tools, call `diff_webmcp_tools` again.
877
+ navigate. If the new page has different tools, call `list_webmcp_tools` again.
878
878
 
879
879
  ## Related Packages
880
880
 
@@ -10,7 +10,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
10
10
  import { extractUrlLikeFromDevToolsTitle, urlsEqual } from './DevtoolsUtils.js';
11
11
  import { ToolListChangedNotificationSchema } from './third_party/index.js';
12
12
  import { NetworkCollector, ConsoleCollector } from './PageCollector.js';
13
- import { WEB_MCP_BRIDGE_SCRIPT, CHECK_WEBMCP_AVAILABLE_SCRIPT } from './transports/WebMCPBridgeScript.js';
13
+ import { WEB_MCP_BRIDGE_SCRIPT } from './transports/WebMCPBridgeScript.js';
14
14
  import { WebMCPClientTransport } from './transports/WebMCPClientTransport.js';
15
15
  import { Locator } from './third_party/index.js';
16
16
  import { listPages } from './tools/pages.js';
@@ -219,11 +219,11 @@ export class McpContext {
219
219
  newUrl === 'about:blank') {
220
220
  return;
221
221
  }
222
- // Wait a bit for the page to initialize WebMCP
223
- // The bridge script runs on DOMContentLoaded, and WebMCP may initialize after that
224
- await new Promise(resolve => setTimeout(resolve, 500));
225
- // Proactively check for WebMCP and sync tools
226
- await this.#proactivelyDetectWebMCP(page);
222
+ // Immediately try to connect - no polling needed
223
+ // If no WebMCP, connection will timeout gracefully
224
+ this.#tryConnectWebMCP(page).catch(err => {
225
+ this.logger('WebMCP connection attempt failed (expected if page has no WebMCP):', err);
226
+ });
227
227
  };
228
228
  page.on('framenavigated', onFrameNavigated);
229
229
  // Clean up listener when page closes
@@ -234,33 +234,37 @@ export class McpContext {
234
234
  this.logger(`WebMCP auto-detection listener installed for page: ${url}`);
235
235
  }
236
236
  /**
237
- * Proactively detect WebMCP on a page and sync tools if available.
238
- * This is called after page navigation to automatically discover WebMCP tools.
237
+ * Attempt to connect to WebMCP on a page without pre-checking.
238
+ * Uses the extension's approach: just try to connect, handle failure gracefully.
239
+ *
240
+ * This matches the WebMCP extension's behavior:
241
+ * - No pre-flight detection polling
242
+ * - Immediate connection attempt
243
+ * - Graceful handling if no server exists
244
+ * - Notification-based syncing when tools appear later
245
+ *
246
+ * Reference: /WebMCP/apps/extension/entrypoints/content/connection.ts lines 88-118
239
247
  */
240
- async #proactivelyDetectWebMCP(page) {
248
+ async #tryConnectWebMCP(page) {
241
249
  // Skip if tool hub is not enabled
242
250
  if (!this.#toolHub?.isEnabled()) {
243
251
  return;
244
252
  }
245
253
  try {
246
- // Check if WebMCP is available on the page
247
- const hasWebMCP = await this.#checkWebMCPAvailable(page);
248
- if (!hasWebMCP) {
249
- this.logger(`No WebMCP detected on page: ${page.url()}`);
250
- return;
251
- }
252
- this.logger(`WebMCP detected on page: ${page.url()}, connecting...`);
253
- // Connect and sync tools - this handles everything including sending list_changed
254
+ // Immediately try to get/create WebMCP client
255
+ // Transport is configured with requireWebMCP: false and 30s timeout in getWebMCPClient
254
256
  const result = await this.getWebMCPClient(page);
255
257
  if (result.connected) {
256
- this.logger(`WebMCP tools synced for page: ${page.url()}`);
258
+ this.logger(`WebMCP connected for page: ${page.url()}`);
257
259
  }
258
260
  else {
259
- this.logger(`Failed to connect to WebMCP: ${result.error}`);
261
+ // This is normal for pages without WebMCP
262
+ this.logger(`No WebMCP on page: ${page.url()}`);
260
263
  }
261
264
  }
262
265
  catch (err) {
263
- this.logger('Error during proactive WebMCP detection:', err);
266
+ // Connection timeout or error is expected on pages without WebMCP
267
+ this.logger(`WebMCP connection failed for ${page.url()} (normal if page has no WebMCP):`, err);
264
268
  }
265
269
  }
266
270
  /**
@@ -270,8 +274,10 @@ export class McpContext {
270
274
  async #setupWebMCPAutoDetectionForAllPages() {
271
275
  for (const page of this.#pages) {
272
276
  this.#setupWebMCPAutoDetection(page);
273
- // Also do an initial check for existing pages
274
- await this.#proactivelyDetectWebMCP(page);
277
+ // Try to connect immediately (don't await - run in parallel for all pages)
278
+ this.#tryConnectWebMCP(page).catch(err => {
279
+ this.logger('Initial WebMCP connection attempt failed (expected):', err);
280
+ });
275
281
  }
276
282
  }
277
283
  /**
@@ -358,6 +364,33 @@ export class McpContext {
358
364
  this.#setupWebMCPAutoDetection(page);
359
365
  return page;
360
366
  }
367
+ async newWindow() {
368
+ // Use CDP to create a new browser window instead of just a tab
369
+ const browserTarget = this.browser.target();
370
+ const cdpSession = await browserTarget.createCDPSession();
371
+ // Create a new target with newWindow: true
372
+ const { targetId } = await cdpSession.send('Target.createTarget', {
373
+ url: 'about:blank',
374
+ newWindow: true,
375
+ });
376
+ await cdpSession.detach();
377
+ // Wait for the new page to be available
378
+ const target = await this.browser.waitForTarget(target => {
379
+ // @ts-expect-error _targetId is internal but stable
380
+ return target._targetId === targetId;
381
+ }, { timeout: 5000 });
382
+ const page = await target.page();
383
+ if (!page) {
384
+ throw new Error('Failed to get page from new window target');
385
+ }
386
+ await this.createPagesSnapshot();
387
+ this.selectPage(page);
388
+ this.#networkCollector.addPage(page);
389
+ this.#consoleCollector.addPage(page);
390
+ // Set up WebMCP auto-detection for the new page
391
+ this.#setupWebMCPAutoDetection(page);
392
+ return page;
393
+ }
361
394
  async closePage(pageIdx) {
362
395
  if (this.#pages.length === 1) {
363
396
  throw new Error(CLOSE_PAGE_ERROR);
@@ -547,7 +580,7 @@ export class McpContext {
547
580
  }
548
581
  }
549
582
  getPages() {
550
- return this.#pages;
583
+ return [...this.#pages];
551
584
  }
552
585
  getDevToolsPage(page) {
553
586
  return this.#pageToDevToolsPage.get(page);
@@ -716,17 +749,12 @@ export class McpContext {
716
749
  }
717
750
  this.#webMCPConnections.delete(targetPage);
718
751
  }
719
- // Check if WebMCP is available
720
- const hasWebMCP = await this.#checkWebMCPAvailable(targetPage);
721
- if (!hasWebMCP) {
722
- return { connected: false, error: 'WebMCP not detected on this page' };
723
- }
724
- // Connect
752
+ // Connect - no pre-checking needed (extension approach)
725
753
  try {
726
754
  const transport = new WebMCPClientTransport({
727
755
  page: targetPage,
728
- readyTimeout: 10000,
729
- requireWebMCP: false, // We already checked
756
+ readyTimeout: 30000, // 30s to handle slow React apps (up from 10s)
757
+ requireWebMCP: false, // Don't pre-check, just try to connect
730
758
  });
731
759
  const client = new Client({ name: 'chrome-devtools-mcp', version: '1.0.0' }, { capabilities: {} });
732
760
  // Set up onclose handler to clean up connection state
@@ -775,19 +803,6 @@ export class McpContext {
775
803
  };
776
804
  }
777
805
  }
778
- /**
779
- * Check if WebMCP is available on a page by checking the bridge's hasWebMCP() method.
780
- * The bridge is auto-injected into all pages, so we just need to check if it detected WebMCP.
781
- */
782
- async #checkWebMCPAvailable(page) {
783
- try {
784
- const result = await page.evaluate(CHECK_WEBMCP_AVAILABLE_SCRIPT);
785
- return result.available;
786
- }
787
- catch {
788
- return false;
789
- }
790
- }
791
806
  /**
792
807
  * We need to ignore favicon request as they make our test flaky
793
808
  */
@@ -17,6 +17,7 @@ export class McpResponse {
17
17
  #attachedConsoleMessageId;
18
18
  #textResponseLines = [];
19
19
  #images = [];
20
+ #isError = false;
20
21
  #networkRequestsOptions;
21
22
  #consoleDataOptions;
22
23
  #devToolsData;
@@ -96,6 +97,12 @@ export class McpResponse {
96
97
  appendResponseLine(value) {
97
98
  this.#textResponseLines.push(value);
98
99
  }
100
+ setIsError(value) {
101
+ this.#isError = value;
102
+ }
103
+ get isError() {
104
+ return this.#isError;
105
+ }
99
106
  attachImage(value) {
100
107
  this.#images.push(value);
101
108
  }
File without changes
package/build/src/main.js CHANGED
@@ -12,7 +12,7 @@ import { logger, saveLogsToFile } from './logger.js';
12
12
  import { McpContext } from './McpContext.js';
13
13
  import { McpResponse } from './McpResponse.js';
14
14
  import { Mutex } from './Mutex.js';
15
- import { McpServer, StdioServerTransport, SetLevelRequestSchema, } from './third_party/index.js';
15
+ import { McpServer, StdioServerTransport, SetLevelRequestSchema, zod, } from './third_party/index.js';
16
16
  import { registerPrompts } from './prompts/index.js';
17
17
  import { ToolCategory } from './tools/categories.js';
18
18
  import { tools } from './tools/tools.js';
@@ -120,6 +120,10 @@ async function getContext() {
120
120
  experimentalDevToolsDebugging: devtools,
121
121
  experimentalIncludeAllPages: args.experimentalIncludeAllPages,
122
122
  });
123
+ // Always create a new window for this MCP session
124
+ // This ensures multiple MCP clients don't step on each other's toes
125
+ await context.newWindow();
126
+ logger('Created new window for this MCP session');
123
127
  // Initialize WebMCP tool hub for dynamic tool registration
124
128
  const toolHub = new WebMCPToolHub(server, context);
125
129
  context.setToolHub(toolHub);
@@ -164,7 +168,10 @@ function registerTool(tool) {
164
168
  }
165
169
  server.registerTool(tool.name, {
166
170
  description: tool.description,
167
- inputSchema: tool.schema,
171
+ // For call_webmcp_tool, use a fully permissive schema to allow any properties
172
+ inputSchema: tool.name === 'call_webmcp_tool'
173
+ ? zod.record(zod.unknown())
174
+ : zod.object(tool.schema).passthrough(),
168
175
  annotations: tool.annotations,
169
176
  }, async (params) => {
170
177
  const guard = await toolMutex.acquire();
@@ -178,9 +185,13 @@ function registerTool(tool) {
178
185
  params,
179
186
  }, response, context);
180
187
  const content = await response.handle(tool.name, context);
181
- return {
188
+ const result = {
182
189
  content,
183
190
  };
191
+ if (response.isError) {
192
+ result.isError = true;
193
+ }
194
+ return result;
184
195
  }
185
196
  catch (err) {
186
197
  logger(`${tool.name} error:`, err, err?.stack);
@@ -0,0 +1,44 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { createRequire } from 'node:module';
7
+ import { readFileSync } from 'node:fs';
8
+ const require = createRequire(import.meta.url);
9
+ let cachedPolyfill = null;
10
+ /**
11
+ * Load the @mcp-b/global IIFE polyfill code.
12
+ * This polyfill provides the navigator.modelContext API for pages that don't have WebMCP.
13
+ * Cached after first load for performance.
14
+ *
15
+ * @returns The IIFE JavaScript code as a string
16
+ * @throws Error if the polyfill file cannot be found or read
17
+ */
18
+ export function getPolyfillCode() {
19
+ if (cachedPolyfill) {
20
+ return cachedPolyfill;
21
+ }
22
+ try {
23
+ // Use Node's module resolution to find the package regardless of
24
+ // workspace structure or installation method (pnpm, npm, yarn).
25
+ // Use the package's exported subpath '@mcp-b/global/iife' which resolves
26
+ // to the IIFE bundle via the exports field in package.json.
27
+ const polyfillPath = require.resolve('@mcp-b/global/iife');
28
+ cachedPolyfill = readFileSync(polyfillPath, 'utf-8');
29
+ return cachedPolyfill;
30
+ }
31
+ catch (err) {
32
+ const message = err instanceof Error ? err.message : String(err);
33
+ // Provide actionable error message
34
+ throw new Error(`Could not find @mcp-b/global polyfill: ${message}\n` +
35
+ 'Run `pnpm build` in the global package or ensure @mcp-b/global is installed.');
36
+ }
37
+ }
38
+ /**
39
+ * Clear the cached polyfill code, forcing the next getPolyfillCode() call
40
+ * to reload from disk. Useful for testing or after @mcp-b/global updates.
41
+ */
42
+ export function clearPolyfillCache() {
43
+ cachedPolyfill = null;
44
+ }
@@ -27,7 +27,7 @@ export function registerPrompts(server) {
27
27
  1. **Write the Tool**: I'll ask you to create a WebMCP tool in my codebase using @mcp-b/global
28
28
  2. **Hot Reload**: My dev server will automatically reload with the new tool
29
29
  3. **Navigate**: Use navigate_page to open my dev server (e.g., http://localhost:3000)
30
- 4. **Discover**: Use diff_webmcp_tools to see registered tools (shown with callable names like webmcp_localhost_3000_page0_my_tool)
30
+ 4. **Discover**: Use list_webmcp_tools to see registered tools (shown with callable names like webmcp_localhost_3000_page0_my_tool)
31
31
  5. **Test**: Call the tool directly by its prefixed name (e.g., webmcp_localhost_3000_page0_my_tool)
32
32
  6. **Iterate**: If something is wrong, fix the code and repeat
33
33
 
@@ -92,7 +92,7 @@ What would you like to build? Describe the tool you need and I'll help you imple
92
92
  ## Test Plan
93
93
 
94
94
  1. Navigate to ${url}
95
- 2. Use diff_webmcp_tools to discover registered tools (shown with callable names like webmcp_localhost_3000_page0_tool_name)
95
+ 2. Use list_webmcp_tools to discover registered tools (shown with callable names like webmcp_localhost_3000_page0_tool_name)
96
96
  3. ${toolNameInstruction}
97
97
  4. Call the tool directly by its prefixed name and test with various inputs:
98
98
  - Valid inputs (happy path)
@@ -138,7 +138,7 @@ ${urlInstruction}
138
138
  - Errors loading @mcp-b/global
139
139
  - Tool registration errors
140
140
  - Any JavaScript errors
141
- 3. **Test WebMCP**: Try diff_webmcp_tools to see if connection works
141
+ 3. **Test WebMCP**: Try list_webmcp_tools to see if connection works
142
142
  4. **Verify registration**: If no tools appear, check if the code properly imports '@mcp-b/global' and calls registerTool
143
143
 
144
144
  ## Common Issues