@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.
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![npm @mcp-b/chrome-devtools-mcp package](https://img.shields.io/npm/v/@mcp-b/chrome-devtools-mcp.svg)](https://www.npmjs.com/package/@mcp-b/chrome-devtools-mcp)
6
6
  [![npm downloads](https://img.shields.io/npm/dm/@mcp-b/chrome-devtools-mcp?style=flat-square)](https://www.npmjs.com/package/@mcp-b/chrome-devtools-mcp)
7
7
  [![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg?style=flat-square)](https://opensource.org/licenses/Apache-2.0)
8
- [![28 Tools](https://img.shields.io/badge/MCP_Tools-28-green?style=flat-square)](./docs/tool-reference.md)
8
+ [![27 Tools](https://img.shields.io/badge/MCP_Tools-27-green?style=flat-square)](./docs/tool-reference.md)
9
9
  [![Chrome](https://img.shields.io/badge/Chrome-DevTools-4285F4?style=flat-square&logo=googlechrome)](https://developer.chrome.com/docs/devtools/)
10
10
 
11
11
  📖 **[WebMCP Documentation](https://docs.mcp-b.ai)** | 🚀 **[Quick Start](https://docs.mcp-b.ai/quickstart)** | 🔌 **[Connecting Agents](https://docs.mcp-b.ai/connecting-agents)** | 🎯 **[Chrome DevTools Quickstart](https://github.com/WebMCP-org/chrome-devtools-quickstart)**
@@ -21,7 +21,7 @@
21
21
 
22
22
  | Feature | Benefit |
23
23
  |---------|---------|
24
- | **28 MCP Tools** | Comprehensive browser control - navigation, input, screenshots, performance, debugging |
24
+ | **27 MCP Tools** | Comprehensive browser control - navigation, input, screenshots, performance, debugging |
25
25
  | **WebMCP Integration** | Connect to website-specific AI tools via `@mcp-b/global` |
26
26
  | **Performance Analysis** | Chrome DevTools-powered performance insights and trace recording |
27
27
  | **Reliable Automation** | Puppeteer-based with automatic waiting for action results |
@@ -56,7 +56,7 @@ This fork adds **WebMCP integration** - the ability to call MCP tools that are r
56
56
  | **List website MCP tools** | ❌ | ✅ |
57
57
  | **AI-driven tool development** | ❌ | ✅ |
58
58
 
59
- The key addition is the `list_webmcp_tools` and `call_webmcp_tool` tools that let your AI agent interact with MCP tools that websites expose via [@mcp-b/global](https://www.npmjs.com/package/@mcp-b/global).
59
+ The key addition is automatic WebMCP tool discovery and registration. When you visit a page with [@mcp-b/global](https://www.npmjs.com/package/@mcp-b/global), its tools are automatically registered as first-class MCP tools that your AI agent can call directly.
60
60
 
61
61
  ## AI-Driven Development Workflow
62
62
 
@@ -123,8 +123,8 @@ 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 `list_webmcp_tools`
127
- - **Test** them through `call_webmcp_tool`
126
+ - **Discover** them through `diff_webmcp_tools`
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
130
130
 
@@ -475,9 +475,8 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles
475
475
  - [`list_console_messages`](docs/tool-reference.md#list_console_messages)
476
476
  - [`take_screenshot`](docs/tool-reference.md#take_screenshot)
477
477
  - [`take_snapshot`](docs/tool-reference.md#take_snapshot)
478
- - **Website MCP Tools** (2 tools)
479
- - [`list_webmcp_tools`](docs/tool-reference.md#list_webmcp_tools) - List available website tools (auto-connects)
480
- - [`call_webmcp_tool`](docs/tool-reference.md#call_webmcp_tool) - Call a website tool (auto-connects)
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)
481
480
 
482
481
  <!-- END AUTO GENERATED TOOLS -->
483
482
 
@@ -674,6 +673,14 @@ all instances of `@mcp-b/chrome-devtools-mcp`. Set the `isolated` option to `tru
674
673
  to use a temporary user data dir instead which will be cleared automatically after
675
674
  the browser is closed.
676
675
 
676
+ > [!NOTE]
677
+ > When using a shared user data directory (non-isolated), the server launches
678
+ > Chrome with a local remote debugging port (`--remote-debugging-port=0` and
679
+ > `--remote-debugging-address=127.0.0.1`) so it can auto-connect on future runs.
680
+ > This means any local process can attach to that port. If you prefer pipe-only
681
+ > mode, pass `--chrome-arg=--remote-debugging-pipe` (auto-connect across runs
682
+ > will be disabled).
683
+
677
684
  ### Connecting to a running Chrome instance
678
685
 
679
686
  You can connect to a running Chrome instance by using the `--browser-url` option. This is useful if you want to use your existing Chrome profile or if you are running the MCP server in a sandboxed environment that does not allow starting a new Chrome instance.
@@ -807,20 +814,42 @@ Navigate to https://example.com/app
807
814
  What tools are available on this website?
808
815
  ```
809
816
 
810
- The AI agent will use `list_webmcp_tools` to show you what functionality the
817
+ The AI agent will use `diff_webmcp_tools` to show you what functionality the
811
818
  website exposes. This automatically connects to the page's WebMCP server.
812
819
 
813
- **3. Use the tools**
820
+ **3. Use the tools directly**
814
821
 
815
822
  ```
816
823
  Search for "wireless headphones" using the website's search tool
817
824
  ```
818
825
 
819
- The AI agent will use `call_webmcp_tool` to invoke the website's functionality.
826
+ The AI agent will call the tool directly by its prefixed name (e.g., `webmcp_example_com_page0_search_products`).
827
+ WebMCP tools are registered as first-class MCP tools, so they appear directly in your agent's tool list.
820
828
 
821
829
  That's it! No explicit connect or disconnect steps needed - WebMCP tools
822
- auto-connect when called and automatically reconnect when you navigate to
823
- a different page.
830
+ auto-connect when detected and automatically update when you navigate.
831
+
832
+ ### Dynamic Tool Registration
833
+
834
+ By default, WebMCP tools are automatically registered as first-class MCP tools when detected on a webpage. This means tools like `search_products` appear directly in your MCP client's tool list with prefixed names like `webmcp_localhost_3000_page0_search_products`.
835
+
836
+ **MCP Client Compatibility:**
837
+
838
+ | Client | Dynamic Tool Updates | Notes |
839
+ |--------|---------------------|-------|
840
+ | Claude Code | Yes | Full support for `tools/list_changed` |
841
+ | GitHub Copilot | Yes | Supports list changed notifications |
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 |
846
+
847
+ **For clients without dynamic tool support:**
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:
850
+ - First call returns the full tool list
851
+ - Subsequent calls return only added/removed tools
852
+ - Use `full: true` to force the complete list
824
853
 
825
854
  ### Example prompts
826
855
 
@@ -843,9 +872,9 @@ Call the website's form submission tool to fill out the contact form
843
872
  - **"WebMCP not detected"**: The current webpage doesn't have `@mcp-b/global`
844
873
  installed or no tools are registered. The page needs the WebMCP polyfill loaded.
845
874
  - **Tool call fails**: Check the tool's input schema matches your parameters.
846
- Use `list_webmcp_tools` to see the expected input format.
875
+ Use `diff_webmcp_tools` to see the expected input format.
847
876
  - **Tools not appearing after navigation**: WebMCP auto-reconnects when you
848
- navigate. If the new page has different tools, call `list_webmcp_tools` again.
877
+ navigate. If the new page has different tools, call `diff_webmcp_tools` again.
849
878
 
850
879
  ## Related Packages
851
880
 
@@ -8,6 +8,7 @@ import os from 'node:os';
8
8
  import path from 'node:path';
9
9
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
10
10
  import { extractUrlLikeFromDevToolsTitle, urlsEqual } from './DevtoolsUtils.js';
11
+ import { ToolListChangedNotificationSchema } from './third_party/index.js';
11
12
  import { NetworkCollector, ConsoleCollector } from './PageCollector.js';
12
13
  import { WEB_MCP_BRIDGE_SCRIPT, CHECK_WEBMCP_AVAILABLE_SCRIPT } from './transports/WebMCPBridgeScript.js';
13
14
  import { WebMCPClientTransport } from './transports/WebMCPClientTransport.js';
@@ -16,8 +17,16 @@ import { listPages } from './tools/pages.js';
16
17
  import { takeSnapshot } from './tools/snapshot.js';
17
18
  import { CLOSE_PAGE_ERROR } from './tools/ToolDefinition.js';
18
19
  import { WaitForHelper } from './WaitForHelper.js';
20
+ /** Default timeout for page operations in milliseconds. */
19
21
  const DEFAULT_TIMEOUT = 5_000;
22
+ /** Default timeout for navigation operations in milliseconds. */
20
23
  const NAVIGATION_TIMEOUT = 10_000;
24
+ /**
25
+ * Get the timeout multiplier for a given network condition.
26
+ *
27
+ * @param condition - The network condition name (e.g., "Fast 4G", "Slow 3G").
28
+ * @returns Multiplier to apply to timeouts (1 = no slowdown, 10 = max slowdown).
29
+ */
21
30
  function getNetworkMultiplierFromString(condition) {
22
31
  const puppeteerCondition = condition;
23
32
  switch (puppeteerCondition) {
@@ -32,6 +41,13 @@ function getNetworkMultiplierFromString(condition) {
32
41
  }
33
42
  return 1;
34
43
  }
44
+ /**
45
+ * Get the file extension for a given MIME type.
46
+ *
47
+ * @param mimeType - The MIME type (e.g., "image/png").
48
+ * @returns The corresponding file extension without the dot.
49
+ * @throws Error if the MIME type is not supported.
50
+ */
35
51
  function getExtensionFromMimeType(mimeType) {
36
52
  switch (mimeType) {
37
53
  case 'image/png':
@@ -43,14 +59,23 @@ function getExtensionFromMimeType(mimeType) {
43
59
  }
44
60
  throw new Error(`No mapping for Mime type ${mimeType}.`);
45
61
  }
62
+ /**
63
+ * Central context for MCP operations on a browser instance.
64
+ *
65
+ * Manages page state, accessibility snapshots, network/console collection,
66
+ * WebMCP connections, and tool registration. This class serves as the primary
67
+ * interface between MCP tools and the browser.
68
+ */
46
69
  export class McpContext {
47
70
  browser;
48
71
  logger;
49
- // The most recent page state.
72
+ /** Cached list of available pages (refreshed by createPagesSnapshot). */
50
73
  #pages = [];
74
+ /** Mapping of content pages to their associated DevTools inspector pages. */
51
75
  #pageToDevToolsPage = new Map();
76
+ /** Currently selected page for tool operations. */
52
77
  #selectedPage;
53
- // The most recent snapshot.
78
+ /** Most recent accessibility snapshot of the selected page. */
54
79
  #textSnapshot = null;
55
80
  #networkCollector;
56
81
  #consoleCollector;
@@ -64,6 +89,9 @@ export class McpContext {
64
89
  #locatorClass;
65
90
  #options;
66
91
  #webMCPConnections = new WeakMap();
92
+ #toolHub;
93
+ /** Tracks pages that have WebMCP auto-detection listeners installed. */
94
+ #pagesWithWebMCPListeners = new WeakSet();
67
95
  constructor(browser, logger, options, locatorClass) {
68
96
  this.browser = browser;
69
97
  this.logger = logger;
@@ -141,9 +169,122 @@ export class McpContext {
141
169
  this.#networkCollector.dispose();
142
170
  this.#consoleCollector.dispose();
143
171
  }
144
- static async from(browser, logger, opts,
145
- /* Let tests use unbundled Locator class to avoid overly strict checks within puppeteer that fail when mixing bundled and unbundled class instances */
146
- locatorClass = Locator) {
172
+ /**
173
+ * Set the WebMCPToolHub for dynamic tool registration.
174
+ * This enables automatic registration of WebMCP tools as native MCP tools.
175
+ * Also sets up auto-detection for all existing pages.
176
+ */
177
+ setToolHub(hub) {
178
+ this.#toolHub = hub;
179
+ // Trigger auto-detection for all existing pages asynchronously
180
+ this.#setupWebMCPAutoDetectionForAllPages().catch(err => {
181
+ this.logger('Error setting up WebMCP auto-detection:', err);
182
+ });
183
+ }
184
+ /**
185
+ * Get the WebMCPToolHub instance (for testing purposes)
186
+ */
187
+ getToolHub() {
188
+ return this.#toolHub;
189
+ }
190
+ /**
191
+ * Set up automatic WebMCP detection for a page.
192
+ * This installs listeners that detect WebMCP after navigation and sync tools.
193
+ */
194
+ #setupWebMCPAutoDetection(page) {
195
+ // Skip if listeners already installed
196
+ if (this.#pagesWithWebMCPListeners.has(page)) {
197
+ return;
198
+ }
199
+ // Skip chrome:// and devtools:// pages
200
+ const url = page.url();
201
+ if (url.startsWith('chrome://') ||
202
+ url.startsWith('chrome-extension://') ||
203
+ url.startsWith('devtools://')) {
204
+ return;
205
+ }
206
+ this.#pagesWithWebMCPListeners.add(page);
207
+ // Handler for frame navigation - detect WebMCP after main frame navigates
208
+ const onFrameNavigated = async (frame) => {
209
+ // Only handle main frame navigation
210
+ // @ts-expect-error Frame type not exported
211
+ if (frame.parentFrame?.() !== null) {
212
+ return;
213
+ }
214
+ // Skip internal pages
215
+ const newUrl = page.url();
216
+ if (newUrl.startsWith('chrome://') ||
217
+ newUrl.startsWith('chrome-extension://') ||
218
+ newUrl.startsWith('devtools://') ||
219
+ newUrl === 'about:blank') {
220
+ return;
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);
227
+ };
228
+ page.on('framenavigated', onFrameNavigated);
229
+ // Clean up listener when page closes
230
+ page.once('close', () => {
231
+ page.off('framenavigated', onFrameNavigated);
232
+ this.#pagesWithWebMCPListeners.delete(page);
233
+ });
234
+ this.logger(`WebMCP auto-detection listener installed for page: ${url}`);
235
+ }
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.
239
+ */
240
+ async #proactivelyDetectWebMCP(page) {
241
+ // Skip if tool hub is not enabled
242
+ if (!this.#toolHub?.isEnabled()) {
243
+ return;
244
+ }
245
+ 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
+ const result = await this.getWebMCPClient(page);
255
+ if (result.connected) {
256
+ this.logger(`WebMCP tools synced for page: ${page.url()}`);
257
+ }
258
+ else {
259
+ this.logger(`Failed to connect to WebMCP: ${result.error}`);
260
+ }
261
+ }
262
+ catch (err) {
263
+ this.logger('Error during proactive WebMCP detection:', err);
264
+ }
265
+ }
266
+ /**
267
+ * Set up WebMCP auto-detection for all current pages.
268
+ * Called during initialization and when tool hub is set.
269
+ */
270
+ async #setupWebMCPAutoDetectionForAllPages() {
271
+ for (const page of this.#pages) {
272
+ this.#setupWebMCPAutoDetection(page);
273
+ // Also do an initial check for existing pages
274
+ await this.#proactivelyDetectWebMCP(page);
275
+ }
276
+ }
277
+ /**
278
+ * Create a new McpContext instance.
279
+ *
280
+ * @param browser - Puppeteer browser instance to operate on.
281
+ * @param logger - Debug logger for internal operations.
282
+ * @param opts - Configuration options.
283
+ * @param locatorClass - Locator class to use (injectable for testing with
284
+ * unbundled Puppeteer to avoid class instance mismatch errors).
285
+ * @returns Initialized McpContext ready for use.
286
+ */
287
+ static async from(browser, logger, opts, locatorClass = Locator) {
147
288
  const context = new McpContext(browser, logger, opts, locatorClass);
148
289
  await context.#init();
149
290
  return context;
@@ -164,6 +305,14 @@ export class McpContext {
164
305
  }
165
306
  return this.#networkCollector.getIdForResource(request);
166
307
  }
308
+ /**
309
+ * Resolve a CDP backend node ID to a snapshot element UID.
310
+ *
311
+ * @param cdpBackendNodeId - The CDP backend node ID from DevTools.
312
+ * @returns The corresponding snapshot UID, or undefined if not found.
313
+ *
314
+ * @todo Optimize with a backendNodeId index instead of tree traversal.
315
+ */
167
316
  resolveCdpElementId(cdpBackendNodeId) {
168
317
  if (!cdpBackendNodeId) {
169
318
  this.logger('no cdpBackendNodeId');
@@ -173,7 +322,6 @@ export class McpContext {
173
322
  this.logger('no text snapshot');
174
323
  return;
175
324
  }
176
- // TODO: index by backendNodeId instead.
177
325
  const queue = [this.#textSnapshot.root];
178
326
  while (queue.length) {
179
327
  const current = queue.pop();
@@ -206,6 +354,8 @@ export class McpContext {
206
354
  this.selectPage(page);
207
355
  this.#networkCollector.addPage(page);
208
356
  this.#consoleCollector.addPage(page);
357
+ // Set up WebMCP auto-detection for the new page
358
+ this.#setupWebMCPAutoDetection(page);
209
359
  return page;
210
360
  }
211
361
  async closePage(pageIdx) {
@@ -352,8 +502,21 @@ export class McpContext {
352
502
  this.selectPage(this.#pages[0]);
353
503
  }
354
504
  await this.detectOpenDevToolsWindows();
505
+ // Set up WebMCP auto-detection for any new pages
506
+ // (safe to call for existing pages - it checks if listeners are already installed)
507
+ for (const page of this.#pages) {
508
+ this.#setupWebMCPAutoDetection(page);
509
+ }
355
510
  return this.#pages;
356
511
  }
512
+ /**
513
+ * Detect and map open DevTools windows to their inspected pages.
514
+ *
515
+ * Iterates through all browser pages to find DevTools windows and
516
+ * associates them with the pages they're inspecting.
517
+ *
518
+ * @todo Optimize page lookup with a URL-indexed map instead of nested loops.
519
+ */
357
520
  async detectOpenDevToolsWindows() {
358
521
  this.logger('Detecting open DevTools windows');
359
522
  const pages = await this.browser.pages(this.#options.experimentalIncludeAllPages);
@@ -371,7 +534,6 @@ export class McpContext {
371
534
  if (!urlLike) {
372
535
  continue;
373
536
  }
374
- // TODO: lookup without a loop.
375
537
  for (const page of this.#pages) {
376
538
  if (urlsEqual(page.url(), urlLike)) {
377
539
  this.#pageToDevToolsPage.set(page, devToolsPage);
@@ -574,10 +736,36 @@ export class McpContext {
574
736
  if (currentConn?.client === client) {
575
737
  this.#webMCPConnections.delete(targetPage);
576
738
  }
739
+ // Remove tools for this page when transport closes
740
+ this.#toolHub?.removeToolsForPage(targetPage);
577
741
  };
742
+ // Also listen for page close events to trigger cleanup
743
+ // This handles cases where the page is closed without navigation
744
+ const onPageClose = () => {
745
+ const currentConn = this.#webMCPConnections.get(targetPage);
746
+ if (currentConn?.client === client) {
747
+ this.#webMCPConnections.delete(targetPage);
748
+ }
749
+ this.#toolHub?.removeToolsForPage(targetPage);
750
+ // Clean up the listener
751
+ targetPage.off('close', onPageClose);
752
+ };
753
+ targetPage.on('close', onPageClose);
578
754
  await client.connect(transport);
579
755
  // Store connection for this page
580
756
  this.#webMCPConnections.set(targetPage, { client, transport, page: targetPage });
757
+ // Subscribe to tool list changes if tool hub is enabled and server supports it
758
+ const serverCapabilities = client.getServerCapabilities();
759
+ if (serverCapabilities?.tools?.listChanged && this.#toolHub?.isEnabled()) {
760
+ client.setNotificationHandler(ToolListChangedNotificationSchema, async () => {
761
+ this.logger('WebMCP tools changed, re-syncing...');
762
+ await this.#toolHub?.syncToolsForPage(targetPage, client);
763
+ });
764
+ }
765
+ // Initial tool sync if tool hub is enabled
766
+ if (this.#toolHub?.isEnabled()) {
767
+ await this.#toolHub.syncToolsForPage(targetPage, client);
768
+ }
581
769
  return { connected: true, client };
582
770
  }
583
771
  catch (err) {
@@ -8,7 +8,16 @@ import os from 'node:os';
8
8
  import path from 'node:path';
9
9
  import { logger } from './logger.js';
10
10
  import { puppeteer } from './third_party/index.js';
11
+ /** Cached browser instance for reuse across calls. */
11
12
  let browser;
13
+ /**
14
+ * Create a target filter for Puppeteer that excludes internal Chrome pages.
15
+ *
16
+ * Includes new tab and inspect pages (may be the only user-accessible page),
17
+ * but excludes chrome://, chrome-extension://, and chrome-untrusted:// pages.
18
+ *
19
+ * @returns A filter function for Puppeteer's targetFilter option.
20
+ */
12
21
  function makeTargetFilter() {
13
22
  const ignoredPrefixes = new Set([
14
23
  'chrome://',
@@ -19,7 +28,6 @@ function makeTargetFilter() {
19
28
  if (target.url() === 'chrome://newtab/') {
20
29
  return true;
21
30
  }
22
- // Could be the only page opened in the browser.
23
31
  if (target.url().startsWith('chrome://inspect')) {
24
32
  return true;
25
33
  }
@@ -31,6 +39,19 @@ function makeTargetFilter() {
31
39
  return true;
32
40
  };
33
41
  }
42
+ /**
43
+ * Connect to an existing Chrome browser instance.
44
+ *
45
+ * Connection priority:
46
+ * 1. wsEndpoint - Direct WebSocket connection with optional headers
47
+ * 2. browserURL - HTTP URL to Chrome's DevTools endpoint
48
+ * 3. userDataDir - Read DevToolsActivePort from profile directory
49
+ * 4. channel - Derive profile directory from channel name
50
+ *
51
+ * @param options - Connection options.
52
+ * @returns Connected browser instance.
53
+ * @throws Error if connection fails or no connection method specified.
54
+ */
34
55
  export async function ensureBrowserConnected(options) {
35
56
  const { channel } = options;
36
57
  if (browser?.connected) {
@@ -85,7 +106,32 @@ export async function ensureBrowserConnected(options) {
85
106
  if (!channel) {
86
107
  throw new Error('Channel must be provided if userDataDir is missing');
87
108
  }
88
- connectOptions.channel = (channel === 'stable' ? 'chrome' : `chrome-${channel}`);
109
+ // Derive the default userDataDir from the channel (same as launch does)
110
+ const profileDirName = channel && channel !== 'stable'
111
+ ? `chrome-profile-${channel}`
112
+ : 'chrome-profile';
113
+ const derivedUserDataDir = path.join(os.homedir(), '.cache', 'chrome-devtools-mcp', profileDirName);
114
+ // Try to read DevToolsActivePort from the derived userDataDir
115
+ const portPath = path.join(derivedUserDataDir, 'DevToolsActivePort');
116
+ try {
117
+ const fileContent = await fs.promises.readFile(portPath, 'utf8');
118
+ const [rawPort, rawPath] = fileContent
119
+ .split('\n')
120
+ .map(line => line.trim())
121
+ .filter(line => !!line);
122
+ if (!rawPort || !rawPath) {
123
+ throw new Error(`Invalid DevToolsActivePort '${fileContent}' found`);
124
+ }
125
+ const port = parseInt(rawPort, 10);
126
+ if (isNaN(port) || port <= 0 || port > 65535) {
127
+ throw new Error(`Invalid port '${rawPort}' found`);
128
+ }
129
+ const browserWSEndpoint = `ws://127.0.0.1:${port}${rawPath}`;
130
+ connectOptions.browserWSEndpoint = browserWSEndpoint;
131
+ }
132
+ catch (error) {
133
+ throw new Error(`Could not connect to Chrome ${channel} channel in ${derivedUserDataDir}. Check if Chrome is running and was launched with remote debugging enabled.`, { cause: error });
134
+ }
89
135
  }
90
136
  }
91
137
  else {
@@ -103,6 +149,13 @@ export async function ensureBrowserConnected(options) {
103
149
  logger('Connected Puppeteer');
104
150
  return browser;
105
151
  }
152
+ /**
153
+ * Launch a new Chrome browser instance.
154
+ *
155
+ * @param options - Launch configuration options.
156
+ * @returns Launched browser instance.
157
+ * @throws Error if Chrome is already running with the same profile.
158
+ */
106
159
  export async function launch(options) {
107
160
  const { channel, executablePath, headless, isolated } = options;
108
161
  const profileDirName = channel && channel !== 'stable'
@@ -115,10 +168,21 @@ export async function launch(options) {
115
168
  recursive: true,
116
169
  });
117
170
  }
171
+ const extraArgs = options.args ?? [];
172
+ const hasRemoteDebuggingPipe = extraArgs.includes('--remote-debugging-pipe');
173
+ const hasRemoteDebuggingPort = extraArgs.some(arg => arg.startsWith('--remote-debugging-port'));
174
+ const hasRemoteDebuggingAddress = extraArgs.some(arg => arg.startsWith('--remote-debugging-address'));
118
175
  const args = [
119
- ...(options.args ?? []),
176
+ ...extraArgs,
120
177
  '--hide-crash-restore-bubble',
121
178
  ];
179
+ const enableRemoteDebuggingPort = !isolated && !hasRemoteDebuggingPipe && !hasRemoteDebuggingPort;
180
+ if (enableRemoteDebuggingPort) {
181
+ args.push('--remote-debugging-port=0');
182
+ if (!hasRemoteDebuggingAddress) {
183
+ args.push('--remote-debugging-address=127.0.0.1');
184
+ }
185
+ }
122
186
  if (headless) {
123
187
  args.push('--screen-info={3840x2160}');
124
188
  }
@@ -132,6 +196,8 @@ export async function launch(options) {
132
196
  ? `chrome-${channel}`
133
197
  : 'chrome';
134
198
  }
199
+ const usePipe = hasRemoteDebuggingPipe ||
200
+ (!hasRemoteDebuggingPort && !enableRemoteDebuggingPort);
135
201
  try {
136
202
  const browser = await puppeteer.launch({
137
203
  channel: puppeteerChannel,
@@ -139,7 +205,7 @@ export async function launch(options) {
139
205
  executablePath,
140
206
  defaultViewport: null,
141
207
  userDataDir,
142
- pipe: true,
208
+ pipe: usePipe,
143
209
  headless,
144
210
  args,
145
211
  acceptInsecureCerts: options.acceptInsecureCerts,
@@ -171,6 +237,12 @@ export async function launch(options) {
171
237
  throw error;
172
238
  }
173
239
  }
240
+ /**
241
+ * Ensure a browser is launched, reusing existing instance if connected.
242
+ *
243
+ * @param options - Launch configuration options.
244
+ * @returns Connected or newly launched browser instance.
245
+ */
174
246
  export async function ensureBrowserLaunched(options) {
175
247
  if (browser?.connected) {
176
248
  return browser;
package/build/src/cli.js CHANGED
@@ -8,7 +8,6 @@ export const cliOptions = {
8
8
  autoConnect: {
9
9
  type: 'boolean',
10
10
  description: 'If specified, automatically connects to a browser (Chrome 145+) running in the user data directory identified by the channel param. Falls back to launching a new instance if no running browser is found.',
11
- conflicts: ['isolated', 'executablePath'],
12
11
  default: true,
13
12
  coerce: (value) => {
14
13
  if (value === false) {
package/build/src/main.js CHANGED
@@ -16,7 +16,11 @@ import { McpServer, StdioServerTransport, SetLevelRequestSchema, } from './third
16
16
  import { registerPrompts } from './prompts/index.js';
17
17
  import { ToolCategory } from './tools/categories.js';
18
18
  import { tools } from './tools/tools.js';
19
- // If moved update release-please config
19
+ import { WebMCPToolHub } from './tools/WebMCPToolHub.js';
20
+ /**
21
+ * Package version (managed by release-please).
22
+ * @remarks If moved, update release-please config.
23
+ */
20
24
  // x-release-please-start-version
21
25
  const VERSION = '0.12.1';
22
26
  // x-release-please-end
@@ -30,13 +34,24 @@ const server = new McpServer({
30
34
  name: 'chrome_devtools',
31
35
  title: 'Chrome DevTools MCP server',
32
36
  version: VERSION,
33
- }, { capabilities: { logging: {}, prompts: {} } });
37
+ }, { capabilities: { logging: {}, prompts: {}, tools: { listChanged: true } } });
34
38
  // Register WebMCP development prompts
35
39
  registerPrompts(server);
36
40
  server.server.setRequestHandler(SetLevelRequestSchema, () => {
37
41
  return {};
38
42
  });
43
+ /** Cached McpContext instance for the current browser. */
39
44
  let context;
45
+ /**
46
+ * Get or create the McpContext for browser operations.
47
+ *
48
+ * Handles browser connection/launch with the following priority:
49
+ * 1. Explicit browserUrl/wsEndpoint - connect directly
50
+ * 2. autoConnect enabled - try connecting, fall back to launching
51
+ * 3. Otherwise - launch a new browser
52
+ *
53
+ * @returns Initialized McpContext ready for tool operations.
54
+ */
40
55
  async function getContext() {
41
56
  const extraArgs = (args.chromeArg ?? []).map(String);
42
57
  if (args.proxyServer) {
@@ -105,15 +120,35 @@ async function getContext() {
105
120
  experimentalDevToolsDebugging: devtools,
106
121
  experimentalIncludeAllPages: args.experimentalIncludeAllPages,
107
122
  });
123
+ // Initialize WebMCP tool hub for dynamic tool registration
124
+ const toolHub = new WebMCPToolHub(server, context);
125
+ context.setToolHub(toolHub);
126
+ logger('WebMCPToolHub initialized for dynamic tool registration');
108
127
  }
109
128
  return context;
110
129
  }
130
+ /**
131
+ * Log security disclaimers to stderr.
132
+ *
133
+ * Warns users that browser content is exposed to MCP clients.
134
+ */
111
135
  const logDisclaimers = () => {
112
136
  console.error(`chrome-devtools-mcp exposes content of the browser instance to the MCP clients allowing them to inspect,
113
137
  debug, and modify any data in the browser or DevTools.
114
138
  Avoid sharing sensitive or personal information that you do not want to share with MCP clients.`);
115
139
  };
140
+ /**
141
+ * Mutex to serialize tool execution and prevent concurrent modifications.
142
+ */
116
143
  const toolMutex = new Mutex();
144
+ /**
145
+ * Register a tool with the MCP server.
146
+ *
147
+ * Handles category-based filtering (emulation, performance, network)
148
+ * and wraps the handler with context initialization and error handling.
149
+ *
150
+ * @param tool - Tool definition to register.
151
+ */
117
152
  function registerTool(tool) {
118
153
  if (tool.annotations.category === ToolCategory.EMULATION &&
119
154
  args.categoryEmulation === false) {