@mcp-b/chrome-devtools-mcp 1.1.5-beta.0 → 1.2.0-beta.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.
@@ -6,9 +6,11 @@
6
6
  import fs from 'node:fs/promises';
7
7
  import os from 'node:os';
8
8
  import path from 'node:path';
9
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
9
10
  import { extractUrlLikeFromDevToolsTitle, urlsEqual } from './DevtoolsUtils.js';
10
11
  import { NetworkCollector, ConsoleCollector } from './PageCollector.js';
11
- import { WEB_MCP_BRIDGE_SCRIPT } from './transports/WebMCPBridgeScript.js';
12
+ import { WEB_MCP_BRIDGE_SCRIPT, CHECK_WEBMCP_AVAILABLE_SCRIPT } from './transports/WebMCPBridgeScript.js';
13
+ import { WebMCPClientTransport } from './transports/WebMCPClientTransport.js';
12
14
  import { Locator } from './third_party/index.js';
13
15
  import { listPages } from './tools/pages.js';
14
16
  import { takeSnapshot } from './tools/snapshot.js';
@@ -61,6 +63,7 @@ export class McpContext {
61
63
  #traceResults = [];
62
64
  #locatorClass;
63
65
  #options;
66
+ #webMCPConnections = new WeakMap();
64
67
  constructor(browser, logger, options, locatorClass) {
65
68
  this.browser = browser;
66
69
  this.logger = logger;
@@ -522,6 +525,81 @@ export class McpContext {
522
525
  }
523
526
  return locator.wait();
524
527
  }
528
+ /**
529
+ * Get a WebMCP client for a page, auto-connecting if needed.
530
+ *
531
+ * This method handles the full lifecycle of WebMCP connections:
532
+ * - Maintains separate connections per page (one-to-many relationship)
533
+ * - Detects stale connections (page reload, navigation)
534
+ * - Cleans up old connections before creating new ones
535
+ * - Auto-connects when WebMCP is available on the page
536
+ *
537
+ * @param page - Optional page to get client for. Defaults to selected page.
538
+ */
539
+ async getWebMCPClient(page) {
540
+ const targetPage = page ?? this.getSelectedPage();
541
+ // Check if we have a valid, active connection for this page
542
+ // We must verify isClosed() to detect page reloads where URL stays the same but frames are invalidated
543
+ const conn = this.#webMCPConnections.get(targetPage);
544
+ if (conn && !conn.transport.isClosed()) {
545
+ return { connected: true, client: conn.client };
546
+ }
547
+ // If we have a stale connection, clean up first
548
+ if (conn) {
549
+ try {
550
+ await conn.client.close();
551
+ }
552
+ catch {
553
+ // Ignore close errors
554
+ }
555
+ this.#webMCPConnections.delete(targetPage);
556
+ }
557
+ // Check if WebMCP is available
558
+ const hasWebMCP = await this.#checkWebMCPAvailable(targetPage);
559
+ if (!hasWebMCP) {
560
+ return { connected: false, error: 'WebMCP not detected on this page' };
561
+ }
562
+ // Connect
563
+ try {
564
+ const transport = new WebMCPClientTransport({
565
+ page: targetPage,
566
+ readyTimeout: 10000,
567
+ requireWebMCP: false, // We already checked
568
+ });
569
+ const client = new Client({ name: 'chrome-devtools-mcp', version: '1.0.0' }, { capabilities: {} });
570
+ // Set up onclose handler to clean up connection state
571
+ // This handles page navigations, reloads, and manual disconnections
572
+ transport.onclose = () => {
573
+ const currentConn = this.#webMCPConnections.get(targetPage);
574
+ if (currentConn?.client === client) {
575
+ this.#webMCPConnections.delete(targetPage);
576
+ }
577
+ };
578
+ await client.connect(transport);
579
+ // Store connection for this page
580
+ this.#webMCPConnections.set(targetPage, { client, transport, page: targetPage });
581
+ return { connected: true, client };
582
+ }
583
+ catch (err) {
584
+ return {
585
+ connected: false,
586
+ error: `Failed to connect: ${err instanceof Error ? err.message : String(err)}`,
587
+ };
588
+ }
589
+ }
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
+ }
525
603
  /**
526
604
  * We need to ignore favicon request as they make our test flaky
527
605
  */
