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.
- package/README.md +12 -3
- package/build/src/DevtoolsUtils.js +57 -0
- package/build/src/HeapSnapshotManager.js +5 -0
- package/build/src/McpContext.js +28 -8
- package/build/src/McpPage.js +9 -9
- package/build/src/McpResponse.js +101 -53
- package/build/src/PageCollector.js +7 -7
- package/build/src/ServiceWorkerCollector.js +171 -0
- package/build/src/TextSnapshot.js +1 -1
- package/build/src/ToolHandler.js +10 -4
- package/build/src/WaitForHelper.js +2 -2
- package/build/src/bin/chrome-devtools-cli-options.js +22 -4
- package/build/src/bin/chrome-devtools-mcp-cli-options.js +19 -4
- package/build/src/bin/chrome-devtools-mcp-main.js +5 -5
- package/build/src/browser.js +8 -4
- package/build/src/daemon/client.js +7 -7
- package/build/src/daemon/daemon.js +12 -12
- package/build/src/daemon/utils.js +2 -2
- package/build/src/formatters/IssueFormatter.js +4 -4
- package/build/src/index.js +13 -1
- package/build/src/telemetry/ClearcutLogger.js +1 -1
- package/build/src/telemetry/WatchdogClient.js +4 -4
- package/build/src/telemetry/persistence.js +2 -2
- package/build/src/telemetry/watchdog/ClearcutSender.js +10 -10
- package/build/src/telemetry/watchdog/main.js +5 -5
- package/build/src/third_party/THIRD_PARTY_NOTICES +30 -0
- package/build/src/third_party/bundled-packages.json +2 -1
- package/build/src/third_party/devtools-formatter-worker.js +1 -0
- package/build/src/third_party/devtools-heap-snapshot-worker.js +107 -0
- package/build/src/third_party/index.js +5906 -4913
- package/build/src/third_party/issue-descriptions/emailVerificationRequestAccountsEmptyList.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestAccountsHttpNotFound.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestAccountsInvalidContentType.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestAccountsInvalidResponse.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestAccountsNoResponse.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestDnsFetchFailed.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestDnsInvalidRecord.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestEmailVerificationWellKnownHttpNotFound.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestEmailVerificationWellKnownInvalidContentType.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestEmailVerificationWellKnownInvalidResponse.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestEmailVerificationWellKnownNoResponse.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestInvalidEmail.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestJwksHttpNotFound.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestJwksInvalidResponse.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestKeyBindingSigningFailed.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestRpOriginIsOpaque.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenHttpNotFound.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenInvalidContentType.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenInvalidResponse.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenInvalidSdJwt.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenMalformedSdJwt.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenNoResponse.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbInvalidAudience.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbInvalidIssuedAt.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbInvalidNonce.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbInvalidSdHash.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbInvalidTyp.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbMissingAud.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbMissingCnf.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbMissingIat.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbMissingNonce.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbMissingSdHash.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbSignatureFailed.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtInvalidEmail.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtInvalidEmailVerified.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtInvalidHolderKey.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtInvalidIssuedAt.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtInvalidIssuer.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtJwksMissingKeys.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtMissingCnf.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtMissingEmail.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtMissingIat.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtMissingIss.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtSignatureFailed.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtUnsupportedHeaderAlg.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestUserLoggedOut.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownAccountsEndpointCrossOrigin.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownHttpNotFound.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownInvalidContentType.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownInvalidResponse.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownIssuanceEndpointCrossOrigin.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownListEmpty.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownMissingAccountsEndpoint.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownMissingIssuanceEndpoint.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownNoResponse.md +1 -0
- package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownUnsupportedSigningAlgorithm.md +1 -0
- package/build/src/tools/console.js +7 -0
- package/build/src/tools/emulation.js +1 -0
- package/build/src/tools/extensions.js +5 -2
- package/build/src/tools/input.js +11 -3
- package/build/src/tools/lighthouse.js +11 -6
- package/build/src/tools/memory.js +33 -10
- package/build/src/tools/network.js +2 -2
- package/build/src/tools/pages.js +13 -5
- package/build/src/tools/performance.js +8 -7
- package/build/src/tools/screencast.js +2 -1
- package/build/src/tools/screenshot.js +1 -1
- package/build/src/tools/script.js +1 -1
- package/build/src/tools/slim/tools.js +3 -0
- package/build/src/tools/snapshot.js +3 -2
- package/build/src/tools/thirdPartyDeveloper.js +12 -2
- package/build/src/tools/webmcp.js +2 -0
- package/build/src/trace-processing/parse.js +5 -5
- package/build/src/version.js +1 -1
- 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
|
};
|
package/build/src/ToolHandler.js
CHANGED
|
@@ -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: --
|
|
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: --
|
|
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: --
|
|
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: --
|
|
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
|
-
|
|
149
|
+
memoryDebugging: {
|
|
150
150
|
type: 'boolean',
|
|
151
|
-
describe: 'Whether to enable
|
|
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
|
-
.
|
|
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));
|
package/build/src/browser.js
CHANGED
|
@@ -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
|
}
|