chrome-devtools-mcp 0.8.1 → 0.9.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 (87) hide show
  1. package/README.md +58 -3
  2. package/build/node_modules/chrome-devtools-frontend/front_end/core/common/Gzip.js +8 -6
  3. package/build/node_modules/chrome-devtools-frontend/front_end/core/common/Settings.js +1 -1
  4. package/build/node_modules/chrome-devtools-frontend/front_end/core/common/Worker.js +10 -2
  5. package/build/node_modules/chrome-devtools-frontend/front_end/core/host/UserMetrics.js +2 -1
  6. package/build/node_modules/chrome-devtools-frontend/front_end/core/platform/ArrayUtilities.js +1 -1
  7. package/build/node_modules/chrome-devtools-frontend/front_end/core/protocol_client/ConnectionTransport.js +12 -0
  8. package/build/node_modules/chrome-devtools-frontend/front_end/core/protocol_client/InspectorBackend.js +15 -27
  9. package/build/node_modules/chrome-devtools-frontend/front_end/core/protocol_client/protocol_client.js +2 -8
  10. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/CSSMatchedStyles.js +42 -7
  11. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/CSSRule.js +34 -6
  12. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/ChildTargetManager.js +3 -0
  13. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/Connections.js +2 -2
  14. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/DOMModel.js +3 -0
  15. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/DebuggerModel.js +2 -1
  16. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/NetworkManager.js +336 -40
  17. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/PreloadingModel.js +56 -13
  18. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/RehydratingConnection.js +32 -7
  19. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/ResourceTreeModel.js +1 -1
  20. package/build/node_modules/chrome-devtools-frontend/front_end/{models/source_map_scopes → core/sdk}/ScopeTreeCache.js +9 -5
  21. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/SourceMap.js +48 -11
  22. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/SourceMapManager.js +8 -2
  23. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/SourceMapScopesInfo.js +131 -8
  24. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/TargetManager.js +0 -21
  25. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/TraceObject.js +9 -6
  26. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/sdk.js +2 -1
  27. package/build/node_modules/chrome-devtools-frontend/front_end/generated/ARIAProperties.js +1301 -174
  28. package/build/node_modules/chrome-devtools-frontend/front_end/generated/Deprecation.js +7 -0
  29. package/build/node_modules/chrome-devtools-frontend/front_end/generated/InspectorBackendCommands.js +8 -6
  30. package/build/node_modules/chrome-devtools-frontend/front_end/generated/SupportedCSSProperties.js +16 -19
  31. package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.js +50 -34
  32. package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/performance/AICallTree.js +2 -3
  33. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/CompilerScriptMapping.js +45 -2
  34. package/build/node_modules/chrome-devtools-frontend/front_end/models/formatter/FormatterWorkerPool.js +14 -0
  35. package/build/node_modules/chrome-devtools-frontend/front_end/models/source_map_scopes/NamesResolver.js +5 -11
  36. package/build/node_modules/chrome-devtools-frontend/front_end/models/source_map_scopes/source_map_scopes.js +1 -2
  37. package/build/node_modules/chrome-devtools-frontend/front_end/models/stack_trace/Trie.js +8 -0
  38. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/ModelImpl.js +6 -3
  39. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/extras/TraceTree.js +10 -3
  40. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/handlers/MetaHandler.js +4 -1
  41. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/handlers/UserTimingsHandler.js +1 -1
  42. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/CLSCulprits.js +2 -1
  43. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/Cache.js +2 -1
  44. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/DOMSize.js +2 -1
  45. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/DocumentLatency.js +2 -1
  46. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/DuplicatedJavaScript.js +2 -1
  47. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/FontDisplay.js +2 -1
  48. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/ForcedReflow.js +3 -2
  49. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/INPBreakdown.js +2 -1
  50. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/ImageDelivery.js +2 -1
  51. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/LCPBreakdown.js +2 -1
  52. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/LCPDiscovery.js +2 -1
  53. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/LegacyJavaScript.js +2 -1
  54. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/ModernHTTP.js +2 -1
  55. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/NetworkDependencyTree.js +2 -1
  56. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/RenderBlocking.js +2 -1
  57. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/SlowCSSSelector.js +2 -1
  58. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/ThirdParties.js +2 -1
  59. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/Viewport.js +2 -1
  60. package/build/src/DevToolsConnectionAdapter.js +32 -0
  61. package/build/src/McpContext.js +70 -31
  62. package/build/src/McpResponse.js +145 -44
  63. package/build/src/PageCollector.js +110 -26
  64. package/build/src/WaitForHelper.js +5 -0
  65. package/build/src/browser.js +16 -4
  66. package/build/src/cli.js +82 -6
  67. package/build/src/formatters/consoleFormatter.js +29 -62
  68. package/build/src/formatters/networkFormatter.js +5 -6
  69. package/build/src/formatters/snapshotFormatter.js +10 -5
  70. package/build/src/logger.js +1 -1
  71. package/build/src/main.js +18 -5
  72. package/build/src/polyfill.js +2 -2
  73. package/build/src/third_party/THIRD_PARTY_NOTICES +1393 -0
  74. package/build/src/third_party/index.js +76159 -0
  75. package/build/src/tools/ToolDefinition.js +2 -2
  76. package/build/src/tools/categories.js +17 -9
  77. package/build/src/tools/console.js +71 -6
  78. package/build/src/tools/emulation.js +6 -7
  79. package/build/src/tools/input.js +21 -21
  80. package/build/src/tools/network.js +18 -10
  81. package/build/src/tools/pages.js +19 -19
  82. package/build/src/tools/performance.js +8 -8
  83. package/build/src/tools/screenshot.js +8 -8
  84. package/build/src/tools/script.js +29 -15
  85. package/build/src/tools/snapshot.js +15 -20
  86. package/build/src/utils/types.js +6 -0
  87. package/package.json +16 -13