@@ -3,132 +3,50 @@
3
3
  * Copyright 2025 Google LLC
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
- import { Client } from '@modelcontextprotocol/sdk/client/index.js';
7
- import { WebMCPClientTransport } from '../transports/index.js';
8
6
  import { zod } from '../third_party/index.js';
9
7
  import { ToolCategory } from './categories.js';
10
8
  import { defineTool } from './ToolDefinition.js';
11
9
  /**
12
- * Module-level storage for WebMCP client and transport.
13
- * Only one connection is maintained at a time per page.
14
- */
15
- let webMCPClient = null;
16
- let webMCPTransport = null;
17
- let connectedPageUrl = null;
18
- /**
19
- * Check if WebMCP is available on a page by checking the bridge's hasWebMCP() method.
20
- * The bridge is auto-injected into all pages, so we just need to check if it detected WebMCP.
21
- */
22
- async function checkWebMCPAvailable(page) {
23
- try {
24
- // First check if the bridge is injected and can detect WebMCP
25
- const result = await page.evaluate(() => {
26
- // Check if bridge exists and can detect WebMCP
27
- if (typeof window !== 'undefined' &&
28
- // @ts-expect-error - bridge is injected
29
- typeof window.__mcpBridge?.hasWebMCP === 'function') {
30
- // @ts-expect-error - bridge is injected
31
- return window.__mcpBridge.hasWebMCP();
32
- }
33
- // Fallback: direct check
34
- // @ts-expect-error - modelContext is a polyfill/experimental API
35
- if (typeof navigator !== 'undefined' && navigator.modelContext) {
36
- return true;
37
- }
38
- // @ts-expect-error - internal marker
39
- if (window.__MCP_BRIDGE__) {
40
- return true;
41
- }
42
- return false;
43
- });
44
- return result;
45
- }
46
- catch {
47
- return false;
48
- }
49
- }
50
- /**
51
- * Auto-connect to WebMCP on the current page if available.
52
- * Returns true if connected (or already connected to same page), false otherwise.
53
- */
54
- async function ensureWebMCPConnected(context) {
55
- const page = context.getSelectedPage();
56
- const currentUrl = page.url();
57
- // If already connected to the same page, we're good
58
- if (webMCPClient && connectedPageUrl === currentUrl) {
59
- return { connected: true };
60
- }
61
- // If connected to a different page, disconnect first
62
- if (webMCPClient && connectedPageUrl !== currentUrl) {
63
- try {
64
- await webMCPClient.close();
65
- }
66
- catch {
67
- // Ignore close errors
68
- }
69
- webMCPClient = null;
70
- webMCPTransport = null;
71
- connectedPageUrl = null;
72
- }
73
- // Check if WebMCP is available
74
- const hasWebMCP = await checkWebMCPAvailable(page);
75
- if (!hasWebMCP) {
76
- return { connected: false, error: 'WebMCP not detected on this page' };
77
- }
78
- // Connect
79
- try {
80
- const transport = new WebMCPClientTransport({
81
- page,
82
- readyTimeout: 10000,
83
- requireWebMCP: false, // We already checked
84
- });
85
- const client = new Client({ name: 'chrome-devtools-mcp', version: '1.0.0' }, { capabilities: {} });
86
- // Set up onclose handler to clean up module-level state
87
- // This handles page navigations, reloads, and manual disconnections
88
- transport.onclose = () => {
89
- if (webMCPClient === client) {
90
- webMCPClient = null;
91
- webMCPTransport = null;
92
- connectedPageUrl = null;
93
- }
94
- };
95
- await client.connect(transport);
96
- // Store for later use
97
- webMCPTransport = transport;
98
- webMCPClient = client;
99
- connectedPageUrl = currentUrl;
100
- return { connected: true };
101
- }
102
- catch (err) {
103
- return {
104
- connected: false,
105
- error: `Failed to connect: ${err instanceof Error ? err.message : String(err)}`,
106
- };
107
- }
108
- }
109
- /**
110
- * List all MCP tools available on the current webpage.
10
+ * List all MCP tools available on a webpage.
111
11
  * Auto-connects to WebMCP if not already connected.
112
12
  */
