@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.
Files changed (53) hide show
  1. package/README.md +119 -24
  2. package/build/src/DevtoolsUtils.js +174 -42
  3. package/build/src/McpContext.js +127 -29
  4. package/build/src/McpResponse.js +245 -159
  5. package/build/src/PageCollector.js +18 -2
  6. package/build/src/WaitForHelper.js +2 -2
  7. package/build/src/browser.js +8 -13
  8. package/build/src/cli.js +65 -3
  9. package/build/src/formatters/ConsoleFormatter.js +240 -0
  10. package/build/src/formatters/IssueFormatter.js +190 -0
  11. package/build/src/formatters/NetworkFormatter.js +218 -0
  12. package/build/src/formatters/SnapshotFormatter.js +134 -0
  13. package/build/src/logger.js +9 -0
  14. package/build/src/main.js +66 -8
  15. package/build/src/telemetry/ClearcutLogger.js +102 -0
  16. package/build/src/telemetry/WatchdogClient.js +60 -0
  17. package/build/src/telemetry/flagUtils.js +45 -0
  18. package/build/src/telemetry/metricUtils.js +14 -0
  19. package/build/src/telemetry/persistence.js +53 -0
  20. package/build/src/telemetry/types.js +33 -0
  21. package/build/src/telemetry/watchdog/ClearcutSender.js +201 -0
  22. package/build/src/telemetry/watchdog/main.js +127 -0
  23. package/build/src/third_party/THIRD_PARTY_NOTICES +10 -9
  24. package/build/src/third_party/bundled-packages.json +8 -0
  25. package/build/src/third_party/devtools-formatter-worker.js +15449 -0
  26. package/build/src/third_party/index.js +4093 -2367
  27. package/build/src/third_party/issue-descriptions/connectionAllowlistInvalidAllowlistItemType.md +12 -0
  28. package/build/src/third_party/issue-descriptions/connectionAllowlistInvalidHeader.md +12 -0
  29. package/build/src/third_party/issue-descriptions/connectionAllowlistInvalidUrlPattern.md +8 -0
  30. package/build/src/third_party/issue-descriptions/connectionAllowlistItemNotInnerList.md +12 -0
  31. package/build/src/third_party/issue-descriptions/connectionAllowlistMoreThanOneList.md +7 -0
  32. package/build/src/third_party/issue-descriptions/connectionAllowlistReportingEndpointNotToken.md +10 -0
  33. package/build/src/tools/categories.js +2 -0
  34. package/build/src/tools/emulation.js +83 -1
  35. package/build/src/tools/extensions.js +79 -0
  36. package/build/src/tools/input.js +93 -13
  37. package/build/src/tools/network.js +17 -3
  38. package/build/src/tools/pages.js +134 -54
  39. package/build/src/tools/performance.js +68 -27
  40. package/build/src/tools/script.js +2 -2
  41. package/build/src/tools/tools.js +2 -0
  42. package/build/src/utils/ExtensionRegistry.js +35 -0
  43. package/build/src/utils/string.js +36 -0
  44. package/package.json +17 -16
  45. package/build/src/formatters/consoleFormatter.js +0 -121
  46. package/build/src/formatters/networkFormatter.js +0 -77
  47. package/build/src/formatters/snapshotFormatter.js +0 -73
  48. package/build/src/third_party/devtools.js +0 -6
  49. package/build/src/third_party/issue-descriptions/SameSiteInvalidSameParty.md +0 -8
  50. package/build/src/third_party/issue-descriptions/SameSiteSamePartyCrossPartyContextSet.md +0 -10
  51. package/build/src/third_party/issue-descriptions/federatedAuthRequestClientMetadataHttpNotFound.md +0 -1
  52. package/build/src/third_party/issue-descriptions/federatedAuthRequestClientMetadataInvalidResponse.md +0 -1
  53. package/build/src/third_party/issue-descriptions/federatedAuthRequestClientMetadataNoResponse.md +0 -1
@@ -27,16 +27,16 @@ export const selectPage = defineTool({
27
27
  readOnlyHint: true,
28
28
  },
29
29
  schema: {
30
- pageIdx: zod
30
+ pageId: zod
31
31
  .number()
32
- .describe(`The index of the page to select. Call ${listPages.name} to get available pages.`),
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.getPageByIdx(request.params.pageIdx);
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
- pageIdx: zod
55
+ pageId: zod
56
56
  .number()
57
- .describe('The index of the page to close. Call list_pages to list pages.'),
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.pageIdx);
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
- await context.waitForEventsAfterAction(async () => {
126
- switch (request.params.type) {
127
- case 'url':
128
- if (!request.params.url) {
129
- throw new Error('A URL is required for navigation of type=url.');
130
- }
131
- try {
132
- await page.goto(request.params.url, options);
133
- response.appendResponseLine(`Successfully navigated to ${request.params.url}.`);
134
- }
135
- catch (error) {
136
- response.appendResponseLine(`Unable to navigate in the selected page: ${error.message}.`);
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
- // @ts-expect-error internal API for now.
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 { getInsightOutput, getTraceSummary, parseRawTraceBuffer, traceResultIsSuccess, } from '../trace-processing/parse.js';
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: '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.',
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: true,
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: true,
89
+ readOnlyHint: false,
90
+ },
91
+ schema: {
92
+ filePath: filePathSchema,
84
93
  },
85
- schema: {},
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
- const insightOutput = getInsightOutput(lastRecording, request.params.insightSetId, request.params.insightName);
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
- const traceSummaryText = getTraceSummary(result);
131
- response.appendResponseLine(traceSummaryText);
153
+ response.attachTraceSummary(result);
132
154
  }
133
155
  else {
134
- response.appendResponseLine('There was an unexpected error parsing the trace:');
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,
@@ -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.12.3",
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:node20": "node --import ./build/tests/setup.js --test-reporter spec --test-force-exit --test build/tests",
20
- "test:no-build": "node --import ./build/tests/setup.js --no-warnings=ExperimentalWarning --test-reporter spec --test-force-exit --test \"build/tests/**/*.test.js\"",
21
- "test": "npm run build && npm run test:no-build",
22
- "test:only": "npm run build && npm run test:only:no-build",
23
- "test:only:no-build": "node --import ./build/tests/setup.js --no-warnings=ExperimentalWarning --test-reporter spec --test-force-exit --test --test-only \"build/tests/**/*.test.js\"",
24
- "test:update-snapshots": "npm run build && node --import ./build/tests/setup.js --no-warnings=ExperimentalWarning --test-force-exit --test --test-update-snapshots \"build/tests/**/*.test.js\"",
25
- "prepare": "node scripts/prepare.js",
26
- "verify-server-json-version": "node build/scripts/verify-server-json-version.js"
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
- "@modelcontextprotocol/sdk": "1.24.3",
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.1555430",
62
- "core-js": "3.47.0",
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": "^16.4.0",
68
+ "globals": "^17.0.0",
68
69
  "prettier": "^3.6.2",
69
- "puppeteer": "24.33.0",
70
- "rollup": "4.53.3",
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
- "tsx": "^4.21.0",
75
+ "tiktoken": "^1.0.22",
75
76
  "typescript": "^5.9.2",
76
77
  "typescript-eslint": "^8.43.0",
77
78
  "yargs": "18.0.0"