chrome-devtools-mcp 1.1.1 → 1.2.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 (105) hide show
  1. package/README.md +12 -3
  2. package/build/src/DevtoolsUtils.js +57 -0
  3. package/build/src/HeapSnapshotManager.js +5 -0
  4. package/build/src/McpContext.js +28 -8
  5. package/build/src/McpPage.js +9 -9
  6. package/build/src/McpResponse.js +101 -53
  7. package/build/src/PageCollector.js +7 -7
  8. package/build/src/ServiceWorkerCollector.js +171 -0
  9. package/build/src/TextSnapshot.js +1 -1
  10. package/build/src/ToolHandler.js +10 -4
  11. package/build/src/WaitForHelper.js +2 -2
  12. package/build/src/bin/chrome-devtools-cli-options.js +22 -4
  13. package/build/src/bin/chrome-devtools-mcp-cli-options.js +19 -4
  14. package/build/src/bin/chrome-devtools-mcp-main.js +5 -5
  15. package/build/src/browser.js +8 -4
  16. package/build/src/daemon/client.js +7 -7
  17. package/build/src/daemon/daemon.js +12 -12
  18. package/build/src/daemon/utils.js +2 -2
  19. package/build/src/formatters/IssueFormatter.js +4 -4
  20. package/build/src/index.js +13 -1
  21. package/build/src/telemetry/ClearcutLogger.js +1 -1
  22. package/build/src/telemetry/WatchdogClient.js +4 -4
  23. package/build/src/telemetry/persistence.js +2 -2
  24. package/build/src/telemetry/watchdog/ClearcutSender.js +10 -10
  25. package/build/src/telemetry/watchdog/main.js +5 -5
  26. package/build/src/third_party/THIRD_PARTY_NOTICES +30 -0
  27. package/build/src/third_party/bundled-packages.json +2 -1
  28. package/build/src/third_party/devtools-formatter-worker.js +1 -0
  29. package/build/src/third_party/devtools-heap-snapshot-worker.js +107 -0
  30. package/build/src/third_party/index.js +5906 -4913
  31. package/build/src/third_party/issue-descriptions/emailVerificationRequestAccountsEmptyList.md +1 -0
  32. package/build/src/third_party/issue-descriptions/emailVerificationRequestAccountsHttpNotFound.md +1 -0
  33. package/build/src/third_party/issue-descriptions/emailVerificationRequestAccountsInvalidContentType.md +1 -0
  34. package/build/src/third_party/issue-descriptions/emailVerificationRequestAccountsInvalidResponse.md +1 -0
  35. package/build/src/third_party/issue-descriptions/emailVerificationRequestAccountsNoResponse.md +1 -0
  36. package/build/src/third_party/issue-descriptions/emailVerificationRequestDnsFetchFailed.md +1 -0
  37. package/build/src/third_party/issue-descriptions/emailVerificationRequestDnsInvalidRecord.md +1 -0
  38. package/build/src/third_party/issue-descriptions/emailVerificationRequestEmailVerificationWellKnownHttpNotFound.md +1 -0
  39. package/build/src/third_party/issue-descriptions/emailVerificationRequestEmailVerificationWellKnownInvalidContentType.md +1 -0
  40. package/build/src/third_party/issue-descriptions/emailVerificationRequestEmailVerificationWellKnownInvalidResponse.md +1 -0
  41. package/build/src/third_party/issue-descriptions/emailVerificationRequestEmailVerificationWellKnownNoResponse.md +1 -0
  42. package/build/src/third_party/issue-descriptions/emailVerificationRequestInvalidEmail.md +1 -0
  43. package/build/src/third_party/issue-descriptions/emailVerificationRequestJwksHttpNotFound.md +1 -0
  44. package/build/src/third_party/issue-descriptions/emailVerificationRequestJwksInvalidResponse.md +1 -0
  45. package/build/src/third_party/issue-descriptions/emailVerificationRequestKeyBindingSigningFailed.md +1 -0
  46. package/build/src/third_party/issue-descriptions/emailVerificationRequestRpOriginIsOpaque.md +1 -0
  47. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenHttpNotFound.md +1 -0
  48. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenInvalidContentType.md +1 -0
  49. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenInvalidResponse.md +1 -0
  50. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenInvalidSdJwt.md +1 -0
  51. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenMalformedSdJwt.md +1 -0
  52. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenNoResponse.md +1 -0
  53. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbInvalidAudience.md +1 -0
  54. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbInvalidIssuedAt.md +1 -0
  55. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbInvalidNonce.md +1 -0
  56. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbInvalidSdHash.md +1 -0
  57. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbInvalidTyp.md +1 -0
  58. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbMissingAud.md +1 -0
  59. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbMissingCnf.md +1 -0
  60. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbMissingIat.md +1 -0
  61. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbMissingNonce.md +1 -0
  62. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbMissingSdHash.md +1 -0
  63. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbSignatureFailed.md +1 -0
  64. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtInvalidEmail.md +1 -0
  65. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtInvalidEmailVerified.md +1 -0
  66. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtInvalidHolderKey.md +1 -0
  67. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtInvalidIssuedAt.md +1 -0
  68. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtInvalidIssuer.md +1 -0
  69. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtJwksMissingKeys.md +1 -0
  70. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtMissingCnf.md +1 -0
  71. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtMissingEmail.md +1 -0
  72. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtMissingIat.md +1 -0
  73. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtMissingIss.md +1 -0
  74. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtSignatureFailed.md +1 -0
  75. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtUnsupportedHeaderAlg.md +1 -0
  76. package/build/src/third_party/issue-descriptions/emailVerificationRequestUserLoggedOut.md +1 -0
  77. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownAccountsEndpointCrossOrigin.md +1 -0
  78. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownHttpNotFound.md +1 -0
  79. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownInvalidContentType.md +1 -0
  80. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownInvalidResponse.md +1 -0
  81. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownIssuanceEndpointCrossOrigin.md +1 -0
  82. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownListEmpty.md +1 -0
  83. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownMissingAccountsEndpoint.md +1 -0
  84. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownMissingIssuanceEndpoint.md +1 -0
  85. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownNoResponse.md +1 -0
  86. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownUnsupportedSigningAlgorithm.md +1 -0
  87. package/build/src/tools/console.js +7 -0
  88. package/build/src/tools/emulation.js +1 -0
  89. package/build/src/tools/extensions.js +5 -2
  90. package/build/src/tools/input.js +11 -3
  91. package/build/src/tools/lighthouse.js +11 -6
  92. package/build/src/tools/memory.js +33 -10
  93. package/build/src/tools/network.js +2 -2
  94. package/build/src/tools/pages.js +13 -5
  95. package/build/src/tools/performance.js +8 -7
  96. package/build/src/tools/screencast.js +2 -1
  97. package/build/src/tools/screenshot.js +1 -1
  98. package/build/src/tools/script.js +1 -1
  99. package/build/src/tools/slim/tools.js +3 -0
  100. package/build/src/tools/snapshot.js +3 -2
  101. package/build/src/tools/thirdPartyDeveloper.js +12 -2
  102. package/build/src/tools/webmcp.js +2 -0
  103. package/build/src/trace-processing/parse.js +5 -5
  104. package/build/src/version.js +1 -1
  105. package/package.json +4 -3