113
13
  export const listWebMCPTools = defineTool({
114
14
  name: 'list_webmcp_tools',
115
- description: 'List all MCP tools available on the current webpage. ' +
116
- 'Automatically connects to WebMCP if the page has @mcp-b/global loaded.',
15
+ description: 'List all MCP tools available on a webpage. ' +
16
+ 'Automatically connects to WebMCP if the page has @mcp-b/global loaded. ' +
17
+ 'Use page_index to target a specific page (see list_pages for indices).',
117
18
  annotations: {
118
19
  title: 'List Website MCP Tools',
119
20
  category: ToolCategory.WEBMCP,
120
21
  readOnlyHint: true,
121
22
  },
122
- schema: {},
123
- handler: async (_request, response, context) => {
124
- // Auto-connect if needed
125
- const connectResult = await ensureWebMCPConnected(context);
126
- if (!connectResult.connected) {
127
- response.appendResponseLine(connectResult.error || 'No WebMCP tools available on this page.');
23
+ schema: {
24
+ page_index: zod
25
+ .number()
26
+ .int()
27
+ .optional()
28
+ .describe('Index of the page to list tools from. If not specified, uses the currently selected page. ' +
29
+ 'Use list_pages to see available pages and their indices.'),
30
+ },
31
+ handler: async (request, response, context) => {
32
+ const { page_index } = request.params;
33
+ // Get the target page
34
+ const page = page_index !== undefined
35
+ ? context.getPageByIdx(page_index)
36
+ : context.getSelectedPage();
37
+ // Get client from context (handles auto-connect and stale connection detection)
38
+ const result = await context.getWebMCPClient(page);
39
+ if (!result.connected) {
40
+ response.appendResponseLine(result.error || 'No WebMCP tools available on this page.');
128
41
  return;
129
42
  }
43
+ const client = result.client;
130
44
  try {
131
- const { tools } = await webMCPClient.listTools();
45
+ const { tools } = await client.listTools();
46
+ if (page_index !== undefined) {
47
+ response.appendResponseLine(`Page ${page_index}: ${page.url()}`);
48
+ response.appendResponseLine('');
49
+ }
132
50
  response.appendResponseLine(`${tools.length} tool(s) available:`);
133
51
  response.appendResponseLine('');
134
52
  for (const tool of tools) {
@@ -155,14 +73,15 @@ export const listWebMCPTools = defineTool({
155
73
  },
156
74
  });
157
75
  /**
158
- * Call a tool registered on the webpage via WebMCP.
76
+ * Call a tool registered on a webpage via WebMCP.
159
77
  * Auto-connects to WebMCP if not already connected.
160
78
  */
161
79
  export const callWebMCPTool = defineTool({
162
80
  name: 'call_webmcp_tool',
163
- description: 'Call a tool registered on the webpage via WebMCP. ' +
81
+ description: 'Call a tool registered on a webpage via WebMCP. ' +
164
82
  'Automatically connects if the page has @mcp-b/global loaded. ' +
165
- 'Use list_webmcp_tools to see available tools and their schemas.',
83
+ 'Use list_webmcp_tools to see available tools and their schemas. ' +
84
+ 'Use page_index to target a specific page.',
166
85
  annotations: {
167
86
  title: 'Call Website MCP Tool',
168
87
  category: ToolCategory.WEBMCP,
@@ -174,29 +93,43 @@ export const callWebMCPTool = defineTool({
174
93
  .record(zod.any())
175
94
  .optional()
176
95
  .describe('Arguments to pass to the tool as a JSON object'),
96
+ page_index: zod
97
+ .number()
98
+ .int()
99
+ .optional()
100
+ .describe('Index of the page to call the tool on. If not specified, uses the currently selected page. ' +
101
+ 'Use list_pages to see available pages and their indices.'),
177
102
  },
178
103
  handler: async (request, response, context) => {
179
- // Auto-connect if needed
180
- const connectResult = await ensureWebMCPConnected(context);
181
- if (!connectResult.connected) {
182
- response.appendResponseLine(connectResult.error || 'No WebMCP tools available on this page.');
104
+ const { name, arguments: args, page_index } = request.params;
105
+ // Get the target page
106
+ const page = page_index !== undefined
107
+ ? context.getPageByIdx(page_index)
108
+ : context.getSelectedPage();
109
+ // Get client from context (handles auto-connect and stale connection detection)
110
+ const result = await context.getWebMCPClient(page);
111
+ if (!result.connected) {
112
+ response.appendResponseLine(result.error || 'No WebMCP tools available on this page.');
183
113
  return;
184
114
  }
185
- const { name, arguments: args } = request.params;
115
+ const client = result.client;
186
116
  try {
117
+ if (page_index !== undefined) {
118
+ response.appendResponseLine(`Page ${page_index}: ${page.url()}`);
119
+ }
187
120
  response.appendResponseLine(`Calling tool: ${name}`);
188
121
  if (args && Object.keys(args).length > 0) {
189
122
  response.appendResponseLine(`Arguments: ${JSON.stringify(args)}`);
190
123
  }
191
124
  response.appendResponseLine('');
192
- const result = await webMCPClient.callTool({
125
+ const callResult = await client.callTool({
193
126
  name,
194
127
  arguments: args || {},
195
128
  });
196
129
  response.appendResponseLine('Result:');
197
130
  // Format the result content
198
- if (result.content && Array.isArray(result.content)) {
199
- for (const content of result.content) {
131
+ if (callResult.content && Array.isArray(callResult.content)) {
132
+ for (const content of callResult.content) {
200
133
  if (content.type === 'text') {
201
134
  response.appendResponseLine(content.text);
202
135
  }
@@ -212,9 +145,9 @@ export const callWebMCPTool = defineTool({
212
145
  }
213
146
  }
214
147
  else {
215
- response.appendResponseLine(JSON.stringify(result, null, 2));
148
+ response.appendResponseLine(JSON.stringify(callResult, null, 2));
216
149
  }
217
- if (result.isError) {
150
+ if (callResult.isError) {
218
151
  response.appendResponseLine('');
219
152
  response.appendResponseLine('(Tool returned an error)');
220
153
  }
@@ -54,6 +54,22 @@ export class WebMCPClientTransport {
54
54
  onclose;
55
55
  onerror;
56
56
  onmessage;
57
+ /**
58
+ * Check if the transport has been closed.
59
+ * This is useful for clients to check if they need to reconnect
60
+ * after a page navigation or reload.
61
+ */
62
+ isClosed() {
63
+ return this._closed;
64
+ }
65
+ /**
66
+ * Get the page this transport is connected to.
67
+ * This allows callers to verify the transport is connected to the expected page,
68
+ * which is important when browsers are closed and reopened.
69
+ */
70
+ getPage() {
71
+ return this._page;
72
+ }
57
73
  constructor(options) {
58
74
  this._page = options.page;
59
75
  this._readyTimeout = options.readyTimeout ?? 10000;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcp-b/chrome-devtools-mcp",
3
- "version": "1.1.5-beta.0",
3
+ "version": "1.2.0-beta.1",
4
4
  "description": "MCP server for Chrome DevTools with WebMCP integration for connecting to website MCP tools",
5
5
  "keywords": [
6
6
  "mcp",