@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.
package/build/src/McpContext.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
|
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
|
|
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 (
|
|
199
|
-
for (const content of
|
|
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(
|
|
148
|
+
response.appendResponseLine(JSON.stringify(callResult, null, 2));
|
|
216
149
|
}
|
|
217
|
-
if (
|
|
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;
|