@mcp-b/chrome-devtools-mcp 1.3.0 → 1.4.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 +40 -11
- package/build/src/McpContext.js +206 -30
- package/build/src/McpResponse.js +7 -0
- package/build/src/browser.js +76 -4
- package/build/src/cli.js +1 -2
- package/build/src/main.js +47 -5
- package/build/src/polyfillLoader.js +44 -0
- package/build/src/prompts/index.js +4 -4
- package/build/src/third_party/index.js +1 -1
- package/build/src/tools/WebMCPToolHub.js +322 -0
- package/build/src/tools/webmcp.js +684 -41
- package/build/src/transports/WebMCPBridgeScript.js +13 -7
- package/build/src/transports/WebMCPClientTransport.js +188 -83
- package/build/src/transports/bridgeConstants.js +22 -0
- package/package.json +7 -4
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/@mcp-b/chrome-devtools-mcp)
|
|
6
6
|
[](https://www.npmjs.com/package/@mcp-b/chrome-devtools-mcp)
|
|
7
7
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
8
|
-
[](./docs/tool-reference.md)
|
|
9
9
|
[](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
|
-
| **
|
|
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
|
|
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
|
|
|
@@ -124,7 +124,7 @@ 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
126
|
- **Discover** them through `list_webmcp_tools`
|
|
127
|
-
- **Test** them
|
|
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** (
|
|
479
|
-
- [`list_webmcp_tools`](docs/tool-reference.md#list_webmcp_tools) - List available website tools (
|
|
480
|
-
- [`call_webmcp_tool`](docs/tool-reference.md#call_webmcp_tool) - Call a website tool (auto-connects)
|
|
478
|
+
- **Website MCP Tools** (1 tool)
|
|
479
|
+
- [`list_webmcp_tools`](docs/tool-reference.md#list_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.
|
|
@@ -810,17 +817,39 @@ What tools are available on this website?
|
|
|
810
817
|
The AI agent will use `list_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
|
|
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
|
|
823
|
-
|
|
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 `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
|
+
|
|
847
|
+
**For clients without dynamic tool support:**
|
|
848
|
+
|
|
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
|
+
- 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
|
|
package/build/src/McpContext.js
CHANGED
|
@@ -8,16 +8,25 @@ 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
|
-
import { WEB_MCP_BRIDGE_SCRIPT
|
|
13
|
+
import { WEB_MCP_BRIDGE_SCRIPT } from './transports/WebMCPBridgeScript.js';
|
|
13
14
|
import { WebMCPClientTransport } from './transports/WebMCPClientTransport.js';
|
|
14
15
|
import { Locator } from './third_party/index.js';
|
|
15
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
|
-
|
|
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
|
-
|
|
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,128 @@ export class McpContext {
|
|
|
141
169
|
this.#networkCollector.dispose();
|
|
142
170
|
this.#consoleCollector.dispose();
|
|
143
171
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
+
// 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
|
+
};
|
|
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
|
+
* 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
|
|
247
|
+
*/
|
|
248
|
+
async #tryConnectWebMCP(page) {
|
|
249
|
+
// Skip if tool hub is not enabled
|
|
250
|
+
if (!this.#toolHub?.isEnabled()) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
// Immediately try to get/create WebMCP client
|
|
255
|
+
// Transport is configured with requireWebMCP: false and 30s timeout in getWebMCPClient
|
|
256
|
+
const result = await this.getWebMCPClient(page);
|
|
257
|
+
if (result.connected) {
|
|
258
|
+
this.logger(`WebMCP connected for page: ${page.url()}`);
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
// This is normal for pages without WebMCP
|
|
262
|
+
this.logger(`No WebMCP on page: ${page.url()}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
catch (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);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Set up WebMCP auto-detection for all current pages.
|
|
272
|
+
* Called during initialization and when tool hub is set.
|
|
273
|
+
*/
|
|
274
|
+
async #setupWebMCPAutoDetectionForAllPages() {
|
|
275
|
+
for (const page of this.#pages) {
|
|
276
|
+
this.#setupWebMCPAutoDetection(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
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Create a new McpContext instance.
|
|
285
|
+
*
|
|
286
|
+
* @param browser - Puppeteer browser instance to operate on.
|
|
287
|
+
* @param logger - Debug logger for internal operations.
|
|
288
|
+
* @param opts - Configuration options.
|
|
289
|
+
* @param locatorClass - Locator class to use (injectable for testing with
|
|
290
|
+
* unbundled Puppeteer to avoid class instance mismatch errors).
|
|
291
|
+
* @returns Initialized McpContext ready for use.
|
|
292
|
+
*/
|
|
293
|
+
static async from(browser, logger, opts, locatorClass = Locator) {
|
|
147
294
|
const context = new McpContext(browser, logger, opts, locatorClass);
|
|
148
295
|
await context.#init();
|
|
149
296
|
return context;
|
|
@@ -164,6 +311,14 @@ export class McpContext {
|
|
|
164
311
|
}
|
|
165
312
|
return this.#networkCollector.getIdForResource(request);
|
|
166
313
|
}
|
|
314
|
+
/**
|
|
315
|
+
* Resolve a CDP backend node ID to a snapshot element UID.
|
|
316
|
+
*
|
|
317
|
+
* @param cdpBackendNodeId - The CDP backend node ID from DevTools.
|
|
318
|
+
* @returns The corresponding snapshot UID, or undefined if not found.
|
|
319
|
+
*
|
|
320
|
+
* @todo Optimize with a backendNodeId index instead of tree traversal.
|
|
321
|
+
*/
|
|
167
322
|
resolveCdpElementId(cdpBackendNodeId) {
|
|
168
323
|
if (!cdpBackendNodeId) {
|
|
169
324
|
this.logger('no cdpBackendNodeId');
|
|
@@ -173,7 +328,6 @@ export class McpContext {
|
|
|
173
328
|
this.logger('no text snapshot');
|
|
174
329
|
return;
|
|
175
330
|
}
|
|
176
|
-
// TODO: index by backendNodeId instead.
|
|
177
331
|
const queue = [this.#textSnapshot.root];
|
|
178
332
|
while (queue.length) {
|
|
179
333
|
const current = queue.pop();
|
|
@@ -206,6 +360,8 @@ export class McpContext {
|
|
|
206
360
|
this.selectPage(page);
|
|
207
361
|
this.#networkCollector.addPage(page);
|
|
208
362
|
this.#consoleCollector.addPage(page);
|
|
363
|
+
// Set up WebMCP auto-detection for the new page
|
|
364
|
+
this.#setupWebMCPAutoDetection(page);
|
|
209
365
|
return page;
|
|
210
366
|
}
|
|
211
367
|
async closePage(pageIdx) {
|
|
@@ -352,8 +508,21 @@ export class McpContext {
|
|
|
352
508
|
this.selectPage(this.#pages[0]);
|
|
353
509
|
}
|
|
354
510
|
await this.detectOpenDevToolsWindows();
|
|
511
|
+
// Set up WebMCP auto-detection for any new pages
|
|
512
|
+
// (safe to call for existing pages - it checks if listeners are already installed)
|
|
513
|
+
for (const page of this.#pages) {
|
|
514
|
+
this.#setupWebMCPAutoDetection(page);
|
|
515
|
+
}
|
|
355
516
|
return this.#pages;
|
|
356
517
|
}
|
|
518
|
+
/**
|
|
519
|
+
* Detect and map open DevTools windows to their inspected pages.
|
|
520
|
+
*
|
|
521
|
+
* Iterates through all browser pages to find DevTools windows and
|
|
522
|
+
* associates them with the pages they're inspecting.
|
|
523
|
+
*
|
|
524
|
+
* @todo Optimize page lookup with a URL-indexed map instead of nested loops.
|
|
525
|
+
*/
|
|
357
526
|
async detectOpenDevToolsWindows() {
|
|
358
527
|
this.logger('Detecting open DevTools windows');
|
|
359
528
|
const pages = await this.browser.pages(this.#options.experimentalIncludeAllPages);
|
|
@@ -371,7 +540,6 @@ export class McpContext {
|
|
|
371
540
|
if (!urlLike) {
|
|
372
541
|
continue;
|
|
373
542
|
}
|
|
374
|
-
// TODO: lookup without a loop.
|
|
375
543
|
for (const page of this.#pages) {
|
|
376
544
|
if (urlsEqual(page.url(), urlLike)) {
|
|
377
545
|
this.#pageToDevToolsPage.set(page, devToolsPage);
|
|
@@ -385,7 +553,7 @@ export class McpContext {
|
|
|
385
553
|
}
|
|
386
554
|
}
|
|
387
555
|
getPages() {
|
|
388
|
-
return this.#pages;
|
|
556
|
+
return [...this.#pages];
|
|
389
557
|
}
|
|
390
558
|
getDevToolsPage(page) {
|
|
391
559
|
return this.#pageToDevToolsPage.get(page);
|
|
@@ -554,17 +722,12 @@ export class McpContext {
|
|
|
554
722
|
}
|
|
555
723
|
this.#webMCPConnections.delete(targetPage);
|
|
556
724
|
}
|
|
557
|
-
//
|
|
558
|
-
const hasWebMCP = await this.#checkWebMCPAvailable(targetPage);
|
|
559
|
-
if (!hasWebMCP) {
|
|
560
|
-
return { connected: false, error: 'WebMCP not detected on this page' };
|
|
561
|
-
}
|
|
562
|
-
// Connect
|
|
725
|
+
// Connect - no pre-checking needed (extension approach)
|
|
563
726
|
try {
|
|
564
727
|
const transport = new WebMCPClientTransport({
|
|
565
728
|
page: targetPage,
|
|
566
|
-
readyTimeout:
|
|
567
|
-
requireWebMCP: false, //
|
|
729
|
+
readyTimeout: 30000, // 30s to handle slow React apps (up from 10s)
|
|
730
|
+
requireWebMCP: false, // Don't pre-check, just try to connect
|
|
568
731
|
});
|
|
569
732
|
const client = new Client({ name: 'chrome-devtools-mcp', version: '1.0.0' }, { capabilities: {} });
|
|
570
733
|
// Set up onclose handler to clean up connection state
|
|
@@ -574,10 +737,36 @@ export class McpContext {
|
|
|
574
737
|
if (currentConn?.client === client) {
|
|
575
738
|
this.#webMCPConnections.delete(targetPage);
|
|
576
739
|
}
|
|
740
|
+
// Remove tools for this page when transport closes
|
|
741
|
+
this.#toolHub?.removeToolsForPage(targetPage);
|
|
742
|
+
};
|
|
743
|
+
// Also listen for page close events to trigger cleanup
|
|
744
|
+
// This handles cases where the page is closed without navigation
|
|
745
|
+
const onPageClose = () => {
|
|
746
|
+
const currentConn = this.#webMCPConnections.get(targetPage);
|
|
747
|
+
if (currentConn?.client === client) {
|
|
748
|
+
this.#webMCPConnections.delete(targetPage);
|
|
749
|
+
}
|
|
750
|
+
this.#toolHub?.removeToolsForPage(targetPage);
|
|
751
|
+
// Clean up the listener
|
|
752
|
+
targetPage.off('close', onPageClose);
|
|
577
753
|
};
|
|
754
|
+
targetPage.on('close', onPageClose);
|
|
578
755
|
await client.connect(transport);
|
|
579
756
|
// Store connection for this page
|
|
580
757
|
this.#webMCPConnections.set(targetPage, { client, transport, page: targetPage });
|
|
758
|
+
// Subscribe to tool list changes if tool hub is enabled and server supports it
|
|
759
|
+
const serverCapabilities = client.getServerCapabilities();
|
|
760
|
+
if (serverCapabilities?.tools?.listChanged && this.#toolHub?.isEnabled()) {
|
|
761
|
+
client.setNotificationHandler(ToolListChangedNotificationSchema, async () => {
|
|
762
|
+
this.logger('WebMCP tools changed, re-syncing...');
|
|
763
|
+
await this.#toolHub?.syncToolsForPage(targetPage, client);
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
// Initial tool sync if tool hub is enabled
|
|
767
|
+
if (this.#toolHub?.isEnabled()) {
|
|
768
|
+
await this.#toolHub.syncToolsForPage(targetPage, client);
|
|
769
|
+
}
|
|
581
770
|
return { connected: true, client };
|
|
582
771
|
}
|
|
583
772
|
catch (err) {
|
|
@@ -587,19 +776,6 @@ export class McpContext {
|
|
|
587
776
|
};
|
|
588
777
|
}
|
|
589
778
|
}
|
|
590
|
-
/**
|
|
591
|
-
* Check if WebMCP is available on a page by checking the bridge's hasWebMCP() method.
|
|
592
|
-
* The bridge is auto-injected into all pages, so we just need to check if it detected WebMCP.
|
|
593
|
-
*/
|
|
594
|
-
async #checkWebMCPAvailable(page) {
|
|
595
|
-
try {
|
|
596
|
-
const result = await page.evaluate(CHECK_WEBMCP_AVAILABLE_SCRIPT);
|
|
597
|
-
return result.available;
|
|
598
|
-
}
|
|
599
|
-
catch {
|
|
600
|
-
return false;
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
779
|
/**
|
|
604
780
|
* We need to ignore favicon request as they make our test flaky
|
|
605
781
|
*/
|
package/build/src/McpResponse.js
CHANGED
|
@@ -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
|
}
|
package/build/src/browser.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
...
|
|
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:
|
|
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,8 +8,7 @@ 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
|
-
|
|
12
|
-
default: true,
|
|
11
|
+
default: false,
|
|
13
12
|
coerce: (value) => {
|
|
14
13
|
if (value === false) {
|
|
15
14
|
return false;
|