@@ -1,4 +1,4 @@
1
- import { formatConsoleEvent } from './formatters/consoleFormatter.js';
1
+ import { formatConsoleEventShort, formatConsoleEventVerbose, } from './formatters/consoleFormatter.js';
2
2
  import { getFormattedHeaderValue, getFormattedResponseBody, getFormattedRequestBody, getShortDescriptionForRequest, getStatusFromRequest, } from './formatters/networkFormatter.js';
3
3
  import { formatA11ySnapshot } from './formatters/snapshotFormatter.js';
4
4
  import { handleDialog } from './tools/pages.js';
@@ -6,17 +6,19 @@ import { paginate } from './utils/pagination.js';
6
6
  export class McpResponse {
7
7
  #includePages = false;
8
8
  #includeSnapshot = false;
9
- #attachedNetworkRequestData;
10
- #includeConsoleData = false;
9
+ #includeVerboseSnapshot = false;
10
+ #attachedNetworkRequestId;
11
+ #attachedConsoleMessageId;
11
12
  #textResponseLines = [];
12
- #formattedConsoleData;
13
13
  #images = [];
14
14
  #networkRequestsOptions;
15
+ #consoleDataOptions;
15
16
  setIncludePages(value) {
16
17
  this.#includePages = value;
17
18
  }
18
- setIncludeSnapshot(value) {
19
+ setIncludeSnapshot(value, verbose = false) {
19
20
  this.#includeSnapshot = value;
21
+ this.#includeVerboseSnapshot = verbose;
20
22
  }
21
23
  setIncludeNetworkRequests(value, options) {
22
24
  if (!value) {
@@ -32,16 +34,32 @@ export class McpResponse {
32
34
  }
33
35
  : undefined,
34
36
  resourceTypes: options?.resourceTypes,
37
+ includePreservedRequests: options?.includePreservedRequests,
35
38
  };
36
39
  }
37
- setIncludeConsoleData(value) {
38
- this.#includeConsoleData = value;
39
- }
40
- attachNetworkRequest(url) {
41
- this.#attachedNetworkRequestData = {
42
- networkRequestUrl: url,
40
+ setIncludeConsoleData(value, options) {
41
+ if (!value) {
42
+ this.#consoleDataOptions = undefined;
43
+ return;
44
+ }
45
+ this.#consoleDataOptions = {
46
+ include: value,
47
+ pagination: options?.pageSize || options?.pageIdx
48
+ ? {
49
+ pageSize: options.pageSize,
50
+ pageIdx: options.pageIdx,
51
+ }
52
+ : undefined,
53
+ types: options?.types,
54
+ includePreservedMessages: options?.includePreservedMessages,
43
55
  };
44
56
  }
57
+ attachNetworkRequest(reqid) {
58
+ this.#attachedNetworkRequestId = reqid;
59
+ }
60
+ attachConsoleMessage(msgid) {
61
+ this.#attachedConsoleMessageId = msgid;
62
+ }
45
63
  get includePages() {
46
64
  return this.#includePages;
47
65
  }
@@ -49,14 +67,20 @@ export class McpResponse {
49
67
  return this.#networkRequestsOptions?.include ?? false;
50
68
  }
51
69
  get includeConsoleData() {
52
- return this.#includeConsoleData;
70
+ return this.#consoleDataOptions?.include ?? false;
53
71
  }
54
- get attachedNetworkRequestUrl() {
55
- return this.#attachedNetworkRequestData?.networkRequestUrl;
72
+ get attachedNetworkRequestId() {
73
+ return this.#attachedNetworkRequestId;
56
74
  }
57
75
  get networkRequestsPageIdx() {
58
76
  return this.#networkRequestsOptions?.pagination?.pageIdx;
59
77
  }
78
+ get consoleMessagesPageIdx() {
79
+ return this.#consoleDataOptions?.pagination?.pageIdx;
80
+ }
81
+ get consoleMessagesTypes() {
82
+ return this.#consoleDataOptions?.types;
83
+ }
60
84
  appendResponseLine(value) {
61
85
  this.#textResponseLines.push(value);
62
86
  }
@@ -72,34 +96,99 @@ export class McpResponse {
72
96
  get includeSnapshot() {
73
97
  return this.#includeSnapshot;
74
98
  }
99
+ get includeVersboseSnapshot() {
100
+ return this.#includeVerboseSnapshot;
101
+ }
75
102
  async handle(toolName, context) {
76
103
  if (this.#includePages) {
77
104
  await context.createPagesSnapshot();
78
105
  }
79
106
  if (this.#includeSnapshot) {
80
- await context.createTextSnapshot();
107
+ await context.createTextSnapshot(this.#includeVerboseSnapshot);
81
108
  }
82
- let formattedConsoleMessages;
83
- if (this.#attachedNetworkRequestData?.networkRequestUrl) {
84
- const request = context.getNetworkRequestByUrl(this.#attachedNetworkRequestData.networkRequestUrl);
85
- this.#attachedNetworkRequestData.requestBody =
86
- await getFormattedRequestBody(request);
109
+ const bodies = {};
110
+ if (this.#attachedNetworkRequestId) {
111
+ const request = context.getNetworkRequestById(this.#attachedNetworkRequestId);
112
+ bodies.requestBody = await getFormattedRequestBody(request);
87
113
  const response = request.response();
88
114
  if (response) {
89
- this.#attachedNetworkRequestData.responseBody =
90
- await getFormattedResponseBody(response);
115
+ bodies.responseBody = await getFormattedResponseBody(response);
91
116
  }
92
117
  }
93
- if (this.#includeConsoleData) {
94
- const consoleMessages = context.getConsoleData();
95
- if (consoleMessages) {
96
- formattedConsoleMessages = await Promise.all(consoleMessages.map(message => formatConsoleEvent(message)));
97
- this.#formattedConsoleData = formattedConsoleMessages;
118
+ let consoleData;
119
+ if (this.#attachedConsoleMessageId) {
120
+ const message = context.getConsoleMessageById(this.#attachedConsoleMessageId);
121
+ const consoleMessageStableId = this.#attachedConsoleMessageId;
122
+ if ('args' in message) {
123
+ const consoleMessage = message;
124
+ consoleData = {
125
+ consoleMessageStableId,
126
+ type: consoleMessage.type(),
127
+ message: consoleMessage.text(),
128
+ args: await Promise.all(consoleMessage.args().map(async (arg) => {
129
+ const stringArg = await arg.jsonValue().catch(() => {
130
+ // Ignore errors.
131
+ });
132
+ return typeof stringArg === 'object'
133
+ ? JSON.stringify(stringArg)
134
+ : String(stringArg);
135
+ })),
136
+ };
137
+ }
138
+ else {
139
+ consoleData = {
140
+ consoleMessageStableId,
141
+ type: 'error',
142
+ message: message.message,
143
+ args: [],
144
+ };
98
145
  }
99
146
  }
100
- return this.format(toolName, context);
147
+ let consoleListData;
148
+ if (this.#consoleDataOptions?.include) {
149
+ let messages = context.getConsoleData(this.#consoleDataOptions.includePreservedMessages);
150
+ if (this.#consoleDataOptions.types?.length) {
151
+ const normalizedTypes = new Set(this.#consoleDataOptions.types);
152
+ messages = messages.filter(message => {
153
+ if ('type' in message) {
154
+ return normalizedTypes.has(message.type());
155
+ }
156
+ return normalizedTypes.has('error');
157
+ });
158
+ }
159
+ consoleListData = await Promise.all(messages.map(async (item) => {
160
+ const consoleMessageStableId = context.getConsoleMessageStableId(item);
161
+ if ('args' in item) {
162
+ const consoleMessage = item;
163
+ return {
164
+ consoleMessageStableId,
165
+ type: consoleMessage.type(),
166
+ message: consoleMessage.text(),
167
+ args: await Promise.all(consoleMessage.args().map(async (arg) => {
168
+ const stringArg = await arg.jsonValue().catch(() => {
169
+ // Ignore errors.
170
+ });
171
+ return typeof stringArg === 'object'
172
+ ? JSON.stringify(stringArg)
173
+ : String(stringArg);
174
+ })),
175
+ };
176
+ }
177
+ return {
178
+ consoleMessageStableId,
179
+ type: 'error',
180
+ message: item.message,
181
+ args: [],
182
+ };
183
+ }));
184
+ }
185
+ return this.format(toolName, context, {
186
+ bodies,
187
+ consoleData,
188
+ consoleListData,
189
+ });
101
190
  }
102
- format(toolName, context) {
191
+ format(toolName, context, data) {
103
192
  const response = [`# ${toolName} response`];
104
193
  for (const line of this.#textResponseLines) {
105
194
  response.push(line);
@@ -141,9 +230,10 @@ Call ${handleDialog.name} to handle it before continuing.`);
141
230
  response.push(formattedSnapshot);
142
231
  }
143
232
  }
144
- response.push(...this.#getIncludeNetworkRequestsData(context));
233
+ response.push(...this.#formatNetworkRequestData(context, data.bodies));
234
+ response.push(...this.#formatConsoleData(data.consoleData));
145
235
  if (this.#networkRequestsOptions?.include) {
146
- let requests = context.getNetworkRequests();
236
+ let requests = context.getNetworkRequests(this.#networkRequestsOptions?.includePreservedRequests);
147
237
  // Apply resource type filtering if specified
148
238
  if (this.#networkRequestsOptions.resourceTypes?.length) {
149
239
  const normalizedTypes = new Set(this.#networkRequestsOptions.resourceTypes);
@@ -157,17 +247,20 @@ Call ${handleDialog.name} to handle it before continuing.`);
157
247
  const data = this.#dataWithPagination(requests, this.#networkRequestsOptions.pagination);
158
248
  response.push(...data.info);
159
249
  for (const request of data.items) {
160
- response.push(getShortDescriptionForRequest(request));
250
+ response.push(getShortDescriptionForRequest(request, context.getNetworkRequestStableId(request)));
161
251
  }
162
252
  }
163
253
  else {
164
254
  response.push('No requests found.');
165
255
  }
166
256
  }
167
- if (this.#includeConsoleData && this.#formattedConsoleData) {
257
+ if (this.#consoleDataOptions?.include) {
258
+ const messages = data.consoleListData ?? [];
168
259
  response.push('## Console messages');
169
- if (this.#formattedConsoleData.length) {
170
- response.push(...this.#formattedConsoleData);
260
+ if (messages.length) {
261
+ const data = this.#dataWithPagination(messages, this.#consoleDataOptions.pagination);
262
+ response.push(...data.info);
263
+ response.push(...data.items.map(message => formatConsoleEventShort(message)));
171
264
  }
172
265
  else {
173
266
  response.push('<no console messages found>');
@@ -206,22 +299,30 @@ Call ${handleDialog.name} to handle it before continuing.`);
206
299
  items: paginationResult.items,
207
300
  };
208
301
  }
209
- #getIncludeNetworkRequestsData(context) {
302
+ #formatConsoleData(data) {
303
+ const response = [];
304
+ if (!data) {
305
+ return response;
306
+ }
307
+ response.push(formatConsoleEventVerbose(data));
308
+ return response;
309
+ }
310
+ #formatNetworkRequestData(context, data) {
210
311
  const response = [];
211
- const url = this.#attachedNetworkRequestData?.networkRequestUrl;
212
- if (!url) {
312
+ const id = this.#attachedNetworkRequestId;
313
+ if (!id) {
213
314
  return response;
214
315
  }
215
- const httpRequest = context.getNetworkRequestByUrl(url);
316
+ const httpRequest = context.getNetworkRequestById(id);
216
317
  response.push(`## Request ${httpRequest.url()}`);
217
318
  response.push(`Status: ${getStatusFromRequest(httpRequest)}`);
218
319
  response.push(`### Request Headers`);
219
320
  for (const line of getFormattedHeaderValue(httpRequest.headers())) {
220
321
  response.push(line);
221
322
  }
222
- if (this.#attachedNetworkRequestData?.requestBody) {
323
+ if (data.requestBody) {
223
324
  response.push(`### Request Body`);
224
- response.push(this.#attachedNetworkRequestData.requestBody);
325
+ response.push(data.requestBody);
225
326
  }
226
327
  const httpResponse = httpRequest.response();
227
328
  if (httpResponse) {
@@ -230,9 +331,9 @@ Call ${handleDialog.name} to handle it before continuing.`);
230
331
  response.push(line);
231
332
  }
232
333
  }
233
- if (this.#attachedNetworkRequestData?.responseBody) {
334
+ if (data.responseBody) {
234
335
  response.push(`### Response Body`);
235
- response.push(this.#attachedNetworkRequestData.responseBody);
336
+ response.push(data.responseBody);
236
337
  }
237
338
  const httpFailure = httpRequest.failure();
238
339
  if (httpFailure) {
@@ -244,7 +345,7 @@ Call ${handleDialog.name} to handle it before continuing.`);
244
345
  response.push(`### Redirect chain`);
245
346
  let indent = 0;
246
347
  for (const request of redirectChain.reverse()) {
247
- response.push(`${' '.repeat(indent)}${getShortDescriptionForRequest(request)}`);
348
+ response.push(`${' '.repeat(indent)}${getShortDescriptionForRequest(request, context.getNetworkRequestStableId(request))}`);
248
349
  indent++;
249
350
  }
250
351
  }
@@ -3,18 +3,30 @@
3
3
  * Copyright 2025 Google LLC
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
+ function createIdGenerator() {
7
+ let i = 1;
8
+ return () => {
9
+ if (i === Number.MAX_SAFE_INTEGER) {
10
+ i = 0;
11
+ }
12
+ return i++;
13
+ };
14
+ }
15
+ export const stableIdSymbol = Symbol('stableIdSymbol');
6
16
  export class PageCollector {
7
17
  #browser;
8
- #initializer;
18
+ #listenersInitializer;
19
+ #listeners = new WeakMap();
20
+ #maxNavigationSaved = 3;
9
21
  /**
10
- * The Array in this map should only be set once
11
- * As we use the reference to it.
12
- * Use methods that manipulate the array in place.
22
+ * This maps a Page to a list of navigations with a sub-list
23
+ * of all collected resources.
24
+ * The newer navigations come first.
13
25
  */
14
26
  storage = new WeakMap();
15
- constructor(browser, initializer) {
27
+ constructor(browser, listeners) {
16
28
  this.#browser = browser;
17
- this.#initializer = initializer;
29
+ this.#listenersInitializer = listeners;
18
30
  }
19
31
  async init() {
20
32
  const pages = await this.#browser.pages();
@@ -28,6 +40,13 @@ export class PageCollector {
28
40
  }
29
41
  this.#initializePage(page);
30
42
  });
43
+ this.#browser.on('targetdestroyed', async (target) => {
44
+ const page = await target.page();
45
+ if (!page) {
46
+ return;
47
+ }
48
+ this.#cleanupPageDestroyed(page);
49
+ });
31
50
  }
32
51
  addPage(page) {
33
52
  this.#initializePage(page);
@@ -36,36 +55,95 @@ export class PageCollector {
36
55
  if (this.storage.has(page)) {
37
56
  return;
38
57
  }
39
- const stored = [];
40
- this.storage.set(page, stored);
41
- page.on('framenavigated', frame => {
42
- // Only reset the storage on main frame navigation
58
+ const idGenerator = createIdGenerator();
59
+ const storedLists = [[]];
60
+ this.storage.set(page, storedLists);
61
+ const listeners = this.#listenersInitializer(value => {
62
+ const withId = value;
63
+ withId[stableIdSymbol] = idGenerator();
64
+ const navigations = this.storage.get(page) ?? [[]];
65
+ navigations[0].push(withId);
66
+ });
67
+ listeners['framenavigated'] = (frame) => {
68
+ // Only split the storage on main frame navigation
43
69
  if (frame !== page.mainFrame()) {
44
70
  return;
45
71
  }
46
- this.cleanup(page);
47
- });
48
- this.#initializer(page, value => {
49
- stored.push(value);
50
- });
72
+ this.splitAfterNavigation(page);
73
+ };
74
+ for (const [name, listener] of Object.entries(listeners)) {
75
+ page.on(name, listener);
76
+ }
77
+ this.#listeners.set(page, listeners);
51
78
  }
52
- cleanup(page) {
53
- const collection = this.storage.get(page);
54
- if (collection) {
55
- // Keep the reference alive
56
- collection.length = 0;
79
+ splitAfterNavigation(page) {
80
+ const navigations = this.storage.get(page);
81
+ if (!navigations) {
82
+ return;
57
83
  }
84
+ // Add the latest navigation first
85
+ navigations.unshift([]);
86
+ navigations.splice(this.#maxNavigationSaved);
58
87
  }
59
- getData(page) {
60
- return this.storage.get(page) ?? [];
88
+ #cleanupPageDestroyed(page) {
89
+ const listeners = this.#listeners.get(page);
90
+ if (listeners) {
91
+ for (const [name, listener] of Object.entries(listeners)) {
92
+ page.off(name, listener);
93
+ }
94
+ }
95
+ this.storage.delete(page);
96
+ }
97
+ getData(page, includePreservedData) {
98
+ const navigations = this.storage.get(page);
99
+ if (!navigations) {
100
+ return [];
101
+ }
102
+ if (!includePreservedData) {
103
+ return navigations[0];
104
+ }
105
+ const data = [];
106
+ for (let index = this.#maxNavigationSaved; index >= 0; index--) {
107
+ if (navigations[index]) {
108
+ data.push(...navigations[index]);
109
+ }
110
+ }
111
+ return data;
112
+ }
113
+ getIdForResource(resource) {
114
+ return resource[stableIdSymbol] ?? -1;
115
+ }
116
+ getById(page, stableId) {
117
+ const navigations = this.storage.get(page);
118
+ if (!navigations) {
119
+ throw new Error('No requests found for selected page');
120
+ }
121
+ for (const navigation of navigations) {
122
+ for (const collected of navigation) {
123
+ if (collected[stableIdSymbol] === stableId) {
124
+ return collected;
125
+ }
126
+ }
127
+ }
128
+ throw new Error('Request not found for selected page');
61
129
  }
62
130
  }
63
131
  export class NetworkCollector extends PageCollector {
64
- cleanup(page) {
65
- const requests = this.storage.get(page) ?? [];
66
- if (!requests) {
132
+ constructor(browser, listeners = collect => {
133
+ return {
134
+ request: req => {
135
+ collect(req);
136
+ },
137
+ };
138
+ }) {
139
+ super(browser, listeners);
140
+ }
141
+ splitAfterNavigation(page) {
142
+ const navigations = this.storage.get(page) ?? [];
143
+ if (!navigations) {
67
144
  return;
68
145
  }
146
+ const requests = navigations[0];
69
147
  const lastRequestIdx = requests.findLastIndex(request => {
70
148
  return request.frame() === page.mainFrame()
71
149
  ? request.isNavigationRequest()
@@ -74,6 +152,12 @@ export class NetworkCollector extends PageCollector {
74
152
  // Keep all requests since the last navigation request including that
75
153
  // navigation request itself.
76
154
  // Keep the reference
77
- requests.splice(0, Math.max(lastRequestIdx, 0));
155
+ if (lastRequestIdx !== -1) {
156
+ const fromCurrentNavigation = requests.splice(lastRequestIdx);
157
+ navigations.unshift(fromCurrentNavigation);
158
+ }
159
+ else {
160
+ navigations.unshift([]);
161
+ }
78
162
  }
79
163
  }
@@ -1,3 +1,8 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
1
6
  import { logger } from './logger.js';
2
7
  export class WaitForHelper {
3
8
  #abortController = new AbortController();
@@ -6,7 +6,7 @@
6
6
  import fs from 'node:fs';
7
7
  import os from 'node:os';
8
8
  import path from 'node:path';
9
- import puppeteer from 'puppeteer-core';
9
+ import { puppeteer } from './third_party/index.js';
10
10
  let browser;
11
11
  function makeTargetFilter(devtools) {
12
12
  const ignoredPrefixes = new Set([
@@ -33,12 +33,24 @@ export async function ensureBrowserConnected(options) {
33
33
  if (browser?.connected) {
34
34
  return browser;
35
35
  }
36
- browser = await puppeteer.connect({
36
+ const connectOptions = {
37
37
  targetFilter: makeTargetFilter(options.devtools),
38
- browserURL: options.browserURL,
39
38
  defaultViewport: null,
40
39
  handleDevToolsAsPage: options.devtools,
41
- });
40
+ };
41
+ if (options.wsEndpoint) {
42
+ connectOptions.browserWSEndpoint = options.wsEndpoint;
43
+ if (options.wsHeaders) {
44
+ connectOptions.headers = options.wsHeaders;
45
+ }
46
+ }
47
+ else if (options.browserURL) {
48
+ connectOptions.browserURL = options.browserURL;
49
+ }
50
+ else {
51
+ throw new Error('Either browserURL or wsEndpoint must be provided');
52
+ }
53
+ browser = await puppeteer.connect(connectOptions);
42
54
  return browser;
43
55
  }
44
56
  export async function launch(options) {
package/build/src/cli.js CHANGED
@@ -3,13 +3,13 @@
3
3
  * Copyright 2025 Google LLC
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
- import yargs from 'yargs';
7
- import { hideBin } from 'yargs/helpers';
6
+ import { yargs, hideBin } from './third_party/index.js';
8
7
  export const cliOptions = {
9
8
  browserUrl: {
10
9
  type: 'string',
11
10
  description: 'Connect to a running Chrome instance using port forwarding. For more details see: https://developer.chrome.com/docs/devtools/remote-debugging/local-server.',
12
11
  alias: 'u',
12
+ conflicts: 'wsEndpoint',
13
13
  coerce: (url) => {
14
14
  if (!url) {
15
15
  return;
@@ -23,6 +23,50 @@ export const cliOptions = {
23
23
  return url;
24
24
  },
25
25
  },
26
+ wsEndpoint: {
27
+ type: 'string',
28
+ description: 'WebSocket endpoint to connect to a running Chrome instance (e.g., ws://127.0.0.1:9222/devtools/browser/<id>). Alternative to --browserUrl.',
29
+ alias: 'w',
30
+ conflicts: 'browserUrl',
31
+ coerce: (url) => {
32
+ if (!url) {
33
+ return;
34
+ }
35
+ try {
36
+ const parsed = new URL(url);
37
+ if (parsed.protocol !== 'ws:' && parsed.protocol !== 'wss:') {
38
+ throw new Error(`Provided wsEndpoint ${url} must use ws:// or wss:// protocol.`);
39
+ }
40
+ return url;
41
+ }
42
+ catch (error) {
43
+ if (error.message.includes('ws://')) {
44
+ throw error;
45
+ }
46
+ throw new Error(`Provided wsEndpoint ${url} is not valid URL.`);
47
+ }
48
+ },
49
+ },
50
+ wsHeaders: {
51
+ type: 'string',
52
+ description: 'Custom headers for WebSocket connection in JSON format (e.g., \'{"Authorization":"Bearer token"}\'). Only works with --wsEndpoint.',
53
+ implies: 'wsEndpoint',
54
+ coerce: (val) => {
55
+ if (!val) {
56
+ return;
57
+ }
58
+ try {
59
+ const parsed = JSON.parse(val);
60
+ if (typeof parsed !== 'object' || Array.isArray(parsed)) {
61
+ throw new Error('Headers must be a JSON object');
62
+ }
63
+ return parsed;
64
+ }
65
+ catch (error) {
66
+ throw new Error(`Invalid JSON for wsHeaders: ${error.message}`);
67
+ }
68
+ },
69
+ },
26
70
  headless: {
27
71
  type: 'boolean',
28
72
  description: 'Whether to run in headless (no UI) mode.',
@@ -31,7 +75,7 @@ export const cliOptions = {
31
75
  executablePath: {
32
76
  type: 'string',
33
77
  description: 'Path to custom Chrome executable.',
34
- conflicts: 'browserUrl',
78
+ conflicts: ['browserUrl', 'wsEndpoint'],
35
79
  alias: 'e',
36
80
  },
37
81
  isolated: {
@@ -43,7 +87,7 @@ export const cliOptions = {
43
87
  type: 'string',
44
88
  description: 'Specify a different Chrome channel that should be used. The default is the stable channel version.',
45
89
  choices: ['stable', 'canary', 'beta', 'dev'],
46
- conflicts: ['browserUrl', 'executablePath'],
90
+ conflicts: ['browserUrl', 'wsEndpoint', 'executablePath'],
47
91
  },
48
92
  logFile: {
49
93
  type: 'string',
@@ -83,6 +127,21 @@ export const cliOptions = {
83
127
  type: 'array',
84
128
  describe: 'Additional arguments for Chrome. Only applies when Chrome is launched by chrome-devtools-mcp.',
85
129
  },
130
+ categoryEmulation: {
131
+ type: 'boolean',
132
+ default: true,
133
+ describe: 'Set to false to exlcude tools related to emulation.',
134
+ },
135
+ categoryPerformance: {
136
+ type: 'boolean',
137
+ default: true,
138
+ describe: 'Set to false to exlcude tools related to performance.',
139
+ },
140
+ categoryNetwork: {
141
+ type: 'boolean',
142
+ default: true,
143
+ describe: 'Set to false to exlcude tools related to network.',
144
+ },
86
145
  };
87
146
  export function parseArguments(version, argv = process.argv) {
88
147
  const yargsInstance = yargs(hideBin(argv))
@@ -91,7 +150,10 @@ export function parseArguments(version, argv = process.argv) {
91
150
  .check(args => {
92
151
  // We can't set default in the options else
93
152
  // Yargs will complain
94
- if (!args.channel && !args.browserUrl && !args.executablePath) {
153
+ if (!args.channel &&
154
+ !args.browserUrl &&
155
+ !args.wsEndpoint &&
156
+ !args.executablePath) {
95
157
  args.channel = 'stable';
96
158
  }
97
159
  return true;
@@ -99,7 +161,15 @@ export function parseArguments(version, argv = process.argv) {
99
161
  .example([
100
162
  [
101
163
  '$0 --browserUrl http://127.0.0.1:9222',
102
- 'Connect to an existing browser instance',
164
+ 'Connect to an existing browser instance via HTTP',
165
+ ],
166
+ [
167
+ '$0 --wsEndpoint ws://127.0.0.1:9222/devtools/browser/abc123',
168
+ 'Connect to an existing browser instance via WebSocket',
169
+ ],
170
+ [
171
+ `$0 --wsEndpoint ws://127.0.0.1:9222/devtools/browser/abc123 --wsHeaders '{"Authorization":"Bearer token"}'`,
172
+ 'Connect via WebSocket with custom headers',
103
173
  ],
104
174
  ['$0 --channel beta', 'Use Chrome Beta installed on this system'],
105
175
  ['$0 --channel canary', 'Use Chrome Canary installed on this system'],
@@ -115,6 +185,12 @@ export function parseArguments(version, argv = process.argv) {
115
185
  `$0 --chrome-arg='--no-sandbox' --chrome-arg='--disable-setuid-sandbox'`,
116
186
  'Launch Chrome without sandboxes. Use with caution.',
117
187
  ],
188
+ ['$0 --no-category-emulation', 'Disable tools in the emulation category'],
189
+ [
190
+ '$0 --no-category-performance',
191
+ 'Disable tools in the performance category',
192
+ ],
193
+ ['$0 --no-category-network', 'Disable tools in the network category'],
118
194
  ]);
119
195
  return yargsInstance
120
196
  .wrap(Math.min(120, yargsInstance.terminalWidth()))