@nimbus21.ai/chrome-devtools-mcp 0.12.3 → 0.17.4
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 +119 -24
- package/build/src/DevtoolsUtils.js +174 -42
- package/build/src/McpContext.js +127 -29
- package/build/src/McpResponse.js +245 -159
- package/build/src/PageCollector.js +18 -2
- package/build/src/WaitForHelper.js +2 -2
- package/build/src/browser.js +8 -13
- package/build/src/cli.js +65 -3
- package/build/src/formatters/ConsoleFormatter.js +240 -0
- package/build/src/formatters/IssueFormatter.js +190 -0
- package/build/src/formatters/NetworkFormatter.js +218 -0
- package/build/src/formatters/SnapshotFormatter.js +134 -0
- package/build/src/logger.js +9 -0
- package/build/src/main.js +66 -8
- package/build/src/telemetry/ClearcutLogger.js +102 -0
- package/build/src/telemetry/WatchdogClient.js +60 -0
- package/build/src/telemetry/flagUtils.js +45 -0
- package/build/src/telemetry/metricUtils.js +14 -0
- package/build/src/telemetry/persistence.js +53 -0
- package/build/src/telemetry/types.js +33 -0
- package/build/src/telemetry/watchdog/ClearcutSender.js +201 -0
- package/build/src/telemetry/watchdog/main.js +127 -0
- package/build/src/third_party/THIRD_PARTY_NOTICES +10 -9
- package/build/src/third_party/bundled-packages.json +8 -0
- package/build/src/third_party/devtools-formatter-worker.js +15449 -0
- package/build/src/third_party/index.js +4093 -2367
- package/build/src/third_party/issue-descriptions/connectionAllowlistInvalidAllowlistItemType.md +12 -0
- package/build/src/third_party/issue-descriptions/connectionAllowlistInvalidHeader.md +12 -0
- package/build/src/third_party/issue-descriptions/connectionAllowlistInvalidUrlPattern.md +8 -0
- package/build/src/third_party/issue-descriptions/connectionAllowlistItemNotInnerList.md +12 -0
- package/build/src/third_party/issue-descriptions/connectionAllowlistMoreThanOneList.md +7 -0
- package/build/src/third_party/issue-descriptions/connectionAllowlistReportingEndpointNotToken.md +10 -0
- package/build/src/tools/categories.js +2 -0
- package/build/src/tools/emulation.js +83 -1
- package/build/src/tools/extensions.js +79 -0
- package/build/src/tools/input.js +93 -13
- package/build/src/tools/network.js +17 -3
- package/build/src/tools/pages.js +134 -54
- package/build/src/tools/performance.js +68 -27
- package/build/src/tools/script.js +2 -2
- package/build/src/tools/tools.js +2 -0
- package/build/src/utils/ExtensionRegistry.js +35 -0
- package/build/src/utils/string.js +36 -0
- package/package.json +17 -16
- package/build/src/formatters/consoleFormatter.js +0 -121
- package/build/src/formatters/networkFormatter.js +0 -77
- package/build/src/formatters/snapshotFormatter.js +0 -73
- package/build/src/third_party/devtools.js +0 -6
- package/build/src/third_party/issue-descriptions/SameSiteInvalidSameParty.md +0 -8
- package/build/src/third_party/issue-descriptions/SameSiteSamePartyCrossPartyContextSet.md +0 -10
- package/build/src/third_party/issue-descriptions/federatedAuthRequestClientMetadataHttpNotFound.md +0 -1
- package/build/src/third_party/issue-descriptions/federatedAuthRequestClientMetadataInvalidResponse.md +0 -1
- package/build/src/third_party/issue-descriptions/federatedAuthRequestClientMetadataNoResponse.md +0 -1
package/build/src/tools/pages.js
CHANGED
|
@@ -27,16 +27,16 @@ export const selectPage = defineTool({
|
|
|
27
27
|
readOnlyHint: true,
|
|
28
28
|
},
|
|
29
29
|
schema: {
|
|
30
|
-
|
|
30
|
+
pageId: zod
|
|
31
31
|
.number()
|
|
32
|
-
.describe(`The
|
|
32
|
+
.describe(`The ID of the page to select. Call ${listPages.name} to get available pages.`),
|
|
33
33
|
bringToFront: zod
|
|
34
34
|
.boolean()
|
|
35
35
|
.optional()
|
|
36
36
|
.describe('Whether to focus the page and bring it to the top.'),
|
|
37
37
|
},
|
|
38
38
|
handler: async (request, response, context) => {
|
|
39
|
-
const page = context.
|
|
39
|
+
const page = context.getPageById(request.params.pageId);
|
|
40
40
|
context.selectPage(page);
|
|
41
41
|
response.setIncludePages(true);
|
|
42
42
|
if (request.params.bringToFront) {
|
|
@@ -52,13 +52,13 @@ export const closePage = defineTool({
|
|
|
52
52
|
readOnlyHint: false,
|
|
53
53
|
},
|
|
54
54
|
schema: {
|
|
55
|
-
|
|
55
|
+
pageId: zod
|
|
56
56
|
.number()
|
|
57
|
-
.describe('The
|
|
57
|
+
.describe('The ID of the page to close. Call list_pages to list pages.'),
|
|
58
58
|
},
|
|
59
59
|
handler: async (request, response, context) => {
|
|
60
60
|
try {
|
|
61
|
-
await context.closePage(request.params.
|
|
61
|
+
await context.closePage(request.params.pageId);
|
|
62
62
|
}
|
|
63
63
|
catch (err) {
|
|
64
64
|
if (err.message === CLOSE_PAGE_ERROR) {
|
|
@@ -80,15 +80,19 @@ export const newPage = defineTool({
|
|
|
80
80
|
},
|
|
81
81
|
schema: {
|
|
82
82
|
url: zod.string().describe('URL to load in a new page.'),
|
|
83
|
+
background: zod
|
|
84
|
+
.boolean()
|
|
85
|
+
.optional()
|
|
86
|
+
.describe('Whether to open the page in the background without bringing it to the front. Default is false (foreground).'),
|
|
83
87
|
...timeoutSchema,
|
|
84
88
|
},
|
|
85
89
|
handler: async (request, response, context) => {
|
|
86
|
-
const page = await context.newPage();
|
|
90
|
+
const page = await context.newPage(request.params.background);
|
|
87
91
|
await context.waitForEventsAfterAction(async () => {
|
|
88
92
|
await page.goto(request.params.url, {
|
|
89
93
|
timeout: request.params.timeout,
|
|
90
94
|
});
|
|
91
|
-
});
|
|
95
|
+
}, { timeout: request.params.timeout });
|
|
92
96
|
response.setIncludePages(true);
|
|
93
97
|
},
|
|
94
98
|
});
|
|
@@ -109,6 +113,14 @@ export const navigatePage = defineTool({
|
|
|
109
113
|
.boolean()
|
|
110
114
|
.optional()
|
|
111
115
|
.describe('Whether to ignore cache on reload.'),
|
|
116
|
+
handleBeforeUnload: zod
|
|
117
|
+
.enum(['accept', 'decline'])
|
|
118
|
+
.optional()
|
|
119
|
+
.describe('Whether to auto accept or beforeunload dialogs triggered by this navigation. Default is accept.'),
|
|
120
|
+
initScript: zod
|
|
121
|
+
.string()
|
|
122
|
+
.optional()
|
|
123
|
+
.describe('A JavaScript script to be executed on each new document before any other scripts for the next navigation.'),
|
|
112
124
|
...timeoutSchema,
|
|
113
125
|
},
|
|
114
126
|
handler: async (request, response, context) => {
|
|
@@ -122,52 +134,85 @@ export const navigatePage = defineTool({
|
|
|
122
134
|
if (!request.params.type) {
|
|
123
135
|
request.params.type = 'url';
|
|
124
136
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
break;
|
|
139
|
-
case 'back':
|
|
140
|
-
try {
|
|
141
|
-
await page.goBack(options);
|
|
142
|
-
response.appendResponseLine(`Successfully navigated back to ${page.url()}.`);
|
|
143
|
-
}
|
|
144
|
-
catch (error) {
|
|
145
|
-
response.appendResponseLine(`Unable to navigate back in the selected page: ${error.message}.`);
|
|
146
|
-
}
|
|
147
|
-
break;
|
|
148
|
-
case 'forward':
|
|
149
|
-
try {
|
|
150
|
-
await page.goForward(options);
|
|
151
|
-
response.appendResponseLine(`Successfully navigated forward to ${page.url()}.`);
|
|
152
|
-
}
|
|
153
|
-
catch (error) {
|
|
154
|
-
response.appendResponseLine(`Unable to navigate forward in the selected page: ${error.message}.`);
|
|
155
|
-
}
|
|
156
|
-
break;
|
|
157
|
-
case 'reload':
|
|
158
|
-
try {
|
|
159
|
-
await page.reload({
|
|
160
|
-
...options,
|
|
161
|
-
ignoreCache: request.params.ignoreCache,
|
|
162
|
-
});
|
|
163
|
-
response.appendResponseLine(`Successfully reloaded the page.`);
|
|
164
|
-
}
|
|
165
|
-
catch (error) {
|
|
166
|
-
response.appendResponseLine(`Unable to reload the selected page: ${error.message}.`);
|
|
167
|
-
}
|
|
168
|
-
break;
|
|
137
|
+
const handleBeforeUnload = request.params.handleBeforeUnload ?? 'accept';
|
|
138
|
+
const dialogHandler = (dialog) => {
|
|
139
|
+
if (dialog.type() === 'beforeunload') {
|
|
140
|
+
if (handleBeforeUnload === 'accept') {
|
|
141
|
+
response.appendResponseLine(`Accepted a beforeunload dialog.`);
|
|
142
|
+
void dialog.accept();
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
response.appendResponseLine(`Declined a beforeunload dialog.`);
|
|
146
|
+
void dialog.dismiss();
|
|
147
|
+
}
|
|
148
|
+
// We are not going to report the dialog like regular dialogs.
|
|
149
|
+
context.clearDialog();
|
|
169
150
|
}
|
|
170
|
-
}
|
|
151
|
+
};
|
|
152
|
+
let initScriptId;
|
|
153
|
+
if (request.params.initScript) {
|
|
154
|
+
const { identifier } = await page.evaluateOnNewDocument(request.params.initScript);
|
|
155
|
+
initScriptId = identifier;
|
|
156
|
+
}
|
|
157
|
+
page.on('dialog', dialogHandler);
|
|
158
|
+
try {
|
|
159
|
+
await context.waitForEventsAfterAction(async () => {
|
|
160
|
+
switch (request.params.type) {
|
|
161
|
+
case 'url':
|
|
162
|
+
if (!request.params.url) {
|
|
163
|
+
throw new Error('A URL is required for navigation of type=url.');
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
await page.goto(request.params.url, options);
|
|
167
|
+
response.appendResponseLine(`Successfully navigated to ${request.params.url}.`);
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
response.appendResponseLine(`Unable to navigate in the selected page: ${error.message}.`);
|
|
171
|
+
}
|
|
172
|
+
break;
|
|
173
|
+
case 'back':
|
|
174
|
+
try {
|
|
175
|
+
await page.goBack(options);
|
|
176
|
+
response.appendResponseLine(`Successfully navigated back to ${page.url()}.`);
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
response.appendResponseLine(`Unable to navigate back in the selected page: ${error.message}.`);
|
|
180
|
+
}
|
|
181
|
+
break;
|
|
182
|
+
case 'forward':
|
|
183
|
+
try {
|
|
184
|
+
await page.goForward(options);
|
|
185
|
+
response.appendResponseLine(`Successfully navigated forward to ${page.url()}.`);
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
response.appendResponseLine(`Unable to navigate forward in the selected page: ${error.message}.`);
|
|
189
|
+
}
|
|
190
|
+
break;
|
|
191
|
+
case 'reload':
|
|
192
|
+
try {
|
|
193
|
+
await page.reload({
|
|
194
|
+
...options,
|
|
195
|
+
ignoreCache: request.params.ignoreCache,
|
|
196
|
+
});
|
|
197
|
+
response.appendResponseLine(`Successfully reloaded the page.`);
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
response.appendResponseLine(`Unable to reload the selected page: ${error.message}.`);
|
|
201
|
+
}
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
}, { timeout: request.params.timeout });
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
page.off('dialog', dialogHandler);
|
|
208
|
+
if (initScriptId) {
|
|
209
|
+
await page
|
|
210
|
+
.removeScriptToEvaluateOnNewDocument(initScriptId)
|
|
211
|
+
.catch(error => {
|
|
212
|
+
logger(`Failed to remove init script`, error);
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
171
216
|
response.setIncludePages(true);
|
|
172
217
|
},
|
|
173
218
|
});
|
|
@@ -184,7 +229,22 @@ export const resizePage = defineTool({
|
|
|
184
229
|
},
|
|
185
230
|
handler: async (request, response, context) => {
|
|
186
231
|
const page = context.getSelectedPage();
|
|
187
|
-
|
|
232
|
+
try {
|
|
233
|
+
const browser = page.browser();
|
|
234
|
+
const windowId = await page.windowId();
|
|
235
|
+
const bounds = await browser.getWindowBounds(windowId);
|
|
236
|
+
if (bounds.windowState === 'fullscreen') {
|
|
237
|
+
// Have to call this twice on Ubuntu when the window is in fullscreen mode.
|
|
238
|
+
await browser.setWindowBounds(windowId, { windowState: 'normal' });
|
|
239
|
+
await browser.setWindowBounds(windowId, { windowState: 'normal' });
|
|
240
|
+
}
|
|
241
|
+
else if (bounds.windowState !== 'normal') {
|
|
242
|
+
await browser.setWindowBounds(windowId, { windowState: 'normal' });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
// Window APIs are not supported on all platforms
|
|
247
|
+
}
|
|
188
248
|
await page.resize({
|
|
189
249
|
contentWidth: request.params.width,
|
|
190
250
|
contentHeight: request.params.height,
|
|
@@ -241,3 +301,23 @@ export const handleDialog = defineTool({
|
|
|
241
301
|
response.setIncludePages(true);
|
|
242
302
|
},
|
|
243
303
|
});
|
|
304
|
+
export const getTabId = defineTool({
|
|
305
|
+
name: 'get_tab_id',
|
|
306
|
+
description: `Get the tab ID of the page`,
|
|
307
|
+
annotations: {
|
|
308
|
+
category: ToolCategory.NAVIGATION,
|
|
309
|
+
readOnlyHint: true,
|
|
310
|
+
conditions: ['experimentalInteropTools'],
|
|
311
|
+
},
|
|
312
|
+
schema: {
|
|
313
|
+
pageId: zod
|
|
314
|
+
.number()
|
|
315
|
+
.describe(`The ID of the page to get the tab ID for. Call ${listPages.name} to get available pages.`),
|
|
316
|
+
},
|
|
317
|
+
handler: async (request, response, context) => {
|
|
318
|
+
const page = context.getPageById(request.params.pageId);
|
|
319
|
+
// @ts-expect-error _tabId is internal.
|
|
320
|
+
const tabId = page._tabId;
|
|
321
|
+
response.setTabId(tabId);
|
|
322
|
+
},
|
|
323
|
+
});
|
|
@@ -3,25 +3,31 @@
|
|
|
3
3
|
* Copyright 2025 Google LLC
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
|
+
import zlib from 'node:zlib';
|
|
6
7
|
import { logger } from '../logger.js';
|
|
7
|
-
import { zod } from '../third_party/index.js';
|
|
8
|
-
import {
|
|
8
|
+
import { zod, DevTools } from '../third_party/index.js';
|
|
9
|
+
import { parseRawTraceBuffer, traceResultIsSuccess, } from '../trace-processing/parse.js';
|
|
9
10
|
import { ToolCategory } from './categories.js';
|
|
10
11
|
import { defineTool } from './ToolDefinition.js';
|
|
12
|
+
const filePathSchema = zod
|
|
13
|
+
.string()
|
|
14
|
+
.optional()
|
|
15
|
+
.describe('The absolute file path, or a file path relative to the current working directory, to save the raw trace data. For example, trace.json.gz (compressed) or trace.json (uncompressed).');
|
|
11
16
|
export const startTrace = defineTool({
|
|
12
17
|
name: 'performance_start_trace',
|
|
13
|
-
description:
|
|
18
|
+
description: `Starts a performance trace recording on the selected page. This can be used to look for performance problems and insights to improve the performance of the page. It will also report Core Web Vital (CWV) scores for the page.`,
|
|
14
19
|
annotations: {
|
|
15
20
|
category: ToolCategory.PERFORMANCE,
|
|
16
|
-
readOnlyHint:
|
|
21
|
+
readOnlyHint: false,
|
|
17
22
|
},
|
|
18
23
|
schema: {
|
|
19
24
|
reload: zod
|
|
20
25
|
.boolean()
|
|
21
|
-
.describe('Determines if, once tracing has started, the page should be automatically reloaded.'),
|
|
26
|
+
.describe('Determines if, once tracing has started, the current selected page should be automatically reloaded. Navigate the page to the right URL using the navigate_page tool BEFORE starting the trace if reload or autoStop is set to true.'),
|
|
22
27
|
autoStop: zod
|
|
23
28
|
.boolean()
|
|
24
29
|
.describe('Determines if the trace recording should be automatically stopped.'),
|
|
30
|
+
filePath: filePathSchema,
|
|
25
31
|
},
|
|
26
32
|
handler: async (request, response, context) => {
|
|
27
33
|
if (context.isRunningPerformanceTrace()) {
|
|
@@ -68,7 +74,7 @@ export const startTrace = defineTool({
|
|
|
68
74
|
}
|
|
69
75
|
if (request.params.autoStop) {
|
|
70
76
|
await new Promise(resolve => setTimeout(resolve, 5_000));
|
|
71
|
-
await stopTracingAndAppendOutput(page, response, context);
|
|
77
|
+
await stopTracingAndAppendOutput(page, response, context, request.params.filePath);
|
|
72
78
|
}
|
|
73
79
|
else {
|
|
74
80
|
response.appendResponseLine(`The performance trace is being recorded. Use performance_stop_trace to stop it.`);
|
|
@@ -80,15 +86,17 @@ export const stopTrace = defineTool({
|
|
|
80
86
|
description: 'Stops the active performance trace recording on the selected page.',
|
|
81
87
|
annotations: {
|
|
82
88
|
category: ToolCategory.PERFORMANCE,
|
|
83
|
-
readOnlyHint:
|
|
89
|
+
readOnlyHint: false,
|
|
90
|
+
},
|
|
91
|
+
schema: {
|
|
92
|
+
filePath: filePathSchema,
|
|
84
93
|
},
|
|
85
|
-
|
|
86
|
-
handler: async (_request, response, context) => {
|
|
94
|
+
handler: async (request, response, context) => {
|
|
87
95
|
if (!context.isRunningPerformanceTrace()) {
|
|
88
96
|
return;
|
|
89
97
|
}
|
|
90
98
|
const page = context.getSelectedPage();
|
|
91
|
-
await stopTracingAndAppendOutput(page, response, context);
|
|
99
|
+
await stopTracingAndAppendOutput(page, response, context, request.params.filePath);
|
|
92
100
|
},
|
|
93
101
|
});
|
|
94
102
|
export const analyzeInsight = defineTool({
|
|
@@ -112,36 +120,69 @@ export const analyzeInsight = defineTool({
|
|
|
112
120
|
response.appendResponseLine('No recorded traces found. Record a performance trace so you have Insights to analyze.');
|
|
113
121
|
return;
|
|
114
122
|
}
|
|
115
|
-
|
|
116
|
-
if ('error' in insightOutput) {
|
|
117
|
-
response.appendResponseLine(insightOutput.error);
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
response.appendResponseLine(insightOutput.output);
|
|
123
|
+
response.attachTraceInsight(lastRecording, request.params.insightSetId, request.params.insightName);
|
|
121
124
|
},
|
|
122
125
|
});
|
|
123
|
-
async function stopTracingAndAppendOutput(page, response, context) {
|
|
126
|
+
async function stopTracingAndAppendOutput(page, response, context, filePath) {
|
|
124
127
|
try {
|
|
125
128
|
const traceEventsBuffer = await page.tracing.stop();
|
|
129
|
+
if (filePath && traceEventsBuffer) {
|
|
130
|
+
let dataToWrite = traceEventsBuffer;
|
|
131
|
+
if (filePath.endsWith('.gz')) {
|
|
132
|
+
dataToWrite = await new Promise((resolve, reject) => {
|
|
133
|
+
zlib.gzip(traceEventsBuffer, (error, result) => {
|
|
134
|
+
if (error) {
|
|
135
|
+
reject(error);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
resolve(result);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
const file = await context.saveFile(dataToWrite, filePath);
|
|
144
|
+
response.appendResponseLine(`The raw trace data was saved to ${file.filename}.`);
|
|
145
|
+
}
|
|
126
146
|
const result = await parseRawTraceBuffer(traceEventsBuffer);
|
|
127
147
|
response.appendResponseLine('The performance trace has been stopped.');
|
|
128
148
|
if (traceResultIsSuccess(result)) {
|
|
149
|
+
if (context.isCruxEnabled()) {
|
|
150
|
+
await populateCruxData(result);
|
|
151
|
+
}
|
|
129
152
|
context.storeTraceRecording(result);
|
|
130
|
-
|
|
131
|
-
response.appendResponseLine(traceSummaryText);
|
|
153
|
+
response.attachTraceSummary(result);
|
|
132
154
|
}
|
|
133
155
|
else {
|
|
134
|
-
|
|
135
|
-
response.appendResponseLine(result.error);
|
|
156
|
+
throw new Error(`There was an unexpected error parsing the trace: ${result.error}`);
|
|
136
157
|
}
|
|
137
158
|
}
|
|
138
|
-
catch (e) {
|
|
139
|
-
const errorText = e instanceof Error ? e.message : JSON.stringify(e);
|
|
140
|
-
logger(`Error stopping performance trace: ${errorText}`);
|
|
141
|
-
response.appendResponseLine('An error occurred generating the response for this trace:');
|
|
142
|
-
response.appendResponseLine(errorText);
|
|
143
|
-
}
|
|
144
159
|
finally {
|
|
145
160
|
context.setIsRunningPerformanceTrace(false);
|
|
146
161
|
}
|
|
147
162
|
}
|
|
163
|
+
/** We tell CrUXManager to fetch data so it's available when DevTools.PerformanceTraceFormatter is invoked */
|
|
164
|
+
async function populateCruxData(result) {
|
|
165
|
+
logger('populateCruxData called');
|
|
166
|
+
const cruxManager = DevTools.CrUXManager.instance();
|
|
167
|
+
// go/jtfbx. Yes, we're aware this API key is public. ;)
|
|
168
|
+
cruxManager.setEndpointForTesting('https://chromeuxreport.googleapis.com/v1/records:queryRecord?key=AIzaSyBn5gimNjhiEyA_euicSKko6IlD3HdgUfk');
|
|
169
|
+
const cruxSetting = DevTools.Common.Settings.Settings.instance().createSetting('field-data', {
|
|
170
|
+
enabled: true,
|
|
171
|
+
});
|
|
172
|
+
cruxSetting.set({ enabled: true });
|
|
173
|
+
// Gather URLs to fetch CrUX data for
|
|
174
|
+
const urls = [...(result.parsedTrace.insights?.values() ?? [])].map(c => c.url.toString());
|
|
175
|
+
urls.push(result.parsedTrace.data.Meta.mainFrameURL);
|
|
176
|
+
const urlSet = new Set(urls);
|
|
177
|
+
if (urlSet.size === 0) {
|
|
178
|
+
logger('No URLs found for CrUX data');
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
logger(`Fetching CrUX data for ${urlSet.size} URLs: ${Array.from(urlSet).join(', ')}`);
|
|
182
|
+
const cruxData = await Promise.all(Array.from(urlSet).map(async (url) => {
|
|
183
|
+
const data = await cruxManager.getFieldDataForPage(url);
|
|
184
|
+
logger(`CrUX data for ${url}: ${data ? 'found' : 'not found'}`);
|
|
185
|
+
return data;
|
|
186
|
+
}));
|
|
187
|
+
result.parsedTrace.metadata.cruxFieldData = cruxData;
|
|
188
|
+
}
|
|
@@ -8,8 +8,8 @@ import { ToolCategory } from './categories.js';
|
|
|
8
8
|
import { defineTool } from './ToolDefinition.js';
|
|
9
9
|
export const evaluateScript = defineTool({
|
|
10
10
|
name: 'evaluate_script',
|
|
11
|
-
description: `Evaluate a JavaScript function inside the currently selected page. Returns the response as JSON
|
|
12
|
-
so returned values have to JSON-serializable.`,
|
|
11
|
+
description: `Evaluate a JavaScript function inside the currently selected page. Returns the response as JSON,
|
|
12
|
+
so returned values have to be JSON-serializable.`,
|
|
13
13
|
annotations: {
|
|
14
14
|
category: ToolCategory.DEBUGGING,
|
|
15
15
|
readOnlyHint: false,
|
package/build/src/tools/tools.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import * as consoleTools from './console.js';
|
|
7
7
|
import * as emulationTools from './emulation.js';
|
|
8
|
+
import * as extensionTools from './extensions.js';
|
|
8
9
|
import * as inputTools from './input.js';
|
|
9
10
|
import * as networkTools from './network.js';
|
|
10
11
|
import * as pagesTools from './pages.js';
|
|
@@ -15,6 +16,7 @@ import * as snapshotTools from './snapshot.js';
|
|
|
15
16
|
const tools = [
|
|
16
17
|
...Object.values(consoleTools),
|
|
17
18
|
...Object.values(emulationTools),
|
|
19
|
+
...Object.values(extensionTools),
|
|
18
20
|
...Object.values(inputTools),
|
|
19
21
|
...Object.values(networkTools),
|
|
20
22
|
...Object.values(pagesTools),
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs/promises';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
export class ExtensionRegistry {
|
|
9
|
+
#extensions = new Map();
|
|
10
|
+
async registerExtension(id, extensionPath) {
|
|
11
|
+
const manifestPath = path.join(extensionPath, 'manifest.json');
|
|
12
|
+
const manifestContent = await fs.readFile(manifestPath, 'utf-8');
|
|
13
|
+
const manifest = JSON.parse(manifestContent);
|
|
14
|
+
const name = manifest.name ?? 'Unknown';
|
|
15
|
+
const version = manifest.version ?? 'Unknown';
|
|
16
|
+
const extension = {
|
|
17
|
+
id,
|
|
18
|
+
name,
|
|
19
|
+
version,
|
|
20
|
+
isEnabled: true,
|
|
21
|
+
path: extensionPath,
|
|
22
|
+
};
|
|
23
|
+
this.#extensions.set(extension.id, extension);
|
|
24
|
+
return extension;
|
|
25
|
+
}
|
|
26
|
+
remove(id) {
|
|
27
|
+
this.#extensions.delete(id);
|
|
28
|
+
}
|
|
29
|
+
list() {
|
|
30
|
+
return Array.from(this.#extensions.values());
|
|
31
|
+
}
|
|
32
|
+
getById(id) {
|
|
33
|
+
return this.#extensions.get(id);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Converts a given string to snake_case.
|
|
8
|
+
* This function handles camelCase, PascalCase, and acronyms, including transitions between letters and numbers.
|
|
9
|
+
* It uses Unicode-aware regular expressions (`\p{L}`, `\p{N}`, `\p{Lu}`, `\p{Ll}` with the `u` flag)
|
|
10
|
+
* to correctly process letters and numbers from various languages.
|
|
11
|
+
*
|
|
12
|
+
* @param text The input string to convert to snake_case.
|
|
13
|
+
* @returns The snake_case version of the input string.
|
|
14
|
+
*/
|
|
15
|
+
export function toSnakeCase(text) {
|
|
16
|
+
if (!text) {
|
|
17
|
+
return '';
|
|
18
|
+
}
|
|
19
|
+
// First, handle case-based transformations to insert underscores correctly.
|
|
20
|
+
// 1. Add underscore between a letter and a number.
|
|
21
|
+
// e.g., "version2" -> "version_2"
|
|
22
|
+
// 2. Add underscore between an uppercase letter sequence and a following uppercase+lowercase sequence.
|
|
23
|
+
// e.g., "APIFlags" -> "API_Flags"
|
|
24
|
+
// 3. Add underscore between a lowercase/number and an uppercase letter.
|
|
25
|
+
// e.g., "lastName" -> "last_Name", "version_2Update" -> "version_2_Update"
|
|
26
|
+
// 4. Replace sequences of non-alphanumeric with a single underscore
|
|
27
|
+
// 5. Remove any leading or trailing underscores.
|
|
28
|
+
const result = text
|
|
29
|
+
.replace(/(\p{L})(\p{N})/gu, '$1_$2') // 1
|
|
30
|
+
.replace(/(\p{Lu}+)(\p{Lu}\p{Ll})/gu, '$1_$2') // 2
|
|
31
|
+
.replace(/(\p{Ll}|\p{N})(\p{Lu})/gu, '$1_$2') // 3
|
|
32
|
+
.toLowerCase()
|
|
33
|
+
.replace(/[^\p{L}\p{N}]+/gu, '_') // 4
|
|
34
|
+
.replace(/^_|_$/g, ''); // 5
|
|
35
|
+
return result;
|
|
36
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nimbus21.ai/chrome-devtools-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.4",
|
|
4
4
|
"description": "MCP server for Chrome DevTools with stealth mode support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": "./build/src/index.js",
|
|
@@ -16,14 +16,14 @@
|
|
|
16
16
|
"docs:generate": "node --experimental-strip-types scripts/generate-docs.ts",
|
|
17
17
|
"start": "npm run build && node build/src/index.js",
|
|
18
18
|
"start-debug": "DEBUG=mcp:* DEBUG_COLORS=false npm run build && node build/src/index.js",
|
|
19
|
-
"test
|
|
20
|
-
"test:no-build": "node
|
|
21
|
-
"test": "npm run build &&
|
|
22
|
-
"test:
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
19
|
+
"test": "npm run build && node scripts/test.mjs",
|
|
20
|
+
"test:no-build": "node scripts/test.mjs",
|
|
21
|
+
"test:only": "npm run build && node scripts/test.mjs --test-only",
|
|
22
|
+
"test:update-snapshots": "npm run build && node scripts/test.mjs --test-update-snapshots",
|
|
23
|
+
"prepare": "node --experimental-strip-types scripts/prepare.ts",
|
|
24
|
+
"verify-server-json-version": "node --experimental-strip-types scripts/verify-server-json-version.ts",
|
|
25
|
+
"eval": "npm run build && CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS=true node --experimental-strip-types scripts/eval_gemini.ts",
|
|
26
|
+
"count-tokens": "node --experimental-strip-types scripts/count_tokens.ts"
|
|
27
27
|
},
|
|
28
28
|
"files": [
|
|
29
29
|
"build/src",
|
|
@@ -46,7 +46,8 @@
|
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@eslint/js": "^9.35.0",
|
|
49
|
-
"@
|
|
49
|
+
"@google/genai": "^1.37.0",
|
|
50
|
+
"@modelcontextprotocol/sdk": "1.26.0",
|
|
50
51
|
"@rollup/plugin-commonjs": "^29.0.0",
|
|
51
52
|
"@rollup/plugin-json": "^6.1.0",
|
|
52
53
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
@@ -58,20 +59,20 @@
|
|
|
58
59
|
"@types/yargs": "^17.0.33",
|
|
59
60
|
"@typescript-eslint/eslint-plugin": "^8.43.0",
|
|
60
61
|
"@typescript-eslint/parser": "^8.43.0",
|
|
61
|
-
"chrome-devtools-frontend": "1.0.
|
|
62
|
-
"core-js": "3.
|
|
62
|
+
"chrome-devtools-frontend": "1.0.1583146",
|
|
63
|
+
"core-js": "3.48.0",
|
|
63
64
|
"debug": "4.4.3",
|
|
64
65
|
"eslint": "^9.35.0",
|
|
65
66
|
"eslint-import-resolver-typescript": "^4.4.4",
|
|
66
67
|
"eslint-plugin-import": "^2.32.0",
|
|
67
|
-
"globals": "^
|
|
68
|
+
"globals": "^17.0.0",
|
|
68
69
|
"prettier": "^3.6.2",
|
|
69
|
-
"puppeteer": "24.
|
|
70
|
-
"rollup": "4.
|
|
70
|
+
"puppeteer": "24.37.4",
|
|
71
|
+
"rollup": "4.57.1",
|
|
71
72
|
"rollup-plugin-cleanup": "^3.2.1",
|
|
72
73
|
"rollup-plugin-license": "^3.6.0",
|
|
73
74
|
"sinon": "^21.0.0",
|
|
74
|
-
"
|
|
75
|
+
"tiktoken": "^1.0.22",
|
|
75
76
|
"typescript": "^5.9.2",
|
|
76
77
|
"typescript-eslint": "^8.43.0",
|
|
77
78
|
"yargs": "18.0.0"
|