@@ -0,0 +1,171 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { UncaughtError } from './PageCollector.js';
7
+ import { createIdGenerator, stableIdSymbol } from './utils/id.js';
8
+ const CHROME_EXTENSION_PREFIX = 'chrome-extension://';
9
+ export class ServiceWorkerSubscriber {
10
+ #target;
11
+ #callback;
12
+ #session;
13
+ #worker;
14
+ constructor(target, callback) {
15
+ this.#target = target;
16
+ this.#callback = callback;
17
+ }
18
+ async subscribe() {
19
+ this.#session = await this.#target.createCDPSession();
20
+ await this.#session.send('Runtime.enable');
21
+ this.#session.on('Runtime.exceptionThrown', this.#onExceptionThrown);
22
+ this.#worker = (await this.#target.worker()) ?? undefined;
23
+ if (this.#worker) {
24
+ this.#worker.on('console', this.#onConsole);
25
+ }
26
+ }
27
+ async unsubscribe() {
28
+ if (this.#worker) {
29
+ this.#worker.off('console', this.#onConsole);
30
+ }
31
+ await this.#session?.detach();
32
+ }
33
+ #onConsole = (message) => {
34
+ this.#callback(message);
35
+ };
36
+ #onExceptionThrown = (event) => {
37
+ const url = this.#target.url();
38
+ const extensionId = extractExtensionId(url);
39
+ if (extensionId) {
40
+ this.#callback(new UncaughtError(event.exceptionDetails, extensionId));
41
+ }
42
+ };
43
+ }
44
+ export class ServiceWorkerConsoleCollector {
45
+ #storage = new Map();
46
+ #maxLogs;
47
+ #browser;
48
+ #serviceWorkerSubscribers = new Map();
49
+ #idGenerator = createIdGenerator();
50
+ constructor(browser, maxLogs = 1000) {
51
+ this.#browser = browser;
52
+ this.#maxLogs = maxLogs;
53
+ }
54
+ async init(workers) {
55
+ if (!this.#browser) {
56
+ return;
57
+ }
58
+ this.#browser.on('targetcreated', this.#onTargetCreated);
59
+ this.#browser.on('targetdestroyed', this.#onTargetDestroyed);
60
+ for (const worker of workers) {
61
+ void this.#onTargetCreated(worker.target);
62
+ }
63
+ }
64
+ dispose() {
65
+ if (!this.#browser) {
66
+ return;
67
+ }
68
+ this.#browser.off('targetcreated', this.#onTargetCreated);
69
+ this.#browser.off('targetdestroyed', this.#onTargetDestroyed);
70
+ for (const subscriber of this.#serviceWorkerSubscribers.values()) {
71
+ subscriber.unsubscribe().catch(err => {
72
+ if (err instanceof Error &&
73
+ !err.message.includes('Target closed') &&
74
+ !err.message.includes('Session closed')) {
75
+ // Swallow error as we are tearing down the system
76
+ }
77
+ });
78
+ }
79
+ this.#serviceWorkerSubscribers.clear();
80
+ }
81
+ #onTargetCreated = async (target) => {
82
+ if (this.#serviceWorkerSubscribers.has(target)) {
83
+ return;
84
+ }
85
+ const origin = target.url();
86
+ if (target.type() === 'service_worker' && isExtensionOrigin(origin)) {
87
+ const extensionId = extractExtensionId(origin);
88
+ if (!extensionId) {
89
+ return;
90
+ }
91
+ const subscriber = new ServiceWorkerSubscriber(target, item => {
92
+ this.addLog(extensionId, item);
93
+ });
94
+ try {
95
+ await subscriber.subscribe();
96
+ }
97
+ catch (err) {
98
+ if (err instanceof Error &&
99
+ !err.message.includes('Target closed') &&
100
+ !err.message.includes('Session closed')) {
101
+ throw err;
102
+ }
103
+ }
104
+ this.#serviceWorkerSubscribers.set(target, subscriber);
105
+ }
106
+ };
107
+ #onTargetDestroyed = async (target) => {
108
+ const subscriber = this.#serviceWorkerSubscribers.get(target);
109
+ if (subscriber) {
110
+ try {
111
+ await subscriber.unsubscribe();
112
+ }
113
+ catch (err) {
114
+ if (err instanceof Error &&
115
+ !err.message.includes('Target closed') &&
116
+ !err.message.includes('Session closed')) {
117
+ throw err;
118
+ }
119
+ }
120
+ this.#serviceWorkerSubscribers.delete(target);
121
+ }
122
+ };
123
+ addLog(extensionId, log) {
124
+ const logs = this.#storage.get(extensionId) ?? [];
125
+ const withId = log;
126
+ withId[stableIdSymbol] = this.#idGenerator();
127
+ logs.push(withId);
128
+ if (logs.length > this.#maxLogs) {
129
+ logs.shift();
130
+ }
131
+ this.#storage.set(extensionId, logs);
132
+ }
133
+ getData(extensionId) {
134
+ return this.#storage.get(extensionId) ?? [];
135
+ }
136
+ getById(extensionId, stableId) {
137
+ const logs = this.#storage.get(extensionId);
138
+ if (!logs) {
139
+ throw new Error('No logs found for selected extension');
140
+ }
141
+ const item = logs.find(item => item[stableIdSymbol] === stableId);
142
+ if (item) {
143
+ return item;
144
+ }
145
+ throw new Error('Log not found for selected extension');
146
+ }
147
+ find(extensionId, filter) {
148
+ const logs = this.#storage.get(extensionId);
149
+ if (!logs) {
150
+ return;
151
+ }
152
+ return logs.find(filter);
153
+ }
154
+ clearLogs(extensionId) {
155
+ this.#storage.delete(extensionId);
156
+ }
157
+ }
158
+ function extractExtensionId(origin) {
159
+ if (!origin || !isExtensionOrigin(origin)) {
160
+ return null;
161
+ }
162
+ const pathPart = origin.substring(CHROME_EXTENSION_PREFIX.length);
163
+ const slashIndex = pathPart.indexOf('/');
164
+ // if there's no / it means that pathPart is now the extensionId, otherwise
165
+ // we take everything until the first /
166
+ return slashIndex === -1 ? pathPart : pathPart.substring(0, slashIndex);
167
+ }
168
+ function isExtensionOrigin(origin) {
169
+ return origin.startsWith(CHROME_EXTENSION_PREFIX);
170
+ }
171
+ //# sourceMappingURL=ServiceWorkerCollector.js.map
@@ -186,7 +186,7 @@ export class TextSnapshot {
186
186
  }
187
187
  }
188
188
  catch (e) {
189
- logger(`Failed to collect descendants for backend node ${backendNodeId}`, e);
189
+ logger?.(`Failed to collect descendants for backend node ${backendNodeId}`, e);
190
190
  }
191
191
  return descendantIds;
192
192
  };
@@ -139,15 +139,21 @@ export class ToolHandler {
139
139
  const startTime = Date.now();
140
140
  let success = false;
141
141
  try {
142
- logger(`${this.tool.name} request: ${JSON.stringify(params, null, ' ')}`);
142
+ logger?.(`${this.tool.name} request: ${JSON.stringify(params, null, ' ')}`);
143
143
  const context = await this.getContext();
144
- logger(`${this.tool.name} context: resolved`);
144
+ logger?.(`${this.tool.name} context: resolved`);
145
145
  await context.detectOpenDevToolsWindows();
146
146
  const response = this.serverArgs.slim
147
147
  ? new SlimMcpResponse(this.serverArgs)
148
148
  : new McpResponse(this.serverArgs);
149
149
  response.setRedactNetworkHeaders(this.serverArgs.redactNetworkHeaders);
150
150
  try {
151
+ if (this.tool.verifyFilesSchema) {
152
+ for (const key of this.tool.verifyFilesSchema) {
153
+ const filePath = params[key];
154
+ await context.validatePath(filePath);
155
+ }
156
+ }
151
157
  if (isPageScopedTool(this.tool)) {
152
158
  const pageId = typeof params.pageId === 'number' ? params.pageId : undefined;
153
159
  const page = this.serverArgs.experimentalPageIdRouting &&
@@ -173,7 +179,7 @@ export class ToolHandler {
173
179
  catch (err) {
174
180
  response.setError(err);
175
181
  }
176
- const { content, structuredContent } = await response.handle(this.tool.name, context);
182
+ const { content, structuredContent } = await response.handle(this.tool.name, context, this.serverArgs.experimentalToonFormat ?? false);
177
183
  const result = {
178
184
  content,
179
185
  };
@@ -187,7 +193,7 @@ export class ToolHandler {
187
193
  return result;
188
194
  }
189
195
  catch (err) {
190
- logger(`${this.tool.name} error:`, err, err?.stack);
196
+ logger?.(`${this.tool.name} error:`, err, err?.stack);
191
197
  let errorText = err && 'message' in err ? err.message : String(err);
192
198
  if ('cause' in err && err.cause) {
193
199
  errorText += `\nCause: ${err.cause.message}`;
@@ -138,7 +138,7 @@ export class WaitForHelper {
138
138
  }
139
139
  return;
140
140
  })
141
- .catch(error => logger(error));
141
+ .catch(error => logger?.(error));
142
142
  try {
143
143
  await action();
144
144
  }
@@ -157,7 +157,7 @@ export class WaitForHelper {
157
157
  await this.waitForStableDom();
158
158
  }
159
159
  catch (error) {
160
- logger(error);
160
+ logger?.(error);
161
161
  }
162
162
  finally {
163
163
  this.#abortController.abort();
@@ -58,6 +58,18 @@ export const commands = {
58
58
  },
59
59
  },
60
60
  },
61
+ close_heapsnapshot: {
62
+ description: 'Closes a previously loaded memory heapsnapshot, freeing its memory. (requires flag: --memoryDebugging=true)',
63
+ category: 'Memory',
64
+ args: {
65
+ filePath: {
66
+ name: 'filePath',
67
+ type: 'string',
68
+ description: 'A path to the .heapsnapshot file to close.',
69
+ required: true,
70
+ },
71
+ },
72
+ },
61
73
  close_page: {
62
74
  description: 'Closes the page by its index. The last open page cannot be closed.',
63
75
  category: 'Navigation automation',
@@ -247,7 +259,7 @@ export const commands = {
247
259
  },
248
260
  },
249
261
  get_heapsnapshot_class_nodes: {
250
- description: 'Loads a memory heapsnapshot and returns instances of a specific class with their IDs. (requires flag: --experimentalMemory=true)',
262
+ description: 'Loads a memory heapsnapshot and returns instances of a specific class with their IDs. (requires flag: --memoryDebugging=true)',
251
263
  category: 'Memory',
252
264
  args: {
253
265
  filePath: {
@@ -277,7 +289,7 @@ export const commands = {
277
289
  },
278
290
  },
279
291
  get_heapsnapshot_details: {
280
- description: 'Loads a memory heapsnapshot and returns all available information including statistics, static data, and aggregated node information. Supports pagination for aggregates. (requires flag: --experimentalMemory=true)',
292
+ description: 'Loads a memory heapsnapshot and returns all available information including statistics, static data, and aggregated node information. Supports pagination for aggregates. (requires flag: --memoryDebugging=true)',
281
293
  category: 'Memory',
282
294
  args: {
283
295
  filePath: {
@@ -301,7 +313,7 @@ export const commands = {
301
313
  },
302
314
  },
303
315
  get_heapsnapshot_retainers: {
304
- description: 'Loads a memory heapsnapshot and returns retainers for a specific node ID. (requires flag: --experimentalMemory=true)',
316
+ description: 'Loads a memory heapsnapshot and returns retainers for a specific node ID. (requires flag: --memoryDebugging=true)',
305
317
  category: 'Memory',
306
318
  args: {
307
319
  filePath: {
@@ -331,7 +343,7 @@ export const commands = {
331
343
  },
332
344
  },
333
345
  get_heapsnapshot_summary: {
334
- description: 'Loads a memory heapsnapshot and returns snapshot summary stats. (requires flag: --experimentalMemory=true)',
346
+ description: 'Loads a memory heapsnapshot and returns snapshot summary stats. (requires flag: --memoryDebugging=true)',
335
347
  category: 'Memory',
336
348
  args: {
337
349
  filePath: {
@@ -477,6 +489,12 @@ export const commands = {
477
489
  required: false,
478
490
  default: false,
479
491
  },
492
+ serviceWorkerId: {
493
+ name: 'serviceWorkerId',
494
+ type: 'string',
495
+ description: 'Filter messages to only return messages of the specified service worker.',
496
+ required: false,
497
+ },
480
498
  },
481
499
  },
482
500
  list_extensions: {
@@ -146,14 +146,20 @@ export const cliOptions = {
146
146
  type: 'boolean',
147
147
  describe: 'Whether to enable coordinate-based tools such as click_at(x,y). Usually requires a computer-use model able to produce accurate coordinates by looking at screenshots.',
148
148
  },
149
- experimentalMemory: {
149
+ memoryDebugging: {
150
150
  type: 'boolean',
151
- describe: 'Whether to enable experimental memory tools.',
151
+ describe: 'Whether to enable memory debugging tools.',
152
+ alias: 'experimentalMemory',
152
153
  },
153
154
  experimentalStructuredContent: {
154
155
  type: 'boolean',
155
156
  describe: 'Whether to output structured formatted content.',
156
157
  },
158
+ experimentalToonFormat: {
159
+ type: 'boolean',
160
+ describe: 'Whether to format structured data in text response using Token-Oriented Object Notation. Defaults to false which represents the embedded content as formatted JSON instead.',
161
+ hidden: true,
162
+ },
157
163
  experimentalIncludeAllPages: {
158
164
  type: 'boolean',
159
165
  describe: 'Whether to include all kinds of pages such as webviews or background pages as pages.',
@@ -185,6 +191,16 @@ export const cliOptions = {
185
191
  type: 'array',
186
192
  describe: 'Additional arguments for Chrome. Only applies when Chrome is launched by chrome-devtools-mcp.',
187
193
  },
194
+ blockedUrlPattern: {
195
+ type: 'array',
196
+ describe: 'Restricts network access by blocking specified URL patterns (uses https://urlpattern.spec.whatwg.org/). Silently detaches from targets with blocked URLs upon connection, and blocks runtime requests (including navigations and subresources). Accepts an array of patterns.',
197
+ conflicts: ['allowedUrlPattern'],
198
+ },
199
+ allowedUrlPattern: {
200
+ type: 'array',
201
+ describe: 'Restricts network access by allowing only specified URL patterns (uses https://urlpattern.spec.whatwg.org/). Requires Chrome 149+. Silently detaches from targets with unallowed URLs upon connection, and blocks runtime requests (including navigations and subresources). Accepts an array of patterns.',
202
+ conflicts: ['blockedUrlPattern'],
203
+ },
188
204
  ignoreDefaultChromeArg: {
189
205
  type: 'array',
190
206
  describe: 'Explicitly disable default arguments for Chrome. Only applies when Chrome is launched by chrome-devtools-mcp.',
@@ -259,7 +275,7 @@ export function parseArguments(version, argv = process.argv, env = process.env)
259
275
  const yargsInstance = yargs(hideBin(argv))
260
276
  .scriptName('npx chrome-devtools-mcp@latest')
261
277
  .options(cliOptions)
262
- .check(args => {
278
+ .middleware(args => {
263
279
  // We can't set default in the options else
264
280
  // Yargs will complain
265
281
  if (!args.channel &&
@@ -272,7 +288,6 @@ export function parseArguments(version, argv = process.argv, env = process.env)
272
288
  console.error("turning off usage statistics. process.env['CI'] || process.env['CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS'] is set.");
273
289
  args.usageStatistics = false;
274
290
  }
275
- return true;
276
291
  })
277
292
  .example([
278
293
  [
@@ -19,7 +19,7 @@ export const args = parseArguments(VERSION);
19
19
  const logFile = args.logFile ? saveLogsToFile(args.logFile) : undefined;
20
20
  if (process.env['CHROME_DEVTOOLS_MCP_CRASH_ON_UNCAUGHT'] !== 'true') {
21
21
  process.on('unhandledRejection', (reason, promise) => {
22
- logger('Unhandled promise rejection', promise, reason);
22
+ logger?.('Unhandled promise rejection', promise, reason);
23
23
  });
24
24
  }
25
25
  // Shutdown on stdin EOF (stdio MCP convention — the client closes the
@@ -32,13 +32,13 @@ async function shutdown(reason) {
32
32
  return;
33
33
  }
34
34
  shuttingDown = true;
35
- logger(`Shutting down (${reason})`);
35
+ logger?.(`Shutting down (${reason})`);
36
36
  // Backstop in case browser teardown hangs (e.g. unresponsive Chrome,
37
37
  // slow beforeunload handlers, many tabs). Exits 0 because we still
38
38
  // honored the shutdown request; the log line preserves observability.
39
39
  // Unref'd so it doesn't keep the loop alive on the clean path.
40
40
  setTimeout(() => {
41
- logger('Shutdown timeout exceeded, forcing exit');
41
+ logger?.('Shutdown timeout exceeded, forcing exit');
42
42
  process.exit(0);
43
43
  }, 10000).unref();
44
44
  await closeBrowser();
@@ -59,13 +59,13 @@ process.on('SIGINT', () => {
59
59
  process.on('SIGHUP', () => {
60
60
  void shutdown('SIGHUP');
61
61
  });
62
- logger(`Starting Chrome DevTools MCP Server v${VERSION}`);
62
+ logger?.(`Starting Chrome DevTools MCP Server v${VERSION}`);
63
63
  const { server } = await createMcpServer(args, {
64
64
  logFile,
65
65
  });
66
66
  const transport = new StdioServerTransport();
67
67
  await server.connect(transport);
68
- logger('Chrome DevTools MCP Server connected');
68
+ logger?.('Chrome DevTools MCP Server connected');
69
69
  logDisclaimers(args);
70
70
  void ClearcutLogger.get()?.logDailyActiveIfNeeded();
71
71
  void ClearcutLogger.get()?.logServerStart(computeFlagUsage(args, cliOptions));
@@ -41,6 +41,8 @@ export async function ensureBrowserConnected(options) {
41
41
  targetFilter: makeTargetFilter(enableExtensions),
42
42
  defaultViewport: null,
43
43
  handleDevToolsAsPage: true,
44
+ blocklist: options.blocklist,
45
+ allowlist: options.allowlist,
44
46
  };
45
47
  let autoConnect = false;
46
48
  if (options.wsEndpoint) {
@@ -94,7 +96,7 @@ export async function ensureBrowserConnected(options) {
94
96
  else {
95
97
  throw new Error('Either browserURL, wsEndpoint, channel or userDataDir must be provided');
96
98
  }
97
- logger('Connecting Puppeteer to ', JSON.stringify(connectOptions));
99
+ logger?.('Connecting Puppeteer to ', JSON.stringify(connectOptions));
98
100
  try {
99
101
  // Assign mode before browser so a concurrent closeBrowser() never sees
100
102
  // `browser` set with `browserMode` still undefined (would fall through
@@ -108,7 +110,7 @@ export async function ensureBrowserConnected(options) {
108
110
  cause: err,
109
111
  });
110
112
  }
111
- logger('Connected Puppeteer');
113
+ logger?.('Connected Puppeteer');
112
114
  return browser;
113
115
  }
114
116
  export function detectDisplay() {
@@ -174,6 +176,8 @@ export async function launch(options) {
174
176
  acceptInsecureCerts: options.acceptInsecureCerts,
175
177
  handleDevToolsAsPage: true,
176
178
  enableExtensions: options.enableExtensions,
179
+ blocklist: options.blocklist,
180
+ allowlist: options.allowlist,
177
181
  });
178
182
  if (options.logFile) {
179
183
  // FIXME: we are probably subscribing too late to catch startup logs. We
@@ -227,12 +231,12 @@ export async function closeBrowser() {
227
231
  }
228
232
  if (mode === 'launched') {
229
233
  await b.close().catch(err => {
230
- logger('Failed to close browser', err);
234
+ logger?.('Failed to close browser', err);
231
235
  });
232
236
  return;
233
237
  }
234
238
  await b.disconnect().catch(err => {
235
- logger('Failed to disconnect from browser', err);
239
+ logger?.('Failed to disconnect from browser', err);
236
240
  });
237
241
  }
238
242
  //# sourceMappingURL=browser.js.map
@@ -50,14 +50,14 @@ function waitForFile(filePath, removed = false) {
50
50
  }
51
51
  export async function startDaemon(mcpArgs = [], sessionId) {
52
52
  if (isDaemonRunning(sessionId)) {
53
- logger('Daemon is already running');
53
+ logger?.('Daemon is already running');
54
54
  return;
55
55
  }
56
56
  const pidFilePath = getPidFilePath(sessionId);
57
57
  if (fs.existsSync(pidFilePath)) {
58
58
  fs.unlinkSync(pidFilePath);
59
59
  }
60
- logger('Starting daemon...', ...mcpArgs);
60
+ logger?.('Starting daemon...', ...mcpArgs);
61
61
  const child = spawn(process.execPath, [DAEMON_SCRIPT_PATH, ...mcpArgs], {
62
62
  detached: true,
63
63
  stdio: 'ignore',
@@ -85,26 +85,26 @@ export async function sendCommand(command, sessionId) {
85
85
  const transport = new PipeTransport(socket, socket);
86
86
  transport.onmessage = async (message) => {
87
87
  clearTimeout(timer);
88
- logger('onmessage', message);
88
+ logger?.('onmessage', message);
89
89
  resolve(JSON.parse(message));
90
90
  };
91
91
  socket.on('error', error => {
92
92
  clearTimeout(timer);
93
- logger('Socket error:', error);
93
+ logger?.('Socket error:', error);
94
94
  reject(error);
95
95
  });
96
96
  socket.on('close', () => {
97
97
  clearTimeout(timer);
98
- logger('Socket closed:');
98
+ logger?.('Socket closed:');
99
99
  reject(new Error('Socket closed'));
100
100
  });
101
- logger('Sending message', command);
101
+ logger?.('Sending message', command);
102
102
  transport.send(JSON.stringify(command));
103
103
  });
104
104
  }
105
105
  export async function stopDaemon(sessionId) {
106
106
  if (!isDaemonRunning(sessionId)) {
107
- logger('Daemon is not running');
107
+ logger?.('Daemon is not running');
108
108
  return;
109
109
  }
110
110
  const pidFilePath = getPidFilePath(sessionId);
@@ -14,9 +14,9 @@ import { Client, PipeTransport, StdioClientTransport, } from '../third_party/ind
14
14
  import { VERSION } from '../version.js';
15
15
  import { DAEMON_CLIENT_NAME, getPidFilePath, getSocketPath, INDEX_SCRIPT_PATH, IS_WINDOWS, isDaemonRunning, } from './utils.js';
16
16
  const sessionId = process.env.CHROME_DEVTOOLS_MCP_SESSION_ID || '';
17
- logger(`Daemon sessionId: ${sessionId}`);
17
+ logger?.(`Daemon sessionId: ${sessionId}`);
18
18
  if (isDaemonRunning(sessionId)) {
19
- logger('Another daemon process is running.');
19
+ logger?.('Another daemon process is running.');
20
20
  process.exit(1);
21
21
  }
22
22
  const pidFilePath = getPidFilePath(sessionId);
@@ -80,7 +80,7 @@ finally {
80
80
  }
81
81
  }
82
82
  }
83
- logger(`Writing ${process.pid.toString()} to ${pidFilePath}`);
83
+ logger?.(`Writing ${process.pid.toString()} to ${pidFilePath}`);
84
84
  const socketPath = getSocketPath(sessionId);
85
85
  const startDate = new Date();
86
86
  const mcpServerArgs = process.argv.slice(2);
@@ -173,13 +173,13 @@ async function startSocketServer() {
173
173
  server = createServer(socket => {
174
174
  const transport = new PipeTransport(socket, socket);
175
175
  transport.onmessage = async (message) => {
176
- logger('onmessage', message);
176
+ logger?.('onmessage', message);
177
177
  const response = await handleRequest(JSON.parse(message));
178
178
  transport.send(JSON.stringify(response));
179
179
  socket.end();
180
180
  };
181
181
  socket.on('error', error => {
182
- logger('Socket error:', error);
182
+ logger?.('Socket error:', error);
183
183
  });
184
184
  });
185
185
  server.listen({
@@ -198,7 +198,7 @@ async function startSocketServer() {
198
198
  }
199
199
  });
200
200
  server.on('error', error => {
201
- logger('Server error:', error);
201
+ logger?.('Server error:', error);
202
202
  reject(error);
203
203
  });
204
204
  });
@@ -209,13 +209,13 @@ async function cleanup() {
209
209
  await mcpClient?.close();
210
210
  }
211
211
  catch (error) {
212
- logger('Error closing MCP client:', error);
212
+ logger?.('Error closing MCP client:', error);
213
213
  }
214
214
  try {
215
215
  await mcpTransport?.close();
216
216
  }
217
217
  catch (error) {
218
- logger('Error closing MCP transport:', error);
218
+ logger?.('Error closing MCP transport:', error);
219
219
  }
220
220
  if (server) {
221
221
  await new Promise(resolve => {
@@ -230,7 +230,7 @@ async function cleanup() {
230
230
  // ignore errors
231
231
  }
232
232
  }
233
- logger(`unlinking ${pidFilePath}`);
233
+ logger?.(`unlinking ${pidFilePath}`);
234
234
  if (fs.existsSync(pidFilePath)) {
235
235
  fs.unlinkSync(pidFilePath);
236
236
  }
@@ -248,14 +248,14 @@ process.on('SIGHUP', () => {
248
248
  });
249
249
  // Handle uncaught errors
250
250
  process.on('uncaughtException', error => {
251
- logger('Uncaught exception:', error);
251
+ logger?.('Uncaught exception:', error);
252
252
  });
253
253
  process.on('unhandledRejection', error => {
254
- logger('Unhandled rejection:', error);
254
+ logger?.('Unhandled rejection:', error);
255
255
  });
256
256
  // Start the server
257
257
  const started = startSocketServer().catch(error => {
258
- logger('Failed to start daemon server:', error);
258
+ logger?.('Failed to start daemon server:', error);
259
259
  process.exit(1);
260
260
  });
261
261
  //# sourceMappingURL=daemon.js.map
@@ -56,13 +56,13 @@ export function getPidFilePath(sessionId) {
56
56
  export function getDaemonPid(sessionId) {
57
57
  try {
58
58
  const pidFile = getPidFilePath(sessionId);
59
- logger(`Daemon pid file ${pidFile} sessionId=${sessionId}`);
59
+ logger?.(`Daemon pid file ${pidFile} sessionId=${sessionId}`);
60
60
  if (!fs.existsSync(pidFile)) {
61
61
  return null;
62
62
  }
63
63
  const pidContent = fs.readFileSync(pidFile, 'utf-8');
64
64
  const pid = parseInt(pidContent.trim(), 10);
65
- logger(`Daemon pid: ${pid}`);
65
+ logger?.(`Daemon pid: ${pid}`);
66
66
  if (isNaN(pid)) {
67
67
  return null;
68
68
  }
@@ -99,7 +99,7 @@ export class IssueFormatter {
99
99
  const markdownDescription = this.#issue.getDescription();
100
100
  const filename = markdownDescription?.file;
101
101
  if (!filename) {
102
- logger(`no description found for issue:` + this.#issue.code());
102
+ logger?.(`no description found for issue:` + this.#issue.code());
103
103
  return undefined;
104
104
  }
105
105
  // We already have the description logic in #getDescription, but title extraction is separate
@@ -107,7 +107,7 @@ export class IssueFormatter {
107
107
  // Ideally we should process markdown once.
108
108
  const rawMarkdown = ISSUE_UTILS.getIssueDescription(filename);
109
109
  if (!rawMarkdown) {
110
- logger(`no markdown ${filename} found for issue:` + this.#issue.code());
110
+ logger?.(`no markdown ${filename} found for issue:` + this.#issue.code());
111
111
  return undefined;
112
112
  }
113
113
  try {
@@ -115,13 +115,13 @@ export class IssueFormatter {
115
115
  const markdownAst = DevTools.Marked.Marked.lexer(processedMarkdown);
116
116
  const title = DevTools.MarkdownIssueDescription.findTitleFromMarkdownAst(markdownAst);
117
117
  if (!title) {
118
- logger('cannot read issue title from ' + filename);
118
+ logger?.('cannot read issue title from ' + filename);
119
119
  return undefined;
120
120
  }
121
121
  return title;
122
122
  }
123
123
  catch {
124
- logger('error parsing markdown for issue ' + this.#issue.code());
124
+ logger?.('error parsing markdown for issue ' + this.#issue.code());
125
125
  return undefined;
126
126
  }
127
127
  }