@mcp-b/chrome-devtools-mcp 0.12.0-beta.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.
Files changed (41) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +554 -0
  3. package/build/src/DevToolsConnectionAdapter.js +69 -0
  4. package/build/src/DevtoolsUtils.js +206 -0
  5. package/build/src/McpContext.js +499 -0
  6. package/build/src/McpResponse.js +396 -0
  7. package/build/src/Mutex.js +37 -0
  8. package/build/src/PageCollector.js +283 -0
  9. package/build/src/WaitForHelper.js +139 -0
  10. package/build/src/browser.js +134 -0
  11. package/build/src/cli.js +213 -0
  12. package/build/src/formatters/consoleFormatter.js +121 -0
  13. package/build/src/formatters/networkFormatter.js +77 -0
  14. package/build/src/formatters/snapshotFormatter.js +73 -0
  15. package/build/src/index.js +21 -0
  16. package/build/src/issue-descriptions.js +39 -0
  17. package/build/src/logger.js +27 -0
  18. package/build/src/main.js +130 -0
  19. package/build/src/polyfill.js +7 -0
  20. package/build/src/third_party/index.js +16 -0
  21. package/build/src/tools/ToolDefinition.js +20 -0
  22. package/build/src/tools/categories.js +24 -0
  23. package/build/src/tools/console.js +85 -0
  24. package/build/src/tools/emulation.js +87 -0
  25. package/build/src/tools/input.js +268 -0
  26. package/build/src/tools/network.js +106 -0
  27. package/build/src/tools/pages.js +237 -0
  28. package/build/src/tools/performance.js +147 -0
  29. package/build/src/tools/screenshot.js +84 -0
  30. package/build/src/tools/script.js +71 -0
  31. package/build/src/tools/snapshot.js +52 -0
  32. package/build/src/tools/tools.js +31 -0
  33. package/build/src/tools/webmcp.js +233 -0
  34. package/build/src/trace-processing/parse.js +84 -0
  35. package/build/src/transports/WebMCPBridgeScript.js +196 -0
  36. package/build/src/transports/WebMCPClientTransport.js +276 -0
  37. package/build/src/transports/index.js +7 -0
  38. package/build/src/utils/keyboard.js +296 -0
  39. package/build/src/utils/pagination.js +49 -0
  40. package/build/src/utils/types.js +6 -0
  41. package/package.json +87 -0
@@ -0,0 +1,276 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { WEB_MCP_BRIDGE_SCRIPT, CHECK_WEBMCP_AVAILABLE_SCRIPT } from './WebMCPBridgeScript.js';
7
+ /**
8
+ * MCP Client Transport that connects to a WebMCP server running in a browser tab.
9
+ *
10
+ * This transport uses Chrome DevTools Protocol (CDP) to inject a bridge script
11
+ * into the page, which then ferries MCP messages between this transport and
12
+ * the page's TabServerTransport via window.postMessage.
13
+ *
14
+ * Architecture:
15
+ * ```
16
+ * WebMCPClientTransport (Node.js)
17
+ * │
18
+ * │ CDP (Runtime.evaluate / Runtime.bindingCalled)
19
+ * ▼
20
+ * Bridge Script (injected into page)
21
+ * │
22
+ * │ window.postMessage
23
+ * ▼
24
+ * TabServerTransport (@mcp-b/global)
25
+ * │
26
+ * ▼
27
+ * MCP Server (tools, resources, prompts)
28
+ * ```
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * import { Client } from '@modelcontextprotocol/sdk/client/index.js';
33
+ * import { WebMCPClientTransport } from './transports/WebMCPClientTransport.js';
34
+ *
35
+ * const transport = new WebMCPClientTransport({ page });
36
+ * const client = new Client({ name: 'my-client', version: '1.0.0' });
37
+ *
38
+ * await client.connect(transport);
39
+ * const { tools } = await client.listTools();
40
+ * ```
41
+ */
42
+ export class WebMCPClientTransport {
43
+ _page;
44
+ _cdpSession = null;
45
+ _started = false;
46
+ _closed = false;
47
+ _readyTimeout;
48
+ _requireWebMCP;
49
+ _serverReady = false;
50
+ _serverReadyPromise;
51
+ _serverReadyResolve;
52
+ _serverReadyReject;
53
+ // Transport callbacks
54
+ onclose;
55
+ onerror;
56
+ onmessage;
57
+ constructor(options) {
58
+ this._page = options.page;
59
+ this._readyTimeout = options.readyTimeout ?? 10000;
60
+ this._requireWebMCP = options.requireWebMCP ?? true;
61
+ // Set up server ready promise
62
+ this._serverReadyPromise = new Promise((resolve, reject) => {
63
+ this._serverReadyResolve = resolve;
64
+ this._serverReadyReject = reject;
65
+ });
66
+ }
67
+ /**
68
+ * Check if WebMCP is available on the page
69
+ */
70
+ async checkWebMCPAvailable() {
71
+ try {
72
+ const result = await this._page.evaluate(CHECK_WEBMCP_AVAILABLE_SCRIPT);
73
+ return result;
74
+ }
75
+ catch {
76
+ return { available: false };
77
+ }
78
+ }
79
+ /**
80
+ * Start the transport and establish connection to the WebMCP server.
81
+ *
82
+ * This method:
83
+ * 1. Creates a CDP session
84
+ * 2. Sets up the binding for receiving messages from the page
85
+ * 3. Injects the bridge script
86
+ * 4. Initiates the server-ready handshake
87
+ * 5. Waits for the server to be ready (with timeout)
88
+ */
89
+ async start() {
90
+ if (this._started) {
91
+ throw new Error('WebMCPClientTransport already started');
92
+ }
93
+ if (this._closed) {
94
+ throw new Error('WebMCPClientTransport has been closed');
95
+ }
96
+ // Check if WebMCP is available
97
+ if (this._requireWebMCP) {
98
+ const check = await this.checkWebMCPAvailable();
99
+ if (!check.available) {
100
+ throw new Error('WebMCP not detected on this page. ' +
101
+ 'Ensure @mcp-b/global is loaded and initialized.');
102
+ }
103
+ }
104
+ // Create CDP session for this page
105
+ this._cdpSession = await this._page.createCDPSession();
106
+ // Enable Runtime domain for bindings and evaluation
107
+ await this._cdpSession.send('Runtime.enable');
108
+ // Set up binding for receiving messages from the bridge
109
+ // When the bridge calls window.__mcpBridgeToClient(msg), we receive it here
110
+ await this._cdpSession.send('Runtime.addBinding', {
111
+ name: '__mcpBridgeToClient',
112
+ });
113
+ // Listen for binding calls (messages from bridge → this transport)
114
+ this._cdpSession.on('Runtime.bindingCalled', event => {
115
+ if (event.name !== '__mcpBridgeToClient')
116
+ return;
117
+ try {
118
+ const payload = JSON.parse(event.payload);
119
+ this._handlePayload(payload);
120
+ }
121
+ catch (err) {
122
+ this.onerror?.(new Error(`Failed to parse message from bridge: ${err}`));
123
+ }
124
+ });
125
+ // Listen for page navigation to detect when bridge is lost
126
+ this._page.on('framenavigated', frame => {
127
+ if (frame === this._page.mainFrame()) {
128
+ // Main frame navigated, bridge is gone
129
+ this._handleNavigation();
130
+ }
131
+ });
132
+ // Inject the bridge script
133
+ const result = await this._page.evaluate(WEB_MCP_BRIDGE_SCRIPT);
134
+ if (!result.success && !result.alreadyInjected) {
135
+ throw new Error('Failed to inject WebMCP bridge script');
136
+ }
137
+ this._started = true;
138
+ // Initiate server-ready handshake
139
+ await this._page.evaluate(() => {
140
+ window.__mcpBridge?.checkReady();
141
+ });
142
+ // Wait for server ready with timeout
143
+ const timeoutPromise = new Promise((_, reject) => {
144
+ const timer = setTimeout(() => {
145
+ reject(new Error(`WebMCP server did not respond within ${this._readyTimeout}ms. ` +
146
+ 'Ensure TabServerTransport is running on the page.'));
147
+ }, this._readyTimeout);
148
+ // Clear timeout if promise resolves
149
+ this._serverReadyPromise.then(() => clearTimeout(timer)).catch(() => clearTimeout(timer));
150
+ });
151
+ try {
152
+ await Promise.race([this._serverReadyPromise, timeoutPromise]);
153
+ }
154
+ catch (err) {
155
+ await this.close();
156
+ throw err;
157
+ }
158
+ }
159
+ /**
160
+ * Handle a payload received from the bridge
161
+ */
162
+ _handlePayload(payload) {
163
+ // Handle special string payloads (handshake signals)
164
+ if (typeof payload === 'string') {
165
+ if (payload === 'mcp-server-ready') {
166
+ if (!this._serverReady) {
167
+ this._serverReady = true;
168
+ this._serverReadyResolve();
169
+ }
170
+ return;
171
+ }
172
+ if (payload === 'mcp-server-stopped') {
173
+ this._handleServerStopped();
174
+ return;
175
+ }
176
+ // Unknown string payload - might be an error
177
+ this.onerror?.(new Error(`Unexpected string payload: ${payload}`));
178
+ return;
179
+ }
180
+ // Handle JSON-RPC messages
181
+ if (typeof payload === 'object' && payload !== null) {
182
+ // Resolve server ready on first real message too
183
+ if (!this._serverReady) {
184
+ this._serverReady = true;
185
+ this._serverReadyResolve();
186
+ }
187
+ this.onmessage?.(payload);
188
+ }
189
+ }
190
+ /**
191
+ * Handle page navigation - bridge is lost
192
+ */
193
+ _handleNavigation() {
194
+ if (this._closed)
195
+ return;
196
+ this._serverReady = false;
197
+ this._serverReadyReject(new Error('Page navigated, connection lost'));
198
+ // Reset the promise for potential reconnection
199
+ this._serverReadyPromise = new Promise((resolve, reject) => {
200
+ this._serverReadyResolve = resolve;
201
+ this._serverReadyReject = reject;
202
+ });
203
+ this.onerror?.(new Error('Page navigated, WebMCP connection lost'));
204
+ }
205
+ /**
206
+ * Handle server stopped signal
207
+ */
208
+ _handleServerStopped() {
209
+ this._serverReady = false;
210
+ this.onerror?.(new Error('WebMCP server stopped'));
211
+ }
212
+ /**
213
+ * Send a JSON-RPC message to the WebMCP server
214
+ */
215
+ async send(message) {
216
+ if (!this._started) {
217
+ throw new Error('WebMCPClientTransport not started');
218
+ }
219
+ if (this._closed) {
220
+ throw new Error('WebMCPClientTransport has been closed');
221
+ }
222
+ // Wait for server to be ready before sending
223
+ await this._serverReadyPromise;
224
+ // Send via CDP → bridge → postMessage → TabServer
225
+ const messageJson = JSON.stringify(message);
226
+ try {
227
+ await this._page.evaluate((msg) => {
228
+ const bridge = window.__mcpBridge;
229
+ if (!bridge) {
230
+ throw new Error('WebMCP bridge not found');
231
+ }
232
+ const sent = bridge.toServer(msg);
233
+ if (!sent) {
234
+ throw new Error('Bridge failed to send message');
235
+ }
236
+ }, messageJson);
237
+ }
238
+ catch (err) {
239
+ const error = new Error(`Failed to send message: ${err}`);
240
+ this.onerror?.(error);
241
+ throw error;
242
+ }
243
+ }
244
+ /**
245
+ * Close the transport and clean up resources
246
+ */
247
+ async close() {
248
+ if (this._closed)
249
+ return;
250
+ this._closed = true;
251
+ this._started = false;
252
+ this._serverReady = false;
253
+ // Dispose the bridge script if possible
254
+ try {
255
+ await this._page.evaluate(() => {
256
+ window.__mcpBridge?.dispose();
257
+ });
258
+ }
259
+ catch {
260
+ // Ignore errors during cleanup
261
+ }
262
+ // Detach CDP session
263
+ if (this._cdpSession) {
264
+ try {
265
+ await this._cdpSession.detach();
266
+ }
267
+ catch {
268
+ // Ignore detach errors
269
+ }
270
+ this._cdpSession = null;
271
+ }
272
+ // Reject any pending server ready promise
273
+ this._serverReadyReject(new Error('Transport closed'));
274
+ this.onclose?.();
275
+ }
276
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ export { WebMCPClientTransport, } from './WebMCPClientTransport.js';
7
+ export { WEB_MCP_BRIDGE_SCRIPT, CHECK_WEBMCP_AVAILABLE_SCRIPT, } from './WebMCPBridgeScript.js';
@@ -0,0 +1,296 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ // See the KeyInput type for the list of supported keys.
7
+ const validKeys = new Set([
8
+ '0',
9
+ '1',
10
+ '2',
11
+ '3',
12
+ '4',
13
+ '5',
14
+ '6',
15
+ '7',
16
+ '8',
17
+ '9',
18
+ 'Power',
19
+ 'Eject',
20
+ 'Abort',
21
+ 'Help',
22
+ 'Backspace',
23
+ 'Tab',
24
+ 'Numpad5',
25
+ 'NumpadEnter',
26
+ 'Enter',
27
+ '\r',
28
+ '\n',
29
+ 'ShiftLeft',
30
+ 'ShiftRight',
31
+ 'ControlLeft',
32
+ 'ControlRight',
33
+ 'AltLeft',
34
+ 'AltRight',
35
+ 'Pause',
36
+ 'CapsLock',
37
+ 'Escape',
38
+ 'Convert',
39
+ 'NonConvert',
40
+ 'Space',
41
+ 'Numpad9',
42
+ 'PageUp',
43
+ 'Numpad3',
44
+ 'PageDown',
45
+ 'End',
46
+ 'Numpad1',
47
+ 'Home',
48
+ 'Numpad7',
49
+ 'ArrowLeft',
50
+ 'Numpad4',
51
+ 'Numpad8',
52
+ 'ArrowUp',
53
+ 'ArrowRight',
54
+ 'Numpad6',
55
+ 'Numpad2',
56
+ 'ArrowDown',
57
+ 'Select',
58
+ 'Open',
59
+ 'PrintScreen',
60
+ 'Insert',
61
+ 'Numpad0',
62
+ 'Delete',
63
+ 'NumpadDecimal',
64
+ 'Digit0',
65
+ 'Digit1',
66
+ 'Digit2',
67
+ 'Digit3',
68
+ 'Digit4',
69
+ 'Digit5',
70
+ 'Digit6',
71
+ 'Digit7',
72
+ 'Digit8',
73
+ 'Digit9',
74
+ 'KeyA',
75
+ 'KeyB',
76
+ 'KeyC',
77
+ 'KeyD',
78
+ 'KeyE',
79
+ 'KeyF',
80
+ 'KeyG',
81
+ 'KeyH',
82
+ 'KeyI',
83
+ 'KeyJ',
84
+ 'KeyK',
85
+ 'KeyL',
86
+ 'KeyM',
87
+ 'KeyN',
88
+ 'KeyO',
89
+ 'KeyP',
90
+ 'KeyQ',
91
+ 'KeyR',
92
+ 'KeyS',
93
+ 'KeyT',
94
+ 'KeyU',
95
+ 'KeyV',
96
+ 'KeyW',
97
+ 'KeyX',
98
+ 'KeyY',
99
+ 'KeyZ',
100
+ 'MetaLeft',
101
+ 'MetaRight',
102
+ 'ContextMenu',
103
+ 'NumpadMultiply',
104
+ 'NumpadAdd',
105
+ 'NumpadSubtract',
106
+ 'NumpadDivide',
107
+ 'F1',
108
+ 'F2',
109
+ 'F3',
110
+ 'F4',
111
+ 'F5',
112
+ 'F6',
113
+ 'F7',
114
+ 'F8',
115
+ 'F9',
116
+ 'F10',
117
+ 'F11',
118
+ 'F12',
119
+ 'F13',
120
+ 'F14',
121
+ 'F15',
122
+ 'F16',
123
+ 'F17',
124
+ 'F18',
125
+ 'F19',
126
+ 'F20',
127
+ 'F21',
128
+ 'F22',
129
+ 'F23',
130
+ 'F24',
131
+ 'NumLock',
132
+ 'ScrollLock',
133
+ 'AudioVolumeMute',
134
+ 'AudioVolumeDown',
135
+ 'AudioVolumeUp',
136
+ 'MediaTrackNext',
137
+ 'MediaTrackPrevious',
138
+ 'MediaStop',
139
+ 'MediaPlayPause',
140
+ 'Semicolon',
141
+ 'Equal',
142
+ 'NumpadEqual',
143
+ 'Comma',
144
+ 'Minus',
145
+ 'Period',
146
+ 'Slash',
147
+ 'Backquote',
148
+ 'BracketLeft',
149
+ 'Backslash',
150
+ 'BracketRight',
151
+ 'Quote',
152
+ 'AltGraph',
153
+ 'Props',
154
+ 'Cancel',
155
+ 'Clear',
156
+ 'Shift',
157
+ 'Control',
158
+ 'Alt',
159
+ 'Accept',
160
+ 'ModeChange',
161
+ ' ',
162
+ 'Print',
163
+ 'Execute',
164
+ '\u0000',
165
+ 'a',
166
+ 'b',
167
+ 'c',
168
+ 'd',
169
+ 'e',
170
+ 'f',
171
+ 'g',
172
+ 'h',
173
+ 'i',
174
+ 'j',
175
+ 'k',
176
+ 'l',
177
+ 'm',
178
+ 'n',
179
+ 'o',
180
+ 'p',
181
+ 'q',
182
+ 'r',
183
+ 's',
184
+ 't',
185
+ 'u',
186
+ 'v',
187
+ 'w',
188
+ 'x',
189
+ 'y',
190
+ 'z',
191
+ 'Meta',
192
+ '*',
193
+ '+',
194
+ '-',
195
+ '/',
196
+ ';',
197
+ '=',
198
+ ',',
199
+ '.',
200
+ '`',
201
+ '[',
202
+ '\\',
203
+ ']',
204
+ "'",
205
+ 'Attn',
206
+ 'CrSel',
207
+ 'ExSel',
208
+ 'EraseEof',
209
+ 'Play',
210
+ 'ZoomOut',
211
+ ')',
212
+ '!',
213
+ '@',
214
+ '#',
215
+ '$',
216
+ '%',
217
+ '^',
218
+ '&',
219
+ '(',
220
+ 'A',
221
+ 'B',
222
+ 'C',
223
+ 'D',
224
+ 'E',
225
+ 'F',
226
+ 'G',
227
+ 'H',
228
+ 'I',
229
+ 'J',
230
+ 'K',
231
+ 'L',
232
+ 'M',
233
+ 'N',
234
+ 'O',
235
+ 'P',
236
+ 'Q',
237
+ 'R',
238
+ 'S',
239
+ 'T',
240
+ 'U',
241
+ 'V',
242
+ 'W',
243
+ 'X',
244
+ 'Y',
245
+ 'Z',
246
+ ':',
247
+ '<',
248
+ '_',
249
+ '>',
250
+ '?',
251
+ '~',
252
+ '{',
253
+ '|',
254
+ '}',
255
+ '"',
256
+ 'SoftLeft',
257
+ 'SoftRight',
258
+ 'Camera',
259
+ 'Call',
260
+ 'EndCall',
261
+ 'VolumeDown',
262
+ 'VolumeUp',
263
+ ]);
264
+ function throwIfInvalidKey(key) {
265
+ if (validKeys.has(key)) {
266
+ return key;
267
+ }
268
+ throw new Error(`${key} is invalid. Valid keys are: ${Array.from(validKeys.values()).join(',')}.`);
269
+ }
270
+ /**
271
+ * Returns the primary key, followed by modifiers in original order.
272
+ */
273
+ export function parseKey(keyInput) {
274
+ let key = '';
275
+ const result = [];
276
+ for (const ch of keyInput) {
277
+ // Handle cases like Shift++.
278
+ if (ch === '+' && key) {
279
+ result.push(throwIfInvalidKey(key));
280
+ key = '';
281
+ }
282
+ else {
283
+ key += ch;
284
+ }
285
+ }
286
+ if (key) {
287
+ result.push(throwIfInvalidKey(key));
288
+ }
289
+ if (result.length === 0) {
290
+ throw new Error(`Key ${keyInput} could not be parsed.`);
291
+ }
292
+ if (new Set(result).size !== result.length) {
293
+ throw new Error(`Key ${keyInput} contains duplicate keys.`);
294
+ }
295
+ return [result.at(-1), ...result.slice(0, -1)];
296
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ const DEFAULT_PAGE_SIZE = 20;
7
+ export function paginate(items, options) {
8
+ const total = items.length;
9
+ if (!options || noPaginationOptions(options)) {
10
+ return {
11
+ items,
12
+ currentPage: 0,
13
+ totalPages: 1,
14
+ hasNextPage: false,
15
+ hasPreviousPage: false,
16
+ startIndex: 0,
17
+ endIndex: total,
18
+ invalidPage: false,
19
+ };
20
+ }
21
+ const pageSize = options.pageSize ?? DEFAULT_PAGE_SIZE;
22
+ const totalPages = Math.max(1, Math.ceil(total / pageSize));
23
+ const { currentPage, invalidPage } = resolvePageIndex(options.pageIdx, totalPages);
24
+ const startIndex = currentPage * pageSize;
25
+ const pageItems = items.slice(startIndex, startIndex + pageSize);
26
+ const endIndex = startIndex + pageItems.length;
27
+ return {
28
+ items: pageItems,
29
+ currentPage,
30
+ totalPages,
31
+ hasNextPage: currentPage < totalPages - 1,
32
+ hasPreviousPage: currentPage > 0,
33
+ startIndex,
34
+ endIndex,
35
+ invalidPage,
36
+ };
37
+ }
38
+ function noPaginationOptions(options) {
39
+ return options.pageSize === undefined && options.pageIdx === undefined;
40
+ }
41
+ function resolvePageIndex(pageIdx, totalPages) {
42
+ if (pageIdx === undefined) {
43
+ return { currentPage: 0, invalidPage: false };
44
+ }
45
+ if (pageIdx < 0 || pageIdx >= totalPages) {
46
+ return { currentPage: 0, invalidPage: true };
47
+ }
48
+ return { currentPage: pageIdx, invalidPage: false };
49
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ export {};