@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 +44 -15
- package/build/src/McpContext.js +195 -7
- package/build/src/browser.js +76 -4
- package/build/src/cli.js +0 -1
- package/build/src/main.js +37 -2
- package/build/src/prompts/index.js +5 -5
- package/build/src/third_party/index.js +1 -1
- package/build/src/tools/WebMCPToolHub.js +372 -0
- package/build/src/tools/webmcp.js +54 -122
- package/build/src/transports/WebMCPClientTransport.js +161 -74
- package/package.json +2 -1
|
@@ -43,6 +43,8 @@ export class WebMCPClientTransport {
|
|
|
43
43
|
_page;
|
|
44
44
|
_cdpSession = null;
|
|
45
45
|
_started = false;
|
|
46
|
+
/** Guards against concurrent start() calls. */
|
|
47
|
+
_starting = false;
|
|
46
48
|
_closed = false;
|
|
47
49
|
_readyTimeout;
|
|
48
50
|
_requireWebMCP;
|
|
@@ -50,9 +52,17 @@ export class WebMCPClientTransport {
|
|
|
50
52
|
_serverReadyPromise;
|
|
51
53
|
_serverReadyResolve;
|
|
52
54
|
_serverReadyReject;
|
|
53
|
-
|
|
55
|
+
/** Tracks if promise was rejected to prevent double rejection. */
|
|
56
|
+
_serverReadyRejected = false;
|
|
57
|
+
/** Bound handler for frame navigation events (stored for cleanup). */
|
|
58
|
+
_frameNavigatedHandler = null;
|
|
59
|
+
/** Bound handler for CDP binding calls (stored for cleanup). */
|
|
60
|
+
_bindingCalledHandler = null;
|
|
61
|
+
/** Callback invoked when transport is closed. */
|
|
54
62
|
onclose;
|
|
63
|
+
/** Callback invoked when an error occurs. */
|
|
55
64
|
onerror;
|
|
65
|
+
/** Callback invoked when a JSON-RPC message is received. */
|
|
56
66
|
onmessage;
|
|
57
67
|
/**
|
|
58
68
|
* Check if the transport has been closed.
|
|
@@ -74,21 +84,43 @@ export class WebMCPClientTransport {
|
|
|
74
84
|
this._page = options.page;
|
|
75
85
|
this._readyTimeout = options.readyTimeout ?? 10000;
|
|
76
86
|
this._requireWebMCP = options.requireWebMCP ?? true;
|
|
77
|
-
// Set up server ready promise
|
|
87
|
+
// Set up server ready promise with attached rejection handler
|
|
88
|
+
// to prevent unhandled rejection if close() is called before start()
|
|
78
89
|
this._serverReadyPromise = new Promise((resolve, reject) => {
|
|
79
90
|
this._serverReadyResolve = resolve;
|
|
80
|
-
this._serverReadyReject =
|
|
91
|
+
this._serverReadyReject = (err) => {
|
|
92
|
+
this._serverReadyRejected = true;
|
|
93
|
+
reject(err);
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
// Attach a no-op catch to prevent unhandled rejection warnings
|
|
97
|
+
// The actual error will be propagated when the promise is awaited
|
|
98
|
+
this._serverReadyPromise.catch(() => {
|
|
99
|
+
// Intentionally empty - errors are handled where the promise is awaited
|
|
81
100
|
});
|
|
82
101
|
}
|
|
83
102
|
/**
|
|
84
|
-
* Check if WebMCP is available on the page
|
|
103
|
+
* Check if WebMCP is available on the page.
|
|
104
|
+
*
|
|
105
|
+
* Returns `available: false` with an `error` field if the check failed due to
|
|
106
|
+
* page-level issues (closed, navigating, etc.) vs WebMCP simply not being present.
|
|
85
107
|
*/
|
|
86
108
|
async checkWebMCPAvailable() {
|
|
87
109
|
try {
|
|
88
110
|
const result = await this._page.evaluate(CHECK_WEBMCP_AVAILABLE_SCRIPT);
|
|
89
111
|
return result;
|
|
90
112
|
}
|
|
91
|
-
catch {
|
|
113
|
+
catch (err) {
|
|
114
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
115
|
+
// Distinguish between "page is broken" vs "WebMCP not present"
|
|
116
|
+
const isPageError = message.includes('Execution context was destroyed') ||
|
|
117
|
+
message.includes('Target closed') ||
|
|
118
|
+
message.includes('Session closed') ||
|
|
119
|
+
message.includes('Protocol error');
|
|
120
|
+
if (isPageError) {
|
|
121
|
+
return { available: false, error: `Page error: ${message}` };
|
|
122
|
+
}
|
|
123
|
+
// Other errors (e.g., script threw) mean WebMCP is not available
|
|
92
124
|
return { available: false };
|
|
93
125
|
}
|
|
94
126
|
}
|
|
@@ -106,76 +138,103 @@ export class WebMCPClientTransport {
|
|
|
106
138
|
if (this._started) {
|
|
107
139
|
throw new Error('WebMCPClientTransport already started');
|
|
108
140
|
}
|
|
141
|
+
if (this._starting) {
|
|
142
|
+
throw new Error('WebMCPClientTransport start already in progress');
|
|
143
|
+
}
|
|
109
144
|
if (this._closed) {
|
|
110
145
|
throw new Error('WebMCPClientTransport has been closed');
|
|
111
146
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (
|
|
116
|
-
|
|
117
|
-
|
|
147
|
+
this._starting = true;
|
|
148
|
+
try {
|
|
149
|
+
// Check if WebMCP is available
|
|
150
|
+
if (this._requireWebMCP) {
|
|
151
|
+
const check = await this.checkWebMCPAvailable();
|
|
152
|
+
if (!check.available) {
|
|
153
|
+
const errorDetail = check.error ? ` (${check.error})` : '';
|
|
154
|
+
throw new Error(`WebMCP not detected on this page${errorDetail}. ` +
|
|
155
|
+
'Ensure @mcp-b/global is loaded and initialized.');
|
|
156
|
+
}
|
|
118
157
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
158
|
+
// Create CDP session for this page
|
|
159
|
+
this._cdpSession = await this._page.createCDPSession();
|
|
160
|
+
// Enable Runtime domain for bindings and evaluation
|
|
161
|
+
await this._cdpSession.send('Runtime.enable');
|
|
162
|
+
// Set up binding for receiving messages from the bridge
|
|
163
|
+
// When the bridge calls window.__mcpBridgeToClient(msg), we receive it here
|
|
164
|
+
await this._cdpSession.send('Runtime.addBinding', {
|
|
165
|
+
name: '__mcpBridgeToClient',
|
|
166
|
+
});
|
|
167
|
+
// Create bound handlers so we can remove them later
|
|
168
|
+
this._bindingCalledHandler = (event) => {
|
|
169
|
+
if (event.name !== '__mcpBridgeToClient')
|
|
170
|
+
return;
|
|
171
|
+
// Guard against processing messages after close
|
|
172
|
+
if (this._closed)
|
|
173
|
+
return;
|
|
174
|
+
try {
|
|
175
|
+
const payload = JSON.parse(event.payload);
|
|
176
|
+
this._handlePayload(payload);
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
this.onerror?.(new Error(`Failed to parse message from bridge: ${err}`));
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
this._frameNavigatedHandler = (frame) => {
|
|
183
|
+
if (frame === this._page.mainFrame()) {
|
|
184
|
+
// Main frame navigated, bridge is gone
|
|
185
|
+
this._handleNavigation();
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
// Listen for binding calls (messages from bridge → this transport)
|
|
189
|
+
this._cdpSession.on('Runtime.bindingCalled', this._bindingCalledHandler);
|
|
190
|
+
// Listen for page navigation to detect when bridge is lost
|
|
191
|
+
this._page.on('framenavigated', this._frameNavigatedHandler);
|
|
192
|
+
// Inject the bridge script
|
|
193
|
+
const result = await this._page.evaluate(WEB_MCP_BRIDGE_SCRIPT);
|
|
194
|
+
if (!result.success && !result.alreadyInjected) {
|
|
195
|
+
throw new Error('Failed to inject WebMCP bridge script');
|
|
196
|
+
}
|
|
197
|
+
this._started = true;
|
|
198
|
+
// Initiate server-ready handshake
|
|
199
|
+
await this._page.evaluate(() => {
|
|
200
|
+
window.__mcpBridge?.checkReady();
|
|
201
|
+
});
|
|
202
|
+
// Wait for server ready with timeout
|
|
203
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
204
|
+
const timer = setTimeout(() => {
|
|
205
|
+
reject(new Error(`WebMCP server did not respond within ${this._readyTimeout}ms. ` +
|
|
206
|
+
'Ensure TabServerTransport is running on the page.'));
|
|
207
|
+
}, this._readyTimeout);
|
|
208
|
+
// Clear timeout if promise resolves
|
|
209
|
+
this._serverReadyPromise.then(() => clearTimeout(timer)).catch(() => clearTimeout(timer));
|
|
210
|
+
});
|
|
133
211
|
try {
|
|
134
|
-
|
|
135
|
-
this._handlePayload(payload);
|
|
212
|
+
await Promise.race([this._serverReadyPromise, timeoutPromise]);
|
|
136
213
|
}
|
|
137
214
|
catch (err) {
|
|
138
|
-
this.
|
|
139
|
-
|
|
140
|
-
});
|
|
141
|
-
// Listen for page navigation to detect when bridge is lost
|
|
142
|
-
this._page.on('framenavigated', frame => {
|
|
143
|
-
if (frame === this._page.mainFrame()) {
|
|
144
|
-
// Main frame navigated, bridge is gone
|
|
145
|
-
this._handleNavigation();
|
|
215
|
+
await this.close();
|
|
216
|
+
throw err;
|
|
146
217
|
}
|
|
147
|
-
});
|
|
148
|
-
// Inject the bridge script
|
|
149
|
-
const result = await this._page.evaluate(WEB_MCP_BRIDGE_SCRIPT);
|
|
150
|
-
if (!result.success && !result.alreadyInjected) {
|
|
151
|
-
throw new Error('Failed to inject WebMCP bridge script');
|
|
152
|
-
}
|
|
153
|
-
this._started = true;
|
|
154
|
-
// Initiate server-ready handshake
|
|
155
|
-
await this._page.evaluate(() => {
|
|
156
|
-
window.__mcpBridge?.checkReady();
|
|
157
|
-
});
|
|
158
|
-
// Wait for server ready with timeout
|
|
159
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
160
|
-
const timer = setTimeout(() => {
|
|
161
|
-
reject(new Error(`WebMCP server did not respond within ${this._readyTimeout}ms. ` +
|
|
162
|
-
'Ensure TabServerTransport is running on the page.'));
|
|
163
|
-
}, this._readyTimeout);
|
|
164
|
-
// Clear timeout if promise resolves
|
|
165
|
-
this._serverReadyPromise.then(() => clearTimeout(timer)).catch(() => clearTimeout(timer));
|
|
166
|
-
});
|
|
167
|
-
try {
|
|
168
|
-
await Promise.race([this._serverReadyPromise, timeoutPromise]);
|
|
169
218
|
}
|
|
170
219
|
catch (err) {
|
|
171
|
-
|
|
220
|
+
// Clean up on any error during start
|
|
221
|
+
this._starting = false;
|
|
222
|
+
if (!this._closed) {
|
|
223
|
+
await this._cleanup();
|
|
224
|
+
}
|
|
172
225
|
throw err;
|
|
173
226
|
}
|
|
227
|
+
finally {
|
|
228
|
+
this._starting = false;
|
|
229
|
+
}
|
|
174
230
|
}
|
|
175
231
|
/**
|
|
176
232
|
* Handle a payload received from the bridge
|
|
177
233
|
*/
|
|
178
234
|
_handlePayload(payload) {
|
|
235
|
+
// Guard against processing messages after close
|
|
236
|
+
if (this._closed)
|
|
237
|
+
return;
|
|
179
238
|
// Handle special string payloads (handshake signals)
|
|
180
239
|
if (typeof payload === 'string') {
|
|
181
240
|
if (payload === 'mcp-server-ready') {
|
|
@@ -207,25 +266,32 @@ export class WebMCPClientTransport {
|
|
|
207
266
|
* Handle page navigation - bridge is lost
|
|
208
267
|
*
|
|
209
268
|
* Navigation is a normal lifecycle event in browsers, not a fatal error.
|
|
210
|
-
* We
|
|
269
|
+
* We perform full teardown (detach CDP session, remove listeners) and
|
|
270
|
+
* allow the client to reconnect by creating a new transport instance.
|
|
211
271
|
*/
|
|
212
272
|
_handleNavigation() {
|
|
213
273
|
if (this._closed)
|
|
214
274
|
return;
|
|
215
275
|
this._serverReady = false;
|
|
216
|
-
// Mark as closed to prevent further operations
|
|
217
276
|
this._closed = true;
|
|
218
277
|
this._started = false;
|
|
219
|
-
// Reject any pending server ready promise
|
|
220
|
-
this.
|
|
278
|
+
// Reject any pending server ready promise (safe - has attached catch handler)
|
|
279
|
+
if (!this._serverReadyRejected) {
|
|
280
|
+
this._serverReadyReject(new Error('Page navigated, connection lost'));
|
|
281
|
+
}
|
|
282
|
+
// Full teardown - detach CDP session and remove listeners
|
|
283
|
+
this._cleanup().catch(() => {
|
|
284
|
+
// Ignore cleanup errors during navigation
|
|
285
|
+
});
|
|
221
286
|
// Signal clean disconnection (not an error)
|
|
222
|
-
// The client can reconnect by creating a new transport instance
|
|
223
287
|
this.onclose?.();
|
|
224
288
|
}
|
|
225
289
|
/**
|
|
226
290
|
* Handle server stopped signal
|
|
227
291
|
*/
|
|
228
292
|
_handleServerStopped() {
|
|
293
|
+
if (this._closed)
|
|
294
|
+
return;
|
|
229
295
|
this._serverReady = false;
|
|
230
296
|
this.onerror?.(new Error('WebMCP server stopped'));
|
|
231
297
|
}
|
|
@@ -242,8 +308,9 @@ export class WebMCPClientTransport {
|
|
|
242
308
|
// Wait for server to be ready before sending
|
|
243
309
|
await this._serverReadyPromise;
|
|
244
310
|
// Send via CDP → bridge → postMessage → TabServer
|
|
245
|
-
const messageJson = JSON.stringify(message);
|
|
246
311
|
try {
|
|
312
|
+
// JSON.stringify inside try block to catch non-serializable messages
|
|
313
|
+
const messageJson = JSON.stringify(message);
|
|
247
314
|
await this._page.evaluate((msg) => {
|
|
248
315
|
const bridge = window.__mcpBridge;
|
|
249
316
|
if (!bridge) {
|
|
@@ -262,14 +329,20 @@ export class WebMCPClientTransport {
|
|
|
262
329
|
}
|
|
263
330
|
}
|
|
264
331
|
/**
|
|
265
|
-
*
|
|
332
|
+
* Internal cleanup method - removes listeners and detaches CDP session.
|
|
333
|
+
* Does not set _closed flag or call onclose (caller handles those).
|
|
266
334
|
*/
|
|
267
|
-
async
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
335
|
+
async _cleanup() {
|
|
336
|
+
// Remove page event listener
|
|
337
|
+
if (this._frameNavigatedHandler) {
|
|
338
|
+
this._page.off('framenavigated', this._frameNavigatedHandler);
|
|
339
|
+
this._frameNavigatedHandler = null;
|
|
340
|
+
}
|
|
341
|
+
// Remove CDP event listener
|
|
342
|
+
if (this._cdpSession && this._bindingCalledHandler) {
|
|
343
|
+
this._cdpSession.off('Runtime.bindingCalled', this._bindingCalledHandler);
|
|
344
|
+
this._bindingCalledHandler = null;
|
|
345
|
+
}
|
|
273
346
|
// Dispose the bridge script if possible
|
|
274
347
|
try {
|
|
275
348
|
await this._page.evaluate(() => {
|
|
@@ -277,7 +350,7 @@ export class WebMCPClientTransport {
|
|
|
277
350
|
});
|
|
278
351
|
}
|
|
279
352
|
catch {
|
|
280
|
-
// Ignore errors during cleanup
|
|
353
|
+
// Ignore errors during cleanup (page might be closed/navigated)
|
|
281
354
|
}
|
|
282
355
|
// Detach CDP session
|
|
283
356
|
if (this._cdpSession) {
|
|
@@ -289,8 +362,22 @@ export class WebMCPClientTransport {
|
|
|
289
362
|
}
|
|
290
363
|
this._cdpSession = null;
|
|
291
364
|
}
|
|
292
|
-
|
|
293
|
-
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Close the transport and clean up resources
|
|
368
|
+
*/
|
|
369
|
+
async close() {
|
|
370
|
+
if (this._closed)
|
|
371
|
+
return;
|
|
372
|
+
this._closed = true;
|
|
373
|
+
this._started = false;
|
|
374
|
+
this._serverReady = false;
|
|
375
|
+
// Full cleanup
|
|
376
|
+
await this._cleanup();
|
|
377
|
+
// Reject any pending server ready promise (safe - has attached catch handler)
|
|
378
|
+
if (!this._serverReadyRejected) {
|
|
379
|
+
this._serverReadyReject(new Error('Transport closed'));
|
|
380
|
+
}
|
|
294
381
|
this.onclose?.();
|
|
295
382
|
}
|
|
296
383
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mcp-b/chrome-devtools-mcp",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "MCP server for Chrome DevTools with WebMCP integration for connecting to website MCP tools",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"!*.tsbuildinfo"
|
|
49
49
|
],
|
|
50
50
|
"dependencies": {
|
|
51
|
+
"@composio/json-schema-to-zod": "^0.1.17",
|
|
51
52
|
"@modelcontextprotocol/sdk": "1.24.1",
|
|
52
53
|
"core-js": "3.47.0",
|
|
53
54
|
"debug": "4.4.3",
|