From fed35b950a045480c11fc1ff2118fe1b446ba45c Mon Sep 17 00:00:00 2001
From: Wolfgang Beyer
Date: Thu, 9 Apr 2026 08:47:47 +0000
Subject: [PATCH 01/26] in-page tool output: handle DOM elements and limit
depth
---
src/McpContext.ts | 177 ++++++++++++++++++++++++--
src/McpPage.ts | 17 +++
src/tools/ToolDefinition.ts | 18 +++
src/tools/inPage.ts | 144 ++++++++++++++++++++-
tests/McpContext.test.ts | 135 ++++++++++++++++++++
tests/tools/inPage.test.ts | 243 ++++++++++++++++++++++++++++++++++--
6 files changed, 712 insertions(+), 22 deletions(-)
diff --git a/src/McpContext.ts b/src/McpContext.ts
index 32b7413e6..cd11fc13d 100644
--- a/src/McpContext.ts
+++ b/src/McpContext.ts
@@ -16,7 +16,7 @@ import {
type ListenerMap,
type UncaughtError,
} from './PageCollector.js';
-import type {DevTools} from './third_party/index.js';
+import type {DevTools, Protocol} from './third_party/index.js';
import type {
Browser,
BrowserContext,
@@ -29,11 +29,15 @@ import type {
Viewport,
Target,
} from './third_party/index.js';
-import {Locator} from './third_party/index.js';
+import {Locator, type ElementHandle} from './third_party/index.js';
import {PredefinedNetworkConditions} from './third_party/index.js';
import {listPages} from './tools/pages.js';
import {CLOSE_PAGE_ERROR} from './tools/ToolDefinition.js';
-import type {Context, DevToolsData} from './tools/ToolDefinition.js';
+import type {
+ Context,
+ DevToolsData,
+ ContextPage,
+} from './tools/ToolDefinition.js';
import type {TraceResult} from './trace-processing/parse.js';
import type {
EmulationSettings,
@@ -74,7 +78,7 @@ export class McpContext implements Context {
#extensionServiceWorkers: ExtensionServiceWorker[] = [];
#mcpPages = new Map();
- #selectedPage?: McpPage;
+ #selectedPage?: ContextPage;
#networkCollector: NetworkCollector;
#consoleCollector: ConsoleCollector;
#devtoolsUniverseManager: UniverseManager;
@@ -159,7 +163,10 @@ export class McpContext implements Context {
return context;
}
- resolveCdpRequestId(page: McpPage, cdpRequestId: string): number | undefined {
+ resolveCdpRequestId(
+ page: ContextPage,
+ cdpRequestId: string,
+ ): number | undefined {
if (!cdpRequestId) {
this.logger('no network request');
return;
@@ -176,14 +183,14 @@ export class McpContext implements Context {
}
resolveCdpElementId(
- page: McpPage,
+ page: ContextPage,
cdpBackendNodeId: number,
): string | undefined {
if (!cdpBackendNodeId) {
this.logger('no cdpBackendNodeId');
return;
}
- const snapshot = page.textSnapshot;
+ const snapshot = page.getSnapshot();
if (!snapshot) {
this.logger('no text snapshot');
return;
@@ -276,7 +283,7 @@ export class McpContext implements Context {
return this.#networkCollector.getById(page.pptrPage, reqid);
}
- async restoreEmulation(page: McpPage) {
+ async restoreEmulation(page: ContextPage) {
const currentSetting = page.emulationSettings;
await this.emulate(currentSetting, page.pptrPage);
}
@@ -442,7 +449,7 @@ export class McpContext implements Context {
return this.#selectedPage?.pptrPage === page;
}
- selectPage(newPage: McpPage): void {
+ selectPage(newPage: ContextPage): void {
this.#selectedPage = newPage;
this.#updateSelectedPageTimeouts();
}
@@ -675,7 +682,7 @@ export class McpContext implements Context {
return this.#mcpPages.get(page)?.devToolsPage;
}
- async getDevToolsData(page: McpPage): Promise {
+ async getDevToolsData(page: ContextPage): Promise {
try {
this.logger('Getting DevTools UI data');
const devtoolsPage = this.getDevToolsPage(page.pptrPage);
@@ -712,9 +719,10 @@ export class McpContext implements Context {
* Creates a text snapshot of a page.
*/
async createTextSnapshot(
- page: McpPage,
+ page: ContextPage,
verbose = false,
devtoolsData: DevToolsData | undefined = undefined,
+ extraHandles?: ElementHandle[],
): Promise {
const rootNode = await page.pptrPage.accessibility.snapshot({
includeIframes: true,
@@ -768,6 +776,151 @@ export class McpContext implements Context {
};
const rootNodeWithId = assignIds(rootNode);
+
+ const createExtraNode = async (
+ handle: ElementHandle,
+ ): Promise => {
+ const backendNodeId = await handle.backendNodeId();
+ if (!backendNodeId) {
+ return null;
+ }
+ const uniqueBackendId = `custom_${backendNodeId}`;
+ if (seenUniqueIds.has(uniqueBackendId)) {
+ return null;
+ }
+
+ let id = '';
+ if (uniqueBackendNodeIdToMcpId.has(uniqueBackendId)) {
+ id = uniqueBackendNodeIdToMcpId.get(uniqueBackendId)!;
+ } else {
+ id = `${snapshotId}_${idCounter++}`;
+ uniqueBackendNodeIdToMcpId.set(uniqueBackendId, id);
+ }
+ seenUniqueIds.add(uniqueBackendId);
+
+ const tagHandle = await handle.getProperty('localName');
+ const tagValue = await tagHandle.jsonValue();
+ const extraNode: TextSnapshotNode = {
+ role: tagValue,
+ id,
+ backendNodeId,
+ children: [],
+ elementHandle: async () => handle,
+ };
+ return extraNode;
+ };
+
+ const findAncestorNode = async (
+ handle: ElementHandle,
+ ): Promise => {
+ let ancestorHandle = await handle.evaluateHandle(el => el.parentElement);
+
+ while (ancestorHandle) {
+ const ancestorElement = ancestorHandle.asElement();
+ if (!ancestorElement) {
+ await ancestorHandle.dispose();
+ return null;
+ }
+
+ const ancestorBackendId = await ancestorElement.backendNodeId();
+ if (ancestorBackendId) {
+ const ancestorNode = idToNode
+ .values()
+ .find(node => node.backendNodeId === ancestorBackendId);
+ if (ancestorNode) {
+ await ancestorHandle.dispose();
+ return ancestorNode;
+ }
+ }
+
+ const nextHandle = await ancestorElement.evaluateHandle(
+ el => el.parentElement,
+ );
+ await ancestorHandle.dispose();
+ ancestorHandle = nextHandle;
+ }
+ return null;
+ };
+
+ const findDescendantNodes = async (
+ backendNodeId: number,
+ ): Promise> => {
+ const descendantIds = new Set();
+ try {
+ // @ts-expect-error internal API
+ const client = page.pptrPage._client();
+ if (client) {
+ const {node}: {node: Protocol.DOM.Node} = await client.send(
+ 'DOM.describeNode',
+ {
+ backendNodeId,
+ depth: -1,
+ pierce: true,
+ },
+ );
+ const collect = (node: Protocol.DOM.Node) => {
+ if (node.backendNodeId && node.backendNodeId !== backendNodeId) {
+ descendantIds.add(node.backendNodeId);
+ }
+ if (node.children) {
+ for (const child of node.children) {
+ collect(child);
+ }
+ }
+ };
+ collect(node);
+ }
+ } catch (e) {
+ this.logger(
+ `Failed to collect descendants for backend node ${backendNodeId}`,
+ e,
+ );
+ }
+ return descendantIds;
+ };
+
+ const moveChildNodes = (
+ attachTarget: TextSnapshotNode,
+ extraNode: TextSnapshotNode,
+ descendantIds: Set,
+ ): number => {
+ let firstMovedIndex = -1;
+ if (descendantIds.size > 0 && attachTarget.children) {
+ const remainingChildren: TextSnapshotNode[] = [];
+ for (const child of attachTarget.children) {
+ if (child.backendNodeId && descendantIds.has(child.backendNodeId)) {
+ if (firstMovedIndex === -1) {
+ firstMovedIndex = remainingChildren.length;
+ }
+ extraNode.children.push(child);
+ } else {
+ remainingChildren.push(child);
+ }
+ }
+ attachTarget.children = remainingChildren;
+ }
+ return firstMovedIndex !== -1
+ ? firstMovedIndex
+ : attachTarget.children
+ ? attachTarget.children.length
+ : 0;
+ };
+
+ if (extraHandles) {
+ page.setExtraHandles(extraHandles);
+ }
+ for (const handle of page.getExtraHandles() ?? []) {
+ const extraNode = await createExtraNode(handle);
+ if (!extraNode) {
+ continue;
+ }
+ idToNode.set(extraNode.id, extraNode);
+ const attachTarget = (await findAncestorNode(handle)) || rootNodeWithId;
+ const descendantIds = await findDescendantNodes(extraNode.backendNodeId!);
+ const index = moveChildNodes(attachTarget, extraNode, descendantIds);
+ attachTarget.children.splice(index, 0, extraNode);
+ }
+
const snapshot: TextSnapshot = {
root: rootNodeWithId,
snapshotId: String(snapshotId),
@@ -775,7 +928,7 @@ export class McpContext implements Context {
hasSelectedElement: false,
verbose,
};
- page.textSnapshot = snapshot;
+ page.setSnapshot(snapshot);
const data = devtoolsData ?? (await this.getDevToolsData(page));
if (data?.cdpBackendNodeId) {
snapshot.hasSelectedElement = true;
diff --git a/src/McpPage.ts b/src/McpPage.ts
index 1e311bc62..f31dd0b8d 100644
--- a/src/McpPage.ts
+++ b/src/McpPage.ts
@@ -39,6 +39,7 @@ export class McpPage implements ContextPage {
// Snapshot
textSnapshot: TextSnapshot | null = null;
uniqueBackendNodeIdToMcpId = new Map();
+ extraHandles?: ElementHandle[];
// Emulation
emulationSettings: EmulationSettings = {};
@@ -159,4 +160,20 @@ export class McpPage implements ContextPage {
getAXNodeByUid(uid: string) {
return this.textSnapshot?.idToNode.get(uid);
}
+
+ getSnapshot(): TextSnapshot | null {
+ return this.textSnapshot;
+ }
+
+ setSnapshot(snapshot: TextSnapshot): void {
+ this.textSnapshot = snapshot;
+ }
+
+ getExtraHandles(): ElementHandle[] | undefined {
+ return this.extraHandles;
+ }
+
+ setExtraHandles(extraHandles: ElementHandle[]): void {
+ this.extraHandles = extraHandles;
+ }
}
diff --git a/src/tools/ToolDefinition.ts b/src/tools/ToolDefinition.ts
index 8001e18c7..2d1b52e58 100644
--- a/src/tools/ToolDefinition.ts
+++ b/src/tools/ToolDefinition.ts
@@ -19,6 +19,8 @@ import type {
TextSnapshotNode,
GeolocationOptions,
ExtensionServiceWorker,
+ TextSnapshot,
+ EmulationSettings,
} from '../types.js';
import type {InstalledExtension} from '../utils/ExtensionRegistry.js';
import type {PaginationOptions} from '../utils/types.js';
@@ -194,6 +196,16 @@ export type Context = Readonly<{
triggerExtensionAction(id: string): Promise;
listExtensions(): InstalledExtension[];
getExtension(id: string): InstalledExtension | undefined;
+ resolveCdpElementId(
+ page: ContextPage,
+ cdpBackendNodeId: number,
+ ): string | undefined;
+ createTextSnapshot(
+ page: ContextPage,
+ verbose: boolean,
+ devtoolsData: DevToolsData | undefined,
+ extraHandles?: ElementHandle[],
+ ): Promise;
getSelectedMcpPage(): McpPage;
getExtensionServiceWorkers(): ExtensionServiceWorker[];
getExtensionServiceWorkerId(
@@ -213,6 +225,12 @@ export type ContextPage = Readonly<{
options?: {timeout?: number},
): Promise;
getInPageTools(): ToolGroup | undefined;
+ getSnapshot(): TextSnapshot | null;
+ setSnapshot(snapshot: TextSnapshot): void;
+ getExtraHandles(): ElementHandle[] | undefined;
+ setExtraHandles(extraHandles: ElementHandle[]): void;
+ readonly uniqueBackendNodeIdToMcpId: Map;
+ readonly emulationSettings: EmulationSettings;
}>;
export function defineTool(
diff --git a/src/tools/inPage.ts b/src/tools/inPage.ts
index 0cb1b6ba7..239c952ed 100644
--- a/src/tools/inPage.ts
+++ b/src/tools/inPage.ts
@@ -4,6 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
+import {logger} from '../logger.js';
import {
zod,
ajv,
@@ -32,6 +33,7 @@ declare global {
toolGroup?: ToolGroup<
ToolDefinition & {execute: (args: Record) => unknown}
>;
+ stashedElements?: Element[];
executeTool?: (
toolName: string,
args: Record,
@@ -75,7 +77,7 @@ export const executeInPageTool = definePageTool({
.optional()
.describe('The JSON-stringified parameters to pass to the tool'),
},
- handler: async (request, response) => {
+ handler: async (request, response, context) => {
const toolName = request.params.toolName;
let params: Record = {};
if (request.params.params) {
@@ -141,14 +143,150 @@ export const executeInPageTool = definePageTool({
}
const toolResult = await window.__dtmcp.executeTool(name, args);
+ const stashDOMElement = (el: Element) => {
+ if (!window.__dtmcp) {
+ window.__dtmcp = {};
+ }
+ if (window.__dtmcp.stashedElements === undefined) {
+ window.__dtmcp.stashedElements = [];
+ }
+ window.__dtmcp.stashedElements.push(el);
+ return {
+ stashedId: `stashed-${window.__dtmcp.stashedElements.length - 1}`,
+ };
+ };
+
+ // Recursively walks the tool result:
+ // - Replaces DOM elements with UIDs
+ // - Replaces functions with the string 'function'
+ // - Removes items which are deeper than cutoff
+ const processToolResult = (
+ data: unknown,
+ depth = 0,
+ cutoff = 8,
+ ): unknown => {
+ if (depth >= cutoff) {
+ return undefined;
+ }
+
+ // 1. Handle DOM Elements
+ if (data instanceof Element) {
+ return stashDOMElement(data);
+ }
+
+ // 2. Handle Arrays
+ if (Array.isArray(data)) {
+ if (depth >= cutoff - 1) {
+ return [];
+ }
+ return data.map((item: unknown) =>
+ processToolResult(item, depth + 1, cutoff),
+ );
+ }
+
+ // 3. Handle Objects
+ if (data !== null && typeof data === 'object') {
+ const processedObj: Record = {};
+ if (depth < cutoff - 1) {
+ for (const [key, value] of Object.entries(data)) {
+ processedObj[key] = processToolResult(value, depth + 1, cutoff);
+ }
+ }
+ return processedObj;
+ }
+
+ // 4. Handle Functions
+ if (typeof data === 'function') {
+ return 'function';
+ }
+
+ // 5. Return primitives (strings, numbers, booleans) as-is
+ return data;
+ };
+
return {
- result: toolResult,
+ result: processToolResult(toolResult),
+ stashed: window.__dtmcp?.stashedElements?.length ?? 0,
};
},
toolName,
params,
...handles,
);
- response.appendResponseLine(JSON.stringify(result, null, 2));
+
+ const elementHandles: ElementHandle[] = [];
+ for (let i = 0; i < (result.stashed ?? 0); i++) {
+ const elementHandle = await request.page.pptrPage.evaluateHandle(
+ index => {
+ return window.__dtmcp?.stashedElements?.[index] ?? null;
+ },
+ i,
+ );
+ elementHandles.push(elementHandle as ElementHandle);
+ }
+ const resultWithStashedElements = result.result;
+
+ let isPageSnapshotUpdated = false;
+ const stashedToUid = async (index: number) => {
+ const backendNodeId = await elementHandles[index].backendNodeId();
+ if (!backendNodeId) {
+ logger(`No backendNodeId for stashed DOM element with index ${index}`);
+ return {uid: `stashed-${index}`};
+ }
+ let cdpElementId = context.resolveCdpElementId(
+ request.page,
+ backendNodeId,
+ );
+ if (!cdpElementId) {
+ await context.createTextSnapshot(
+ request.page,
+ false,
+ undefined,
+ elementHandles,
+ );
+ isPageSnapshotUpdated = true;
+ cdpElementId = context.resolveCdpElementId(request.page, backendNodeId);
+ }
+ if (!cdpElementId) {
+ logger(`Could not get cdpElementId for backend node ${backendNodeId}`);
+ return {uid: `stashed-${index}`};
+ }
+ return {uid: cdpElementId};
+ };
+
+ const recursivelyReplaceStashedElements = async (
+ node: unknown,
+ ): Promise => {
+ if (Array.isArray(node)) {
+ return await Promise.all(
+ node.map(async x => await recursivelyReplaceStashedElements(x)),
+ );
+ }
+ if (node !== null && typeof node === 'object') {
+ if (
+ 'stashedId' in node &&
+ typeof node.stashedId === 'string' &&
+ node.stashedId.startsWith('stashed-') &&
+ Object.keys(node).length === 1
+ ) {
+ const index = parseInt(node.stashedId.split('-')[1]);
+ return stashedToUid(index);
+ }
+ const resultObj: Record = {};
+ for (const [key, value] of Object.entries(node)) {
+ resultObj[key] = await recursivelyReplaceStashedElements(value);
+ }
+ return resultObj;
+ }
+ return node;
+ };
+
+ const resultWithUids = await recursivelyReplaceStashedElements(
+ resultWithStashedElements,
+ );
+ response.appendResponseLine(JSON.stringify(resultWithUids, null, 2));
+ if (isPageSnapshotUpdated) {
+ response.includeSnapshot();
+ }
},
});
diff --git a/tests/McpContext.test.ts b/tests/McpContext.test.ts
index 31a6c88b3..034ce508a 100644
--- a/tests/McpContext.test.ts
+++ b/tests/McpContext.test.ts
@@ -12,6 +12,7 @@ import sinon from 'sinon';
import {NetworkFormatter} from '../src/formatters/NetworkFormatter.js';
import type {HTTPResponse} from '../src/third_party/index.js';
import type {TraceResult} from '../src/trace-processing/parse.js';
+import type {TextSnapshotNode} from '../src/types.js';
import {getMockRequest, html, withMcpContext} from './utils.js';
@@ -208,4 +209,138 @@ describe('McpContext', () => {
fromStub.restore();
});
});
+
+ it('inserts extraHandles into the snapshot correctly', async () => {
+ await withMcpContext(async (_response, context) => {
+ const page = context.getSelectedMcpPage();
+ await page.pptrPage.setContent(html`
+
+ `);
+
+ const middleHandle = await page.pptrPage.$('#middle');
+ if (!middleHandle) {
+ throw new Error('middle element not found');
+ }
+
+ const backendNodeId = await middleHandle.backendNodeId();
+ if (!backendNodeId) {
+ throw new Error('Failed to get backendNodeId');
+ }
+
+ // Verify it is not in the snapshot by default (due to role="none")
+ await context.createTextSnapshot(page, false, undefined, []);
+ const snapshotBefore = page.getSnapshot();
+ if (!snapshotBefore) {
+ throw new Error('Snapshot not created');
+ }
+
+ let foundMiddleBefore = false;
+ for (const node of snapshotBefore.idToNode.values()) {
+ if (node.backendNodeId === backendNodeId) {
+ foundMiddleBefore = true;
+ break;
+ }
+ }
+ assert.ok(
+ !foundMiddleBefore,
+ 'Middle element should NOT be in the snapshot when not passed as extra handle',
+ );
+
+ // Now take snapshot with extra handle
+ await context.createTextSnapshot(page, false, undefined, [middleHandle]);
+
+ const snapshot = page.getSnapshot();
+ if (!snapshot) {
+ throw new Error('Snapshot not created');
+ }
+
+ // Find the extra node in idToNode
+ let extraNode: TextSnapshotNode | undefined;
+ for (const node of snapshot.idToNode.values()) {
+ if (node.backendNodeId === backendNodeId) {
+ extraNode = node;
+ break;
+ }
+ }
+
+ assert.ok(extraNode, 'Extra node should be in the snapshot');
+ assert.strictEqual(
+ extraNode.role,
+ 'div',
+ 'Extra node should have role "div"',
+ );
+
+ // Check if the child was moved to extraNode
+ const childHandle = await page.pptrPage.$('#child');
+ if (!childHandle) {
+ throw new Error('child element not found');
+ }
+ const childBackendNodeId = await childHandle.backendNodeId();
+
+ let foundChild = false;
+ for (const child of extraNode.children) {
+ if (child.backendNodeId === childBackendNodeId) {
+ foundChild = true;
+ break;
+ }
+ }
+ assert.ok(
+ foundChild,
+ 'Child node should be moved to extra node children',
+ );
+
+ // Find parent node in snapshot
+ const parentHandle = await page.pptrPage.$('#parent');
+ if (!parentHandle) {
+ throw new Error('parent element not found');
+ }
+ const parentBackendId = await parentHandle.backendNodeId();
+
+ let parentNode: TextSnapshotNode | undefined;
+ for (const node of snapshot.idToNode.values()) {
+ if (node.backendNodeId === parentBackendId) {
+ parentNode = node;
+ break;
+ }
+ }
+
+ assert.ok(parentNode, 'Parent node should be in snapshot');
+
+ // Check that child is NOT a child of parent anymore
+ let foundChildInParent = false;
+ for (const child of parentNode.children) {
+ if (child.backendNodeId === childBackendNodeId) {
+ foundChildInParent = true;
+ break;
+ }
+ }
+ assert.ok(
+ !foundChildInParent,
+ 'Child node should NOT be in parent children',
+ );
+
+ // Check that middle IS a child of parent
+ let foundMiddleInParent = false;
+ for (const child of parentNode.children) {
+ if (child.backendNodeId === backendNodeId) {
+ foundMiddleInParent = true;
+ break;
+ }
+ }
+ assert.ok(
+ foundMiddleInParent,
+ 'Middle node should be in parent children',
+ );
+ });
+ });
});
diff --git a/tests/tools/inPage.test.ts b/tests/tools/inPage.test.ts
index d87bbea83..ffdad8da1 100644
--- a/tests/tools/inPage.test.ts
+++ b/tests/tools/inPage.test.ts
@@ -7,6 +7,8 @@
import assert from 'node:assert';
import {describe, it} from 'node:test';
+import sinon from 'sinon';
+
import type {ParsedArguments} from '../../src/bin/chrome-devtools-mcp-cli-options.js';
import type {McpContext} from '../../src/McpContext.js';
import type {McpResponse} from '../../src/McpResponse.js';
@@ -208,7 +210,7 @@ describe('inPage', () => {
);
assert.strictEqual(
response.responseLines[0],
- JSON.stringify({result: 'result'}, null, 2),
+ JSON.stringify('result', null, 2),
);
},
undefined,
@@ -340,7 +342,7 @@ describe('inPage', () => {
);
assert.strictEqual(
response.responseLines[0],
- JSON.stringify({result: {foo: 'bar'}}, null, 2),
+ JSON.stringify({foo: 'bar'}, null, 2),
);
},
undefined,
@@ -428,11 +430,9 @@ describe('inPage', () => {
response.responseLines[0],
JSON.stringify(
{
- result: {
- isElement: true,
- tagName: 'DIV',
- id: 'test-id',
- },
+ isElement: true,
+ tagName: 'DIV',
+ id: 'test-id',
},
null,
2,
@@ -440,5 +440,234 @@ describe('inPage', () => {
);
});
});
+
+ it('processToolResult replaces functions with "function"', async () => {
+ await withMcpContext(
+ async (response, context) => {
+ await setupInPageTools(response, context, () => {
+ window.__dtmcp = {
+ toolGroup: {
+ name: 'test-group',
+ description: 'test description',
+ tools: [
+ {
+ name: 'test-tool',
+ description: 'test tool description',
+ inputSchema: {},
+ execute: () => ({
+ foo: 'bar',
+ func: () => undefined,
+ }),
+ },
+ ],
+ },
+ };
+ window.addEventListener('devtoolstooldiscovery', (e: Event) => {
+ // @ts-expect-error Event has `respondWith`
+ e.respondWith(window.__dtmcp?.toolGroup);
+ });
+ });
+
+ await executeInPageTool.handler(
+ {
+ params: {
+ toolName: 'test-tool',
+ params: JSON.stringify({}),
+ },
+ page: context.getSelectedMcpPage(),
+ },
+ response,
+ context,
+ );
+ assert.strictEqual(
+ response.responseLines[0],
+ JSON.stringify({foo: 'bar', func: 'function'}, null, 2),
+ );
+ },
+ undefined,
+ {categoryInPageTools: true} as ParsedArguments,
+ );
+ });
+
+ it('processToolResult respects cutoff depth', async () => {
+ await withMcpContext(
+ async (response, context) => {
+ await setupInPageTools(response, context, () => {
+ window.__dtmcp = {
+ toolGroup: {
+ name: 'test-group',
+ description: 'test description',
+ tools: [
+ {
+ name: 'test-tool',
+ description: 'test tool description',
+ inputSchema: {},
+ execute: () => {
+ const obj: Record = {};
+ let current = obj;
+ for (let i = 0; i < 10; i++) {
+ current.nested = {};
+ current = current.nested as Record;
+ }
+ return obj;
+ },
+ },
+ ],
+ },
+ };
+ window.addEventListener('devtoolstooldiscovery', (e: Event) => {
+ // @ts-expect-error Event has `respondWith`
+ e.respondWith(window.__dtmcp?.toolGroup);
+ });
+ });
+
+ await executeInPageTool.handler(
+ {
+ params: {
+ toolName: 'test-tool',
+ params: JSON.stringify({}),
+ },
+ page: context.getSelectedMcpPage(),
+ },
+ response,
+ context,
+ );
+
+ const parsedResult = JSON.parse(response.responseLines[0]);
+ let current = parsedResult.result;
+ let depth = 0;
+ while (current && Object.keys(current).length > 0) {
+ current = current.nested;
+ depth++;
+ }
+ assert.ok(depth <= 7, `Expected depth to be <= 7, got ${depth}`);
+ },
+ undefined,
+ {categoryInPageTools: true} as ParsedArguments,
+ );
+ });
+
+ it('stashDOMElement stashes elements and returns UID', async () => {
+ await withMcpContext(
+ async (response, context) => {
+ const page = await context.newPage();
+ response.setPage(page);
+
+ page.inPageTools = {
+ name: 'test-group',
+ description: 'test description',
+ tools: [
+ {
+ name: 'test-tool',
+ description: 'test tool description',
+ inputSchema: {},
+ },
+ ],
+ };
+
+ await page.pptrPage.evaluate(() => {
+ window.__dtmcp = {
+ executeTool: async () => {
+ const div = document.createElement('div');
+ div.id = 'test-element';
+ document.body.appendChild(div);
+ return div;
+ },
+ };
+ });
+
+ const stub = sinon
+ .stub(context, 'resolveCdpElementId')
+ .returns('mock-uid');
+
+ await executeInPageTool.handler(
+ {
+ params: {
+ toolName: 'test-tool',
+ params: JSON.stringify({}),
+ },
+ page: page,
+ },
+ response,
+ context,
+ );
+
+ assert.strictEqual(
+ response.responseLines[0],
+ JSON.stringify({uid: 'mock-uid'}, null, 2),
+ );
+
+ stub.restore();
+ },
+ undefined,
+ {categoryInPageTools: true} as ParsedArguments,
+ );
+ });
+
+ it('creates a new snapshot if the stashed ID cannot be mapped to a UID initially', async () => {
+ await withMcpContext(
+ async (response, context) => {
+ const page = await context.newPage();
+ response.setPage(page);
+
+ page.inPageTools = {
+ name: 'test-group',
+ description: 'test description',
+ tools: [
+ {
+ name: 'test-tool',
+ description: 'test tool description',
+ inputSchema: {},
+ },
+ ],
+ };
+
+ await page.pptrPage.evaluate(() => {
+ window.__dtmcp = {
+ executeTool: async () => {
+ const div = document.createElement('div');
+ div.id = 'test-element';
+ document.body.appendChild(div);
+ return div;
+ },
+ };
+ });
+
+ const stubResolve = sinon.stub(context, 'resolveCdpElementId');
+ stubResolve.onFirstCall().returns(undefined);
+ stubResolve.onSecondCall().returns('mock-uid');
+
+ const stubSnapshot = sinon
+ .stub(context, 'createTextSnapshot')
+ .resolves();
+
+ await executeInPageTool.handler(
+ {
+ params: {
+ toolName: 'test-tool',
+ params: JSON.stringify({}),
+ },
+ page: page,
+ },
+ response,
+ context,
+ );
+
+ assert.ok(
+ stubSnapshot.calledOnce,
+ 'Expected createTextSnapshot to be called',
+ );
+ assert.strictEqual(
+ response.responseLines[0],
+ JSON.stringify({uid: 'mock-uid'}, null, 2),
+ );
+
+ stubResolve.restore();
+ stubSnapshot.restore();
+ },
+ undefined,
+ {categoryInPageTools: true} as ParsedArguments,
+ );
+ });
});
});
From 864287e84f70245c03b01d854c77df4beac7d153 Mon Sep 17 00:00:00 2001
From: Wolfgang Beyer
Date: Thu, 16 Apr 2026 11:30:24 +0000
Subject: [PATCH 02/26] circle detection, replace instances with string
representation
---
src/tools/inPage.ts | 41 +++++++++++++++++++++++------------------
1 file changed, 23 insertions(+), 18 deletions(-)
diff --git a/src/tools/inPage.ts b/src/tools/inPage.ts
index 239c952ed..ecae43a17 100644
--- a/src/tools/inPage.ts
+++ b/src/tools/inPage.ts
@@ -156,19 +156,16 @@ export const executeInPageTool = definePageTool({
};
};
+ const ancestors: unknown[] = [];
// Recursively walks the tool result:
- // - Replaces DOM elements with UIDs
- // - Replaces functions with the string 'function'
- // - Removes items which are deeper than cutoff
+ // - Replaces DOM elements with an ID and stashes the DOM element on the window object
+ // - Replaces non-plain-objects with a string representation of the object
+ // - Replaces circular references with the string ''
+ // - Replaces functions with the string ''
const processToolResult = (
data: unknown,
- depth = 0,
- cutoff = 8,
+ parentEl?: unknown,
): unknown => {
- if (depth >= cutoff) {
- return undefined;
- }
-
// 1. Handle DOM Elements
if (data instanceof Element) {
return stashDOMElement(data);
@@ -176,28 +173,36 @@ export const executeInPageTool = definePageTool({
// 2. Handle Arrays
if (Array.isArray(data)) {
- if (depth >= cutoff - 1) {
- return [];
- }
return data.map((item: unknown) =>
- processToolResult(item, depth + 1, cutoff),
+ processToolResult(item, parentEl),
);
}
// 3. Handle Objects
if (data !== null && typeof data === 'object') {
+ while (ancestors.length > 0 && ancestors.at(-1) !== parentEl) {
+ ancestors.pop();
+ }
+ if (ancestors.includes(data)) {
+ return '';
+ }
+ ancestors.push(data);
+
+ // If not a plain object, return a string representation of the object
+ if (Object.getPrototypeOf(data) !== Object.prototype) {
+ return `<${data.constructor.name} instance>`;
+ }
+
const processedObj: Record = {};
- if (depth < cutoff - 1) {
- for (const [key, value] of Object.entries(data)) {
- processedObj[key] = processToolResult(value, depth + 1, cutoff);
- }
+ for (const [key, value] of Object.entries(data)) {
+ processedObj[key] = processToolResult(value, data);
}
return processedObj;
}
// 4. Handle Functions
if (typeof data === 'function') {
- return 'function';
+ return '';
}
// 5. Return primitives (strings, numbers, booleans) as-is
From 93769cb2a739f1a43d8e3eeea4c7747147ece5b9 Mon Sep 17 00:00:00 2001
From: Wolfgang Beyer
Date: Thu, 16 Apr 2026 11:36:30 +0000
Subject: [PATCH 03/26] update tests
---
src/tools/inPage.ts | 2 +-
tests/tools/inPage.test.ts | 80 ++++++++++++++++++++++++++++++--------
2 files changed, 64 insertions(+), 18 deletions(-)
diff --git a/src/tools/inPage.ts b/src/tools/inPage.ts
index ecae43a17..073c0067b 100644
--- a/src/tools/inPage.ts
+++ b/src/tools/inPage.ts
@@ -159,7 +159,7 @@ export const executeInPageTool = definePageTool({
const ancestors: unknown[] = [];
// Recursively walks the tool result:
// - Replaces DOM elements with an ID and stashes the DOM element on the window object
- // - Replaces non-plain-objects with a string representation of the object
+ // - Replaces non-plain objects with a string representation of the object
// - Replaces circular references with the string ''
// - Replaces functions with the string ''
const processToolResult = (
diff --git a/tests/tools/inPage.test.ts b/tests/tools/inPage.test.ts
index ffdad8da1..225fba934 100644
--- a/tests/tools/inPage.test.ts
+++ b/tests/tools/inPage.test.ts
@@ -441,7 +441,7 @@ describe('inPage', () => {
});
});
- it('processToolResult replaces functions with "function"', async () => {
+ it('processToolResult replaces functions with ""', async () => {
await withMcpContext(
async (response, context) => {
await setupInPageTools(response, context, () => {
@@ -481,7 +481,7 @@ describe('inPage', () => {
);
assert.strictEqual(
response.responseLines[0],
- JSON.stringify({foo: 'bar', func: 'function'}, null, 2),
+ JSON.stringify({foo: 'bar', func: ''}, null, 2),
);
},
undefined,
@@ -489,7 +489,7 @@ describe('inPage', () => {
);
});
- it('processToolResult respects cutoff depth', async () => {
+ it('processToolResult replaces circular references with ""', async () => {
await withMcpContext(
async (response, context) => {
await setupInPageTools(response, context, () => {
@@ -503,12 +503,8 @@ describe('inPage', () => {
description: 'test tool description',
inputSchema: {},
execute: () => {
- const obj: Record = {};
- let current = obj;
- for (let i = 0; i < 10; i++) {
- current.nested = {};
- current = current.nested as Record;
- }
+ const obj: Record = {foo: 'bar'};
+ obj.self = obj;
return obj;
},
},
@@ -532,15 +528,65 @@ describe('inPage', () => {
response,
context,
);
+ assert.strictEqual(
+ response.responseLines[0],
+ JSON.stringify({foo: 'bar', self: ''}, null, 2),
+ );
+ },
+ undefined,
+ {categoryInPageTools: true} as ParsedArguments,
+ );
+ });
- const parsedResult = JSON.parse(response.responseLines[0]);
- let current = parsedResult.result;
- let depth = 0;
- while (current && Object.keys(current).length > 0) {
- current = current.nested;
- depth++;
- }
- assert.ok(depth <= 7, `Expected depth to be <= 7, got ${depth}`);
+ it('processToolResult replaces non-plain objects with ""', async () => {
+ await withMcpContext(
+ async (response, context) => {
+ await setupInPageTools(response, context, () => {
+ class CustomClass {
+ val = 'value';
+ }
+ window.__dtmcp = {
+ toolGroup: {
+ name: 'test-group',
+ description: 'test description',
+ tools: [
+ {
+ name: 'test-tool',
+ description: 'test tool description',
+ inputSchema: {},
+ execute: () => ({
+ foo: 'bar',
+ custom: new CustomClass(),
+ }),
+ },
+ ],
+ },
+ };
+ window.addEventListener('devtoolstooldiscovery', (e: Event) => {
+ // @ts-expect-error Event has `respondWith`
+ e.respondWith(window.__dtmcp?.toolGroup);
+ });
+ });
+
+ await executeInPageTool.handler(
+ {
+ params: {
+ toolName: 'test-tool',
+ params: JSON.stringify({}),
+ },
+ page: context.getSelectedMcpPage(),
+ },
+ response,
+ context,
+ );
+ assert.strictEqual(
+ response.responseLines[0],
+ JSON.stringify(
+ {foo: 'bar', custom: ''},
+ null,
+ 2,
+ ),
+ );
},
undefined,
{categoryInPageTools: true} as ParsedArguments,
From 5466128e8d14f9ecd59a31366e8fbdba1f6d0853 Mon Sep 17 00:00:00 2001
From: Michael Hablich
Date: Fri, 10 Apr 2026 09:12:57 +0200
Subject: [PATCH 04/26] docs: Include Mistral Vibe setup in README (#1801)
Add Mistral Vibe configuration instructions to README.
---
README.md | 15 +++++++++++++++
1 file changed, 15 insertions(+)
diff --git a/README.md b/README.md
index 6f83fe56e..41743ac90 100644
--- a/README.md
+++ b/README.md
@@ -373,6 +373,21 @@ Once connected, the Chrome DevTools MCP tools will be available in StudioAssist.
+
+ Mistral Vibe
+
+Add in ~/.vibe/config.toml:
+
+```toml
+[[mcp_servers]]
+name = "chrome-devtools"
+transport = "stdio"
+command = "npx"
+args = ["chrome-devtools-mcp@latest"]
+```
+
+
+
OpenCode
From f09de54cccb89c1c9379f53e1ff77bfb3bcb47ba Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 10 Apr 2026 09:43:28 +0000
Subject: [PATCH 05/26] chore(deps-dev): bump chrome-devtools-frontend from
1.0.1611390 to 1.0.1611825 in the bundled-devtools group (#1843)
Bumps the bundled-devtools group with 1 update:
[chrome-devtools-frontend](https://github.com/ChromeDevTools/devtools-frontend).
Updates `chrome-devtools-frontend` from 1.0.1611390 to 1.0.1611825
Commits
fae9632
Update DevTools DEPS (trusted)
dd6a009
Update target manager getter in isPrimaryPageFrame().
30b1c15
AI: pass modelVersion through from Aida => GCA translation
e813806
Align the Copied to clipboard snack bar with greenlines
04bf5ba
[AI] Update suggested prompts for LH recording
175fd5f
Focus breakpoint editor textbox on open
1d5c490
Roll browser-protocol and CfT
530d7ab
Update DevTools DEPS (trusted)
0579f6c
Add missing accessibility label to walkthrough step title
b961128
Make sure we use heading tags in AI Walkthrough where required
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore ` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore ` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore ` will
remove the ignore condition of the specified dependency and ignore
conditions
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
package-lock.json | 8 ++++----
package.json | 2 +-
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index e35394a6e..daeee2117 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -27,7 +27,7 @@
"@types/yargs": "^17.0.33",
"@typescript-eslint/eslint-plugin": "^8.43.0",
"@typescript-eslint/parser": "^8.43.0",
- "chrome-devtools-frontend": "1.0.1611390",
+ "chrome-devtools-frontend": "1.0.1611825",
"core-js": "3.49.0",
"debug": "4.4.3",
"eslint": "^9.35.0",
@@ -3452,9 +3452,9 @@
}
},
"node_modules/chrome-devtools-frontend": {
- "version": "1.0.1611390",
- "resolved": "https://registry.npmjs.org/chrome-devtools-frontend/-/chrome-devtools-frontend-1.0.1611390.tgz",
- "integrity": "sha512-hoDgtFvV0Gbg12upe+rOq4Q1dgMjVQ66a/ckOiD5bPfFiBerDFJT9mfh5JfJjqEPl3NttC/dqiY6MKT1NlE9lA==",
+ "version": "1.0.1611825",
+ "resolved": "https://registry.npmjs.org/chrome-devtools-frontend/-/chrome-devtools-frontend-1.0.1611825.tgz",
+ "integrity": "sha512-xp7EQPurkgJgYiSjIyLc3d7+BMevetrVeXHm5zEK0Zbr99/XjOlUzMnj18twLsrb/fYXYnMD4g5SjzcJkYATfQ==",
"dev": true,
"license": "BSD-3-Clause"
},
diff --git a/package.json b/package.json
index a5d253ad2..3b794054b 100644
--- a/package.json
+++ b/package.json
@@ -60,7 +60,7 @@
"@types/yargs": "^17.0.33",
"@typescript-eslint/eslint-plugin": "^8.43.0",
"@typescript-eslint/parser": "^8.43.0",
- "chrome-devtools-frontend": "1.0.1611390",
+ "chrome-devtools-frontend": "1.0.1611825",
"core-js": "3.49.0",
"debug": "4.4.3",
"eslint": "^9.35.0",
From 0a38f08e60c193d0308ec0cf81f30dc859f8e353 Mon Sep 17 00:00:00 2001
From: Alex Rudenko
Date: Fri, 10 Apr 2026 12:46:52 +0200
Subject: [PATCH 06/26] test: speed up .only (#1846)
the node test framework runs not only tests respecting
`--test-concurrency=1` but also discovery of the tests (import of the
files). This makes the `npm run test:only` slow. This PR checks ahead of
times which files have .only and only tells the node test runner to
import those.
---
scripts/test.mjs | 26 ++++++++++++++++++++++----
1 file changed, 22 insertions(+), 4 deletions(-)
diff --git a/scripts/test.mjs b/scripts/test.mjs
index 4b07d6021..5c8410069 100644
--- a/scripts/test.mjs
+++ b/scripts/test.mjs
@@ -8,6 +8,7 @@
// Node 20 does not support --experimental-strip-types flag.
import {spawn, execSync} from 'node:child_process';
+import {readFile} from 'node:fs/promises';
import path from 'node:path';
import process from 'node:process';
@@ -38,10 +39,27 @@ if (userArgs.length > 0) {
}
} else {
const isNode20 = process.version.startsWith('v20.');
- if (isNode20) {
- files.push('build/tests');
- } else {
- files.push('build/tests/**/*.test.js');
+ if (flags.includes('--test-only')) {
+ if (isNode20) {
+ throw new Error(`--test-only is not supported for Node 20`);
+ }
+ const {glob} = await import('node:fs/promises');
+ for await (const tsFile of glob('tests/**/*.test.ts')) {
+ const content = await readFile(tsFile, 'utf8');
+ if (content.includes('.only(')) {
+ files.push(path.join('build', tsFile.replace(/\.ts$/, '.js')));
+ }
+ }
+ if (files.length === 0) {
+ console.warn('no files contain .only');
+ process.exit(0);
+ }
+ } else if (files.length === 0) {
+ if (isNode20) {
+ files.push('build/tests');
+ } else {
+ files.push('build/tests/**/*.test.js');
+ }
}
}
From 301d045707857e3cae1f36784d9a5d705b6d40bb Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 13 Apr 2026 09:32:20 +0200
Subject: [PATCH 07/26] chore(deps): bump basic-ftp from 5.2.1 to 5.2.2 (#1849)
Bumps [basic-ftp](https://github.com/patrickjuchli/basic-ftp) from 5.2.1
to 5.2.2.
Release notes
Sourced from basic-ftp's
releases.
5.2.2
Changelog
Sourced from basic-ftp's
changelog.
5.2.2
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/ChromeDevTools/chrome-devtools-mcp/network/alerts).
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
package-lock.json | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index daeee2117..cd5aecf85 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3293,9 +3293,9 @@
"license": "MIT"
},
"node_modules/basic-ftp": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.1.tgz",
- "integrity": "sha512-0yaL8JdxTknKDILitVpfYfV2Ob6yb3udX/hK97M7I3jOeznBNxQPtVvTUtnhUkyHlxFWyr5Lvknmgzoc7jf+1Q==",
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.2.tgz",
+ "integrity": "sha512-1tDrzKsdCg70WGvbFss/ulVAxupNauGnOlgpyjKzeQxzyllBLS0CGLV7tjIXTK3ZQA9/FBEm9qyFFN1bciA6pw==",
"dev": true,
"license": "MIT",
"engines": {
From 8a7b85a38ea726fab51fc5383be1ff50554298ab Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 13 Apr 2026 15:03:04 +0200
Subject: [PATCH 08/26] chore(deps-dev): bump the dev-dependencies group across
1 directory with 9 updates (#1844)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps the dev-dependencies group with 9 updates in the / directory:
| Package | From | To |
| --- | --- | --- |
| [@google/genai](https://github.com/googleapis/js-genai) | `1.48.0` |
`1.49.0` |
|
[@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node)
| `25.5.0` | `25.6.0` |
|
[@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin)
| `8.58.0` | `8.58.1` |
|
[@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser)
| `8.58.0` | `8.58.1` |
| [prettier](https://github.com/prettier/prettier) | `3.8.1` | `3.8.2` |
|
[rollup-plugin-license](https://github.com/mjeanroy/rollup-plugin-license)
| `3.7.0` | `3.7.1` |
| [sinon](https://github.com/sinonjs/sinon) | `21.0.3` | `21.1.0` |
|
[@types/sinon](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/sinon)
| `21.0.0` | `21.0.1` |
|
[typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint)
| `8.58.0` | `8.58.1` |
Updates `@google/genai` from 1.48.0 to 1.49.0
Release notes
Sourced from @google/genai's
releases.
v1.49.0
1.49.0
(2026-04-08)
Features
- Introduce TYPE_L16 audio content and optional fields. (c62cb9a)
Changelog
Sourced from @google/genai's
changelog.
1.49.0
(2026-04-08)
Features
- Introduce TYPE_L16 audio content and optional fields. (c62cb9a)
Commits
9deedb7
chore(main): release 1.49.0 (#1457)
c386d0c
docs: Remove deprecated product recontext model samples from
docstrings
5ab2463
chore(interaction-api): Add Vertex AI Search and Enterprise Web Search
to Int...
227e509
chore: Remove/update tests for the deprecated product recontext
model.
850fbec
chore: Remove constrain for Vertex AI in interactions.
c62cb9a
feat: Introduce TYPE_L16 audio content and optional fields.
a5b2018
chore: Add missing modality types
4b59428
chore: pull in change from custom code
2b064e0
Copybara import of the project:
14b6203
chore: internal change
- See full diff in compare
view
Updates `@types/node` from 25.5.0 to 25.6.0
Commits
Updates `@typescript-eslint/eslint-plugin` from 8.58.0 to 8.58.1
Release notes
Sourced from @typescript-eslint/eslint-plugin's
releases.
v8.58.1
8.58.1 (2026-04-08)
🩹 Fixes
- eslint-plugin: [no-unused-vars] fix false negative
for type predicate parameter (#12004)
❤️ Thank You
See GitHub
Releases for more information.
You can read about our versioning
strategy and releases on our
website.
Changelog
Sourced from @typescript-eslint/eslint-plugin's
changelog.
8.58.1 (2026-04-08)
🩹 Fixes
- eslint-plugin: [no-unused-vars] fix false negative
for type predicate parameter (#12004)
❤️ Thank You
See GitHub
Releases for more information.
You can read about our versioning
strategy and releases on our
website.
Commits
5311ed3
chore(release): publish 8.58.1
c3f8ed5
fix(eslint-plugin): [no-unused-vars] fix false negative for type
predicate pa...
e372a66
Revert: feat(eslint-plugin): [no-unnecessary-type-arguments] report
inferred ...
- See full diff in compare
view
Updates `@typescript-eslint/parser` from 8.58.0 to 8.58.1
Release notes
Sourced from @typescript-eslint/parser's
releases.
v8.58.1
8.58.1 (2026-04-08)
🩹 Fixes
- eslint-plugin: [no-unused-vars] fix false negative
for type predicate parameter (#12004)
❤️ Thank You
See GitHub
Releases for more information.
You can read about our versioning
strategy and releases on our
website.
Changelog
Sourced from @typescript-eslint/parser's
changelog.
8.58.1 (2026-04-08)
This was a version bump only for parser to align it with other
projects, there were no code changes.
See GitHub
Releases for more information.
You can read about our versioning
strategy and releases on our
website.
Commits
Updates `prettier` from 3.8.1 to 3.8.2
Release notes
Sourced from prettier's
releases.
3.8.2
🔗 Changelog
Changelog
Sourced from prettier's
changelog.
3.8.2
diff
Exhaustive typechecking with @default never;
<!-- Input -->
@switch (foo) {
@case (1) {}
@default never;
}
<!-- Prettier 3.8.1 -->
SyntaxError: Incomplete block "default never". If you meant to
write the @ character, you should use the "@"
HTML entity instead. (3:3)
<!-- Prettier 3.8.2 -->
@switch (foo) {
@case (1) {}
@default never;
}
arrow function and instanceof
expressions.
<!-- Input -->
@let fn = (a) => a? 1:2;
{{ fn ( a instanceof b)}}
<!-- Prettier 3.8.1 -->
@let fn = (a) =>
a? 1:2;
{{ fn ( a instanceof b)}}
<!-- Prettier 3.8.2 -->
@let fn = (a) =>
(a ? 1 : 2);
{{ fn(a instanceof b) }}
Commits
Updates `rollup-plugin-license` from 3.7.0 to 3.7.1
Changelog
Sourced from rollup-plugin-license's
changelog.
3.5.0 (2024-06-22)
- release: prepare next release (71f5bcf)
- release: prepare next release (2c51c64)
- release: release version (f27f51d)
- release: release version (8a7f79c)
- chore: fix lint task (a7c455f)
- chore: remove (deprecated) eslint-config-google (0ba031f)
- chore(ci): push release to current branch (03112e9)
- chore(deps-dev): bump
@rollup/plugin-commonjs from
25.0.7 to 25.0.8 (#1732)
(8bd6fb3),
closes #1732
- chore(deps-dev): bump
@rollup/plugin-commonjs from
25.0.8 to 26.0.1 (#1741)
(25f03f2),
closes #1741
- chore(deps-dev): bump globalthis from 1.0.3 to 1.0.4 (#1721)
(54084da),
closes #1721
- chore(deps-dev): bump gulp-conventional-changelog from 4.0.0 to
5.0.0 (#1723)
(d6ae13f),
closes #1723
- chore(deps-dev): bump prettier from 3.2.5 to 3.3.0 (#1737)
(3e80b2a),
closes #1737
- chore(deps-dev): bump prettier from 3.3.0 to 3.3.1 (#1740)
(ef8aabf),
closes #1740
- chore(deps-dev): bump prettier from 3.3.1 to 3.3.2 (#1746)
(e4fbe41),
closes #1746
- chore(deps-dev): bump rimraf from 5.0.5 to 5.0.7 (#1727)
(44fd2d4),
closes #1727
- chore(deps-dev): bump rollup from 4.14.3 to 4.16.2 (#1715)
(0126778),
closes #1715
- chore(deps-dev): bump rollup from 4.16.2 to 4.16.4 (#1716)
(2256205),
closes #1716
- chore(deps-dev): bump rollup from 4.16.4 to 4.17.2 (#1722)
(084276a),
closes #1722
- chore(deps-dev): bump the babel group with 2 updates (#1720)
(6720a77),
closes #1720
- chore(deps-dev): bump the babel group with 2 updates (#1739)
(2d3d1d3),
closes #1739
- chore(deps-dev): bump the babel group with 3 updates (#1733)
(b56dc88),
closes #1733
- chore(deps-dev): bump the typescript-eslint group across 1 directory
with 2 updates (#1729)
(5cfc7c0),
closes #1729
- chore(deps-dev): bump the typescript-eslint group across 1 directory
with 2 updates (#1748)
(c0abf2d),
closes #1748
- chore(deps-dev): bump the typescript-eslint group with 2 updates (#1714)
(1deea69),
closes #1714
- chore(deps-dev): bump the typescript-eslint group with 2 updates (#1717)
(3571d80),
closes #1717
- chore(deps-dev): bump the typescript-eslint group with 2 updates (#1735)
(c3d769f),
closes #1735
- chore(deps-dev): bump the typescript-eslint group with 2 updates (#1736)
(2e053de),
closes #1736
- chore(deps-dev): bump typescript from 5.4.5 to 5.5.2 (#1750)
(374962d),
closes #1750
- --- (#1730)
(40dc5e6),
closes #1730
- Update readme (780ff50)
- feat: drop glob usage (#1742)
(2623a1b),
closes #1742
- feat: drop mkdirp (#1743)
(2f90c74),
closes #1743
- feat: include private self dependency (1401f5d)
- docs: update README & changelog (bdfca87)
3.4.0 (2024-04-18)
- release: prepare next release (1c6c911)
- release: release version (a576572)
- chore: add changelog update workflow (5f4ed57)
- chore: update changelog (a48e164)
- chore: update readme (8254eae)
- chore(ci): add node 21 (80cefa0)
- chore(ci): remove invalid option (16e4d5d)
- chore(ci): update actions/checkout to version 4.1.2 (9742e59)
- chore(ci): use node 20 (184cc0b)
... (truncated)
Commits
6faf8f5
release: release version
9b28938
chore(ci): add provenance
23fd98e
chore: update npmignore
4d9fc6f
chore(ci): use --no-git-checks in publish task
e01dc36
chore(ci): update publish task
fd0f645
chore(ci): fix publish task
3f298e6
fix: fix lint
348e933
Fix bug when a package license type is string (#2108)
3fc6424
chore: enable pnpm trustPolicy
ccc4626
chore: enable pnpm blockExoticSubdeps
- Additional commits viewable in compare
view
Updates `sinon` from 21.0.3 to 21.1.0
Changelog
Sourced from sinon's
changelog.
21.1.0
0a5526c5
updated deps (Carl-Erik Kopseng)
5262204f
fix: build artifacts before running bundled tests (Carl-Erik
Kopseng)
819bb64b
Migration to ECMAScript modules (ESM) (#2683)
(Carl-Erik Kopseng)
This allowed us to finally consume ESM-only dependencies and has
broken us free from some CJS shackes. Now produce the same API surface
for CJS consumers, as well, by generating ./lib
- Modern ignores 😁
- test: add distribution harness
- test: verify packed cjs and esm entrypoints
- test: lock distribution api manifest
- test: smoke test built pkg artifacts
- docs: require contract tests for package migration
- test: guard esm migration regressions
- docs: require contract gate for esm migration
- build: generate cjs lib from esm source entries
- refactor: port root api surface to esm
- build: clean port of root api to esm
- docs: include implementation plans
- fix: align lint and smoke tests with esm migration
- refactor: complete esm port of all core components
- refactor: finalize esm migration with sandbox and naming fixes
- fix: finish esm migration stabilization
- chore: stop tracking generated lib output
- remove plans
- prettier
- linting
- fix: make distribution tests self-contained
- fix: build before coverage test bundle
- refactor: move simple unit tests to src
- refactor: flatten test and coverage script chains
- refactor: use parallel mocha for node tests
- test: restore fake timers cleanup
- refactor: remove node test runner script
- remove unneccessary clutter
- fix: make mocha watch use polling
- simplify
- Increase coverage
- Fix coverage by removing duplicated tests
These were covering the generated lib/ folder.
- Move shared util into esm dir
- fix package dep issues
- Adjust coverage
- Upgrade all dependencies
... (truncated)
Commits
Updates `@types/sinon` from 21.0.0 to 21.0.1
Commits
Updates `typescript-eslint` from 8.58.0 to 8.58.1
Release notes
Sourced from typescript-eslint's
releases.
v8.58.1
8.58.1 (2026-04-08)
🩹 Fixes
- eslint-plugin: [no-unused-vars] fix false negative
for type predicate parameter (#12004)
❤️ Thank You
See GitHub
Releases for more information.
You can read about our versioning
strategy and releases on our
website.
Changelog
Sourced from typescript-eslint's
changelog.
8.58.1 (2026-04-08)
This was a version bump only for typescript-eslint to align it with
other projects, there were no code changes.
See GitHub
Releases for more information.
You can read about our versioning
strategy and releases on our
website.
Commits
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
package-lock.json | 189 +++++++++++++++++++++++-----------------------
1 file changed, 94 insertions(+), 95 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index cd5aecf85..5aefa8c7b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -381,9 +381,9 @@
}
},
"node_modules/@google/genai": {
- "version": "1.48.0",
- "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.48.0.tgz",
- "integrity": "sha512-plonYK4ML2PrxsRD9SeqmFt76eREWkQdPCglOA6aYDzL1AAbE+7PUnT54SvpWGfws13L0AZEqGSpL7+1IPnTxQ==",
+ "version": "1.49.0",
+ "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.49.0.tgz",
+ "integrity": "sha512-hO69Zl0H3x+L0KL4stl1pLYgnqnwHoLqtKy6MRlNnW8TAxjqMdOUVafomKd4z1BePkzoxJWbYILny9a2Zk43VQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -2004,9 +2004,9 @@
}
},
"node_modules/@sinonjs/fake-timers": {
- "version": "15.1.1",
- "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.1.1.tgz",
- "integrity": "sha512-cO5W33JgAPbOh07tvZjUOJ7oWhtaqGHiZw+11DPbyqh2kHTBc3eF/CjJDeQ4205RLQsX6rxCuYOroFQwl7JDRw==",
+ "version": "15.3.2",
+ "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.3.2.tgz",
+ "integrity": "sha512-mrn35Jl2pCpns+mE3HaZa1yPN5EYCRgiMI+135COjr2hr8Cls9DXqIZ57vZe2cz7y2XVSq92tcs6kGQcT1J8Rw==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -2014,9 +2014,9 @@
}
},
"node_modules/@sinonjs/samsam": {
- "version": "9.0.3",
- "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-9.0.3.tgz",
- "integrity": "sha512-ZgYY7Dc2RW+OUdnZ1DEHg00lhRt+9BjymPKHog4PRFzr1U3MbK57+djmscWyKxzO1qfunHqs4N45WWyKIFKpiQ==",
+ "version": "10.0.2",
+ "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-10.0.2.tgz",
+ "integrity": "sha512-8lVwD1Df1BmzoaOLhMcGGcz/Jyr5QY2KSB75/YK1QgKzoabTeLdIVyhXNZK9ojfSKSdirbXqdbsXXqP9/Ve8+A==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -2149,13 +2149,13 @@
}
},
"node_modules/@types/node": {
- "version": "25.5.0",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz",
- "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==",
+ "version": "25.6.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
+ "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "undici-types": "~7.18.0"
+ "undici-types": "~7.19.0"
}
},
"node_modules/@types/pg": {
@@ -2202,9 +2202,9 @@
"license": "MIT"
},
"node_modules/@types/sinon": {
- "version": "21.0.0",
- "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-21.0.0.tgz",
- "integrity": "sha512-+oHKZ0lTI+WVLxx1IbJDNmReQaIsQJjN2e7UUrJHEeByG7bFeKJYsv1E75JxTQ9QKJDp21bAa/0W2Xo4srsDnw==",
+ "version": "21.0.1",
+ "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-21.0.1.tgz",
+ "integrity": "sha512-5yoJSqLbjH8T9V2bksgRayuhpZy+723/z6wBOR+Soe4ZlXC0eW8Na71TeaZPUWDQvM7LYKa9UGFc6LRqxiR5fQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2257,17 +2257,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
- "version": "8.58.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.0.tgz",
- "integrity": "sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==",
+ "version": "8.58.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.1.tgz",
+ "integrity": "sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
- "@typescript-eslint/scope-manager": "8.58.0",
- "@typescript-eslint/type-utils": "8.58.0",
- "@typescript-eslint/utils": "8.58.0",
- "@typescript-eslint/visitor-keys": "8.58.0",
+ "@typescript-eslint/scope-manager": "8.58.1",
+ "@typescript-eslint/type-utils": "8.58.1",
+ "@typescript-eslint/utils": "8.58.1",
+ "@typescript-eslint/visitor-keys": "8.58.1",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.5.0"
@@ -2286,16 +2286,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
- "version": "8.58.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.0.tgz",
- "integrity": "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==",
+ "version": "8.58.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.1.tgz",
+ "integrity": "sha512-gGkiNMPqerb2cJSVcruigx9eHBlLG14fSdPdqMoOcBfh+vvn4iCq2C8MzUB89PrxOXk0y3GZ1yIWb9aOzL93bw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/scope-manager": "8.58.0",
- "@typescript-eslint/types": "8.58.0",
- "@typescript-eslint/typescript-estree": "8.58.0",
- "@typescript-eslint/visitor-keys": "8.58.0",
+ "@typescript-eslint/scope-manager": "8.58.1",
+ "@typescript-eslint/types": "8.58.1",
+ "@typescript-eslint/typescript-estree": "8.58.1",
+ "@typescript-eslint/visitor-keys": "8.58.1",
"debug": "^4.4.3"
},
"engines": {
@@ -2311,14 +2311,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
- "version": "8.58.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.0.tgz",
- "integrity": "sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg==",
+ "version": "8.58.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.1.tgz",
+ "integrity": "sha512-gfQ8fk6cxhtptek+/8ZIqw8YrRW5048Gug8Ts5IYcMLCw18iUgrZAEY/D7s4hkI0FxEfGakKuPK/XUMPzPxi5g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/tsconfig-utils": "^8.58.0",
- "@typescript-eslint/types": "^8.58.0",
+ "@typescript-eslint/tsconfig-utils": "^8.58.1",
+ "@typescript-eslint/types": "^8.58.1",
"debug": "^4.4.3"
},
"engines": {
@@ -2333,14 +2333,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
- "version": "8.58.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.0.tgz",
- "integrity": "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ==",
+ "version": "8.58.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.1.tgz",
+ "integrity": "sha512-TPYUEqJK6avLcEjumWsIuTpuYODTTDAtoMdt8ZZa93uWMTX13Nb8L5leSje1NluammvU+oI3QRr5lLXPgihX3w==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.58.0",
- "@typescript-eslint/visitor-keys": "8.58.0"
+ "@typescript-eslint/types": "8.58.1",
+ "@typescript-eslint/visitor-keys": "8.58.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2351,9 +2351,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
- "version": "8.58.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.0.tgz",
- "integrity": "sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A==",
+ "version": "8.58.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.1.tgz",
+ "integrity": "sha512-JAr2hOIct2Q+qk3G+8YFfqkqi7sC86uNryT+2i5HzMa2MPjw4qNFvtjnw1IiA1rP7QhNKVe21mSSLaSjwA1Olw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2368,15 +2368,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
- "version": "8.58.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.0.tgz",
- "integrity": "sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg==",
+ "version": "8.58.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.1.tgz",
+ "integrity": "sha512-HUFxvTJVroT+0rXVJC7eD5zol6ID+Sn5npVPWoFuHGg9Ncq5Q4EYstqR+UOqaNRFXi5TYkpXXkLhoCHe3G0+7w==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.58.0",
- "@typescript-eslint/typescript-estree": "8.58.0",
- "@typescript-eslint/utils": "8.58.0",
+ "@typescript-eslint/types": "8.58.1",
+ "@typescript-eslint/typescript-estree": "8.58.1",
+ "@typescript-eslint/utils": "8.58.1",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
},
@@ -2393,9 +2393,9 @@
}
},
"node_modules/@typescript-eslint/types": {
- "version": "8.58.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.0.tgz",
- "integrity": "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==",
+ "version": "8.58.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.1.tgz",
+ "integrity": "sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2407,16 +2407,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
- "version": "8.58.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.0.tgz",
- "integrity": "sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA==",
+ "version": "8.58.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.1.tgz",
+ "integrity": "sha512-w4w7WR7GHOjqqPnvAYbazq+Y5oS68b9CzasGtnd6jIeOIeKUzYzupGTB2T4LTPSv4d+WPeccbxuneTFHYgAAWg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/project-service": "8.58.0",
- "@typescript-eslint/tsconfig-utils": "8.58.0",
- "@typescript-eslint/types": "8.58.0",
- "@typescript-eslint/visitor-keys": "8.58.0",
+ "@typescript-eslint/project-service": "8.58.1",
+ "@typescript-eslint/tsconfig-utils": "8.58.1",
+ "@typescript-eslint/types": "8.58.1",
+ "@typescript-eslint/visitor-keys": "8.58.1",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@@ -2474,16 +2474,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
- "version": "8.58.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.0.tgz",
- "integrity": "sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA==",
+ "version": "8.58.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.1.tgz",
+ "integrity": "sha512-Ln8R0tmWC7pTtLOzgJzYTXSCjJ9rDNHAqTaVONF4FEi2qwce8mD9iSOxOpLFFvWp/wBFlew0mjM1L1ihYWfBdQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
- "@typescript-eslint/scope-manager": "8.58.0",
- "@typescript-eslint/types": "8.58.0",
- "@typescript-eslint/typescript-estree": "8.58.0"
+ "@typescript-eslint/scope-manager": "8.58.1",
+ "@typescript-eslint/types": "8.58.1",
+ "@typescript-eslint/typescript-estree": "8.58.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2498,13 +2498,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
- "version": "8.58.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.0.tgz",
- "integrity": "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ==",
+ "version": "8.58.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.1.tgz",
+ "integrity": "sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.58.0",
+ "@typescript-eslint/types": "8.58.1",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
@@ -3877,9 +3877,9 @@
"license": "BSD-3-Clause"
},
"node_modules/diff": {
- "version": "8.0.3",
- "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz",
- "integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==",
+ "version": "8.0.4",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz",
+ "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
@@ -7364,9 +7364,9 @@
}
},
"node_modules/prettier": {
- "version": "3.8.1",
- "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
- "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
+ "version": "3.8.2",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.2.tgz",
+ "integrity": "sha512-8c3mgTe0ASwWAJK+78dpviD+A8EqhndQPUBpNUIPt6+xWlIigCwfN01lWr9MAede4uqXGTEKeQWTvzb3vjia0Q==",
"dev": true,
"license": "MIT",
"bin": {
@@ -7778,9 +7778,9 @@
}
},
"node_modules/rollup-plugin-license": {
- "version": "3.7.0",
- "resolved": "https://registry.npmjs.org/rollup-plugin-license/-/rollup-plugin-license-3.7.0.tgz",
- "integrity": "sha512-RvvOIF+GH3fBR3wffgc/vmjQn6qOn72WjppWVDp/v+CLpT0BbcRBdSkPeeIOL6U5XccdYgSIMjUyXgxlKEEFcw==",
+ "version": "3.7.1",
+ "resolved": "https://registry.npmjs.org/rollup-plugin-license/-/rollup-plugin-license-3.7.1.tgz",
+ "integrity": "sha512-FcGXUbAmPvRSLxjVdjp/r/MUtKBlttVQd+ApUyvKfREnsoAfAZA6Ic2fE1Tz4RL0f9XqEQU9UIRNUMdtQtliDw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -8153,17 +8153,16 @@
}
},
"node_modules/sinon": {
- "version": "21.0.3",
- "resolved": "https://registry.npmjs.org/sinon/-/sinon-21.0.3.tgz",
- "integrity": "sha512-0x8TQFr8EjADhSME01u1ZK31yv2+bd6Z5NrBCHVM+n4qL1wFqbxftmeyi3bwlr49FbbzRfrqSFOpyHCOh/YmYA==",
+ "version": "21.1.2",
+ "resolved": "https://registry.npmjs.org/sinon/-/sinon-21.1.2.tgz",
+ "integrity": "sha512-FS6mN+/bx7e2ajpXkEmOcWB6xBzWiuNoAQT18/+a20SS4U7FSYl8Ms7N6VTUxN/1JAjkx7aXp+THMC8xdpp0gA==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@sinonjs/commons": "^3.0.1",
- "@sinonjs/fake-timers": "^15.1.1",
- "@sinonjs/samsam": "^9.0.3",
- "diff": "^8.0.3",
- "supports-color": "^7.2.0"
+ "@sinonjs/fake-timers": "^15.3.2",
+ "@sinonjs/samsam": "^10.0.2",
+ "diff": "^8.0.4"
},
"funding": {
"type": "opencollective",
@@ -8886,16 +8885,16 @@
}
},
"node_modules/typescript-eslint": {
- "version": "8.58.0",
- "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.0.tgz",
- "integrity": "sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA==",
+ "version": "8.58.1",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.1.tgz",
+ "integrity": "sha512-gf6/oHChByg9HJvhMO1iBexJh12AqqTfnuxscMDOVqfJW3htsdRJI/GfPpHTTcyeB8cSTUY2JcZmVgoyPqcrDg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/eslint-plugin": "8.58.0",
- "@typescript-eslint/parser": "8.58.0",
- "@typescript-eslint/typescript-estree": "8.58.0",
- "@typescript-eslint/utils": "8.58.0"
+ "@typescript-eslint/eslint-plugin": "8.58.1",
+ "@typescript-eslint/parser": "8.58.1",
+ "@typescript-eslint/typescript-estree": "8.58.1",
+ "@typescript-eslint/utils": "8.58.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -8929,9 +8928,9 @@
}
},
"node_modules/undici-types": {
- "version": "7.18.2",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
- "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
+ "version": "7.19.2",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
+ "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
"dev": true,
"license": "MIT"
},
From e959987e800a4b16c7ad3461ba73a26cd5aa1235 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 13 Apr 2026 18:16:46 +0000
Subject: [PATCH 09/26] chore(deps-dev): bump globals from 17.4.0 to 17.5.0 in
the dev-dependencies group (#1857)
Bumps the dev-dependencies group with 1 update:
[globals](https://github.com/sindresorhus/globals).
Updates `globals` from 17.4.0 to 17.5.0
Release notes
Sourced from globals's
releases.
v17.5.0
- Update globals (2026-04-12) (#342)
5d84602
https://github.com/sindresorhus/globals/compare/v17.4.0...v17.5.0
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore ` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore ` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore ` will
remove the ignore condition of the specified dependency and ignore
conditions
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
package-lock.json | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 5aefa8c7b..f317bc4d9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5269,9 +5269,9 @@
}
},
"node_modules/globals": {
- "version": "17.4.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz",
- "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==",
+ "version": "17.5.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-17.5.0.tgz",
+ "integrity": "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==",
"dev": true,
"license": "MIT",
"engines": {
From 74fd545e5ed9ef315538df9cefee8ee299a432f2 Mon Sep 17 00:00:00 2001
From: Alex Rudenko
Date: Tue, 14 Apr 2026 15:21:11 +0200
Subject: [PATCH 10/26] feat: support DevTools header redactions as an option
(#1848)
This PR adds a CLI flag to enable redacting network headers in the same
way they are redacted in DevTools. Note that sometimes it might prevent
the agent from properly analysing network issues. Pass
`--redact-headers=false` to revert to the previous behavior.
---
README.md | 5 +++
src/McpResponse.ts | 7 +++
src/bin/chrome-devtools-mcp-cli-options.ts | 6 +++
src/formatters/NetworkFormatter.ts | 33 ++++++++++++--
src/index.ts | 2 +
tests/McpContext.test.js.snapshot | 2 +-
tests/McpResponse.test.js.snapshot | 8 ++--
tests/cli.test.ts | 2 +
tests/formatters/NetworkFormatter.test.ts | 51 ++++++++++++++++++++++
tests/tools/network.test.js.snapshot | 18 ++++----
tests/utils.ts | 14 +++---
11 files changed, 126 insertions(+), 22 deletions(-)
diff --git a/README.md b/README.md
index 41743ac90..913a58687 100644
--- a/README.md
+++ b/README.md
@@ -620,6 +620,11 @@ The Chrome DevTools MCP server supports the following configuration option:
Exposes a "slim" set of 3 tools covering navigation, script execution and screenshots only. Useful for basic browser tasks.
- **Type:** boolean
+- **`--redactNetworkHeaders`/ `--redact-network-headers`**
+ If true, redacts some of the network headers considered senstive before returning to the client.
+ - **Type:** boolean
+ - **Default:** `false`
+
Pass them via the `args` property in the JSON configuration. For example:
diff --git a/src/McpResponse.ts b/src/McpResponse.ts
index 77898b5df..719c04626 100644
--- a/src/McpResponse.ts
+++ b/src/McpResponse.ts
@@ -185,6 +185,7 @@ export class McpResponse implements Response {
#tabId?: string;
#args: ParsedArguments;
#page?: McpPage;
+ #redactNetworkHeaders = true;
constructor(args: ParsedArguments) {
this.#args = args;
@@ -194,6 +195,10 @@ export class McpResponse implements Response {
this.#page = page;
}
+ setRedactNetworkHeaders(value: boolean): void {
+ this.#redactNetworkHeaders = value;
+ }
+
attachDevToolsData(data: DevToolsData): void {
this.#devToolsData = data;
}
@@ -425,6 +430,7 @@ export class McpResponse implements Response {
requestFilePath: this.#attachedNetworkRequestOptions?.requestFilePath,
responseFilePath: this.#attachedNetworkRequestOptions?.responseFilePath,
saveFile: (data, filename) => context.saveFile(data, filename),
+ redactNetworkHeaders: this.#redactNetworkHeaders,
});
detailedNetworkRequest = formatter;
}
@@ -568,6 +574,7 @@ export class McpResponse implements Response {
this.#networkRequestsOptions?.networkRequestIdInDevToolsUI,
fetchData: false,
saveFile: (data, filename) => context.saveFile(data, filename),
+ redactNetworkHeaders: this.#redactNetworkHeaders,
}),
),
);
diff --git a/src/bin/chrome-devtools-mcp-cli-options.ts b/src/bin/chrome-devtools-mcp-cli-options.ts
index 3cfbacac0..3c8bcf07d 100644
--- a/src/bin/chrome-devtools-mcp-cli-options.ts
+++ b/src/bin/chrome-devtools-mcp-cli-options.ts
@@ -261,6 +261,12 @@ export const cliOptions = {
'Set by Chrome DevTools CLI if the MCP server is started via the CLI client (this arg exists for usage stats)',
hidden: true,
},
+ redactNetworkHeaders: {
+ type: 'boolean',
+ describe:
+ 'If true, redacts some of the network headers considered senstive before returning to the client.',
+ default: false,
+ },
} satisfies Record;
export type ParsedArguments = ReturnType;
diff --git a/src/formatters/NetworkFormatter.ts b/src/formatters/NetworkFormatter.ts
index abfb0693e..045d28855 100644
--- a/src/formatters/NetworkFormatter.ts
+++ b/src/formatters/NetworkFormatter.ts
@@ -6,7 +6,11 @@
import {isUtf8} from 'node:buffer';
-import type {HTTPRequest, HTTPResponse} from '../third_party/index.js';
+import {
+ DevTools,
+ type HTTPRequest,
+ type HTTPResponse,
+} from '../third_party/index.js';
const BODY_CONTEXT_SIZE_LIMIT = 10000;
@@ -21,6 +25,7 @@ export interface NetworkFormatterOptions {
data: Uint8Array,
filename: string,
) => Promise<{filename: string}>;
+ redactNetworkHeaders: boolean;
}
interface NetworkRequestConcise {
@@ -150,6 +155,20 @@ export class NetworkFormatter {
};
}
+ #redactNetworkHeaders(
+ headers: Record,
+ ): Record {
+ const headersList = Object.entries(headers).map(item => {
+ return {name: item[0], value: item[1]};
+ });
+ const redacted =
+ DevTools.NetworkRequestFormatter.sanitizeHeaders(headersList);
+ return redacted.reduce>((acc, item) => {
+ acc[item.name] = item.value;
+ return acc;
+ }, {});
+ }
+
toJSONDetailed(): NetworkRequestDetailed {
const redirectChain = this.#request.redirectChain();
const formattedRedirectChain = redirectChain.reverse().map(request => {
@@ -159,16 +178,24 @@ export class NetworkFormatter {
const formatter = new NetworkFormatter(request, {
requestId: id,
saveFile: this.#options.saveFile,
+ redactNetworkHeaders: this.#options.redactNetworkHeaders,
});
return formatter.toJSON();
});
+ const responseHeaders = this.#request.response()?.headers();
+
return {
...this.toJSON(),
- requestHeaders: this.#request.headers(),
+ requestHeaders: this.#options.redactNetworkHeaders
+ ? this.#redactNetworkHeaders(this.#request.headers())
+ : this.#request.headers(),
requestBody: this.#requestBody,
requestBodyFilePath: this.#requestBodyFilePath,
- responseHeaders: this.#request.response()?.headers(),
+ responseHeaders:
+ this.#options.redactNetworkHeaders && responseHeaders
+ ? this.#redactNetworkHeaders(responseHeaders)
+ : this.#request.response()?.headers(),
responseBody: this.#responseBody,
responseBodyFilePath: this.#responseBodyFilePath,
failure: this.#request.failure()?.errorText,
diff --git a/src/index.ts b/src/index.ts
index 362f2348a..8fed38b83 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -191,6 +191,8 @@ export async function createMcpServer(
const response = serverArgs.slim
? new SlimMcpResponse(serverArgs)
: new McpResponse(serverArgs);
+
+ response.setRedactNetworkHeaders(serverArgs.redactNetworkHeaders);
if ('pageScoped' in tool && tool.pageScoped) {
const page =
serverArgs.experimentalPageIdRouting &&
diff --git a/tests/McpContext.test.js.snapshot b/tests/McpContext.test.js.snapshot
index cfdd6f902..3f69a75c9 100644
--- a/tests/McpContext.test.js.snapshot
+++ b/tests/McpContext.test.js.snapshot
@@ -6,7 +6,7 @@ exports[`McpContext > should include detailed network request in structured cont
"url": "http://example.com/detail",
"status": "pending",
"requestHeaders": {
- "content-size": "10"
+ "content-size": ""
}
}
}
diff --git a/tests/McpResponse.test.js.snapshot b/tests/McpResponse.test.js.snapshot
index 1fb633b76..a0eb9bd1f 100644
--- a/tests/McpResponse.test.js.snapshot
+++ b/tests/McpResponse.test.js.snapshot
@@ -2,7 +2,7 @@ exports[`McpResponse > add network request when attached 1`] = `
## Request http://example.com
Status: pending
### Request Headers
-- content-size:10
+- content-size:
## Network requests
Showing 1-1 of 1 (Page 1 of 1).
reqid=1 GET http://example.com [pending]
@@ -16,7 +16,7 @@ exports[`McpResponse > add network request when attached 2`] = `
"url": "http://example.com",
"status": "pending",
"requestHeaders": {
- "content-size": "10"
+ "content-size": ""
}
},
"pagination": {
@@ -44,7 +44,7 @@ exports[`McpResponse > add network request when attached with POST data 1`] = `
## Request http://example.com
Status: 200
### Request Headers
-- content-size:10
+- content-size:
### Request Body
{"request":"body"}
### Response Headers
@@ -64,7 +64,7 @@ exports[`McpResponse > add network request when attached with POST data 2`] = `
"url": "http://example.com",
"status": "200",
"requestHeaders": {
- "content-size": "10"
+ "content-size": ""
},
"requestBody": "{\\"request\\":\\"body\\"}",
"responseHeaders": {
diff --git a/tests/cli.test.ts b/tests/cli.test.ts
index b18a4532f..44347af2e 100644
--- a/tests/cli.test.ts
+++ b/tests/cli.test.ts
@@ -23,6 +23,8 @@ describe('cli args parsing', () => {
performanceCrux: true,
'usage-statistics': true,
usageStatistics: true,
+ 'redact-network-headers': false,
+ redactNetworkHeaders: false,
};
it('parses with default args', async () => {
diff --git a/tests/formatters/NetworkFormatter.test.ts b/tests/formatters/NetworkFormatter.test.ts
index d09e18184..a8fad2c26 100644
--- a/tests/formatters/NetworkFormatter.test.ts
+++ b/tests/formatters/NetworkFormatter.test.ts
@@ -31,6 +31,7 @@ describe('NetworkFormatter', () => {
const formatter = await NetworkFormatter.from(request, {
requestId: 1,
saveFile: async () => ({filename: ''}),
+ redactNetworkHeaders: false,
});
assert.equal(
@@ -43,6 +44,7 @@ describe('NetworkFormatter', () => {
const formatter = await NetworkFormatter.from(request, {
requestId: 1,
saveFile: async () => ({filename: ''}),
+ redactNetworkHeaders: false,
});
assert.equal(
@@ -56,6 +58,7 @@ describe('NetworkFormatter', () => {
const formatter = await NetworkFormatter.from(request, {
requestId: 1,
saveFile: async () => ({filename: ''}),
+ redactNetworkHeaders: false,
});
assert.equal(
@@ -71,6 +74,7 @@ describe('NetworkFormatter', () => {
const formatter = await NetworkFormatter.from(request, {
requestId: 1,
saveFile: async () => ({filename: ''}),
+ redactNetworkHeaders: false,
});
assert.equal(
@@ -86,6 +90,7 @@ describe('NetworkFormatter', () => {
const formatter = await NetworkFormatter.from(request, {
requestId: 1,
saveFile: async () => ({filename: ''}),
+ redactNetworkHeaders: false,
});
assert.equal(
@@ -104,6 +109,7 @@ describe('NetworkFormatter', () => {
const formatter = await NetworkFormatter.from(request, {
requestId: 1,
saveFile: async () => ({filename: ''}),
+ redactNetworkHeaders: false,
});
assert.equal(
@@ -118,6 +124,7 @@ describe('NetworkFormatter', () => {
requestId: 1,
selectedInDevToolsUI: true,
saveFile: async () => ({filename: ''}),
+ redactNetworkHeaders: false,
});
assert.equal(
@@ -138,6 +145,7 @@ describe('NetworkFormatter', () => {
requestId: 200,
fetchData: true,
saveFile: async () => ({filename: ''}),
+ redactNetworkHeaders: false,
});
const result = formatter.toStringDetailed();
assert.match(result, /test/);
@@ -154,6 +162,7 @@ describe('NetworkFormatter', () => {
requestId: 200,
fetchData: true,
saveFile: async () => ({filename: ''}),
+ redactNetworkHeaders: false,
});
const result = formatter.toStringDetailed();
@@ -176,6 +185,7 @@ describe('NetworkFormatter', () => {
requestId: 20,
fetchData: true,
saveFile: async () => ({filename: ''}),
+ redactNetworkHeaders: false,
});
const result = formatter.toStringDetailed();
assert.match(result, /some text/);
@@ -209,6 +219,7 @@ describe('NetworkFormatter', () => {
await writeFile(filename, data);
return {filename};
},
+ redactNetworkHeaders: false,
});
const json = formatter.toJSONDetailed() as {
@@ -252,6 +263,7 @@ describe('NetworkFormatter', () => {
await writeFile(filename, data);
return {filename};
},
+ redactNetworkHeaders: false,
});
const reqContent = await readFile(reqPath, 'utf8');
@@ -272,6 +284,7 @@ describe('NetworkFormatter', () => {
requestId: 200,
fetchData: true,
saveFile: async () => ({filename: ''}),
+ redactNetworkHeaders: false,
});
const result = formatter.toStringDetailed();
@@ -289,6 +302,7 @@ describe('NetworkFormatter', () => {
requestId: 1,
requestIdResolver: () => 2,
saveFile: async () => ({filename: ''}),
+ redactNetworkHeaders: false,
});
const result = formatter.toStringDetailed();
assert.match(result, /Redirect chain/);
@@ -322,6 +336,7 @@ describe('NetworkFormatter', () => {
await writeFile(filename, data);
return {filename};
},
+ redactNetworkHeaders: false,
});
const result = formatter.toStringDetailed();
@@ -361,6 +376,7 @@ describe('NetworkFormatter', () => {
await writeFile(filename, data);
return {filename};
},
+ redactNetworkHeaders: false,
});
const result = formatter.toStringDetailed();
@@ -379,6 +395,7 @@ describe('NetworkFormatter', () => {
requestId: 1,
selectedInDevToolsUI: true,
saveFile: async () => ({filename: ''}),
+ redactNetworkHeaders: false,
});
const result = formatter.toJSON();
assert.deepEqual(result, {
@@ -404,6 +421,7 @@ describe('NetworkFormatter', () => {
requestId: 1,
fetchData: true,
saveFile: async () => ({filename: ''}),
+ redactNetworkHeaders: false,
});
const result = formatter.toJSONDetailed();
assert.deepEqual(result, {
@@ -425,6 +443,38 @@ describe('NetworkFormatter', () => {
});
});
+ it('redacts headers', async () => {
+ const response = getMockResponse({
+ headers: {
+ 'set-cookie': 'secret=123',
+ 'content-type': 'text/plain',
+ },
+ });
+ response.buffer = () => Promise.resolve(Buffer.from('response'));
+ const request = getMockRequest({
+ response,
+ headers: {
+ cookie: 'secret=123',
+ 'user-agent': 'test',
+ },
+ });
+ const formatter = await NetworkFormatter.from(request, {
+ requestId: 1,
+ fetchData: true,
+ saveFile: async () => ({filename: ''}),
+ redactNetworkHeaders: true,
+ });
+ const result = formatter.toJSONDetailed();
+ assert.deepEqual(result.requestHeaders, {
+ cookie: '',
+ 'user-agent': 'test',
+ });
+ assert.deepEqual(result.responseHeaders, {
+ 'set-cookie': '',
+ 'content-type': 'text/plain',
+ });
+ });
+
it('returns file paths in structured detailed data', async () => {
const request = {
method: () => 'POST',
@@ -453,6 +503,7 @@ describe('NetworkFormatter', () => {
await writeFile(filename, data);
return {filename};
},
+ redactNetworkHeaders: false,
});
const result = formatter.toJSONDetailed() as {
diff --git a/tests/tools/network.test.js.snapshot b/tests/tools/network.test.js.snapshot
index b77255483..4d920f18c 100644
--- a/tests/tools/network.test.js.snapshot
+++ b/tests/tools/network.test.js.snapshot
@@ -5,23 +5,23 @@ Status: 200
- accept-language:
- upgrade-insecure-requests:1
- user-agent:
-- sec-ch-ua:"Not-A.Brand";v="24", "Chromium";v="146"
-- sec-ch-ua-mobile:?0
-- sec-ch-ua-platform:""
+- sec-ch-ua:
+- sec-ch-ua-mobile:
+- sec-ch-ua-platform:
- accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
- accept-encoding:gzip, deflate, br, zstd
- connection:keep-alive
- host:localhost:
-- sec-fetch-dest:document
-- sec-fetch-mode:navigate
-- sec-fetch-site:none
-- sec-fetch-user:?1
+- sec-fetch-dest:
+- sec-fetch-mode:
+- sec-fetch-site:
+- sec-fetch-user:
### Response Headers
- connection:keep-alive
-- content-length:239
+- content-length:
- content-type:text/html; charset=utf-8
- date:
-- keep-alive:timeout=5
+- keep-alive:
### Response Body
`;
diff --git a/tests/utils.ts b/tests/utils.ts
index 23257616f..f764fa9ce 100644
--- a/tests/utils.ts
+++ b/tests/utils.ts
@@ -142,6 +142,7 @@ export function getMockRequest(
navigationRequest?: boolean;
frame?: Frame;
redirectChain?: HTTPRequest[];
+ headers?: Record;
} = {},
): HTTPRequest {
return {
@@ -170,9 +171,11 @@ export function getMockRequest(
return options.resourceType ?? 'document';
},
headers(): Record {
- return {
- 'content-size': '10',
- };
+ return (
+ options.headers ?? {
+ 'content-size': '10',
+ }
+ );
},
redirectChain(): HTTPRequest[] {
return options.redirectChain ?? [];
@@ -190,6 +193,7 @@ export function getMockRequest(
export function getMockResponse(
options: {
status?: number;
+ headers?: Record;
} = {},
): HTTPResponse {
return {
@@ -197,9 +201,9 @@ export function getMockResponse(
return options.status ?? 200;
},
headers(): Record {
- return {};
+ return options.headers ?? {};
},
- } as HTTPResponse;
+ } as unknown as HTTPResponse;
}
export function html(
From e6ca072d8698c9fc061d1a5dd59ca55ccd34dc50 Mon Sep 17 00:00:00 2001
From: Nikolay Vitkov <34244704+Lightning00Blade@users.noreply.github.com>
Date: Tue, 14 Apr 2026 17:26:11 +0200
Subject: [PATCH 11/26] chore: some build and test fixes (#1861)
Under some cases one could not re-build correctly due to the directory
being remove.
And we can save some CPU cycles in test to disable the update checker.
---
scripts/post-build.ts | 1 +
scripts/test.mjs | 1 +
2 files changed, 2 insertions(+)
diff --git a/scripts/post-build.ts b/scripts/post-build.ts
index edf822599..7cf9da6ae 100644
--- a/scripts/post-build.ts
+++ b/scripts/post-build.ts
@@ -26,6 +26,7 @@ function main(): void {
// Create i18n mock
const i18nDir = path.join(BUILD_DIR, devtoolsFrontEndCorePath, 'i18n');
+ fs.mkdirSync(i18nDir, {recursive: true});
const localesFile = path.join(i18nDir, 'locales.js');
const localesContent = `
export const LOCALES = [
diff --git a/scripts/test.mjs b/scripts/test.mjs
index 5c8410069..997b4759a 100644
--- a/scripts/test.mjs
+++ b/scripts/test.mjs
@@ -101,6 +101,7 @@ async function runTests(attempt) {
...process.env,
CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS: true,
CHROME_DEVTOOLS_MCP_CRASH_ON_UNCAUGHT: true,
+ CHROME_DEVTOOLS_MCP_NO_UPDATE_CHECKS: true,
},
});
From 90faa84f55cb46e2bb80b826321ce0751eb0d823 Mon Sep 17 00:00:00 2001
From: Michael Hablich
Date: Wed, 15 Apr 2026 12:57:09 +0200
Subject: [PATCH 12/26] docs: Rename project and enhance README content (#1856)
Updated the README to reflect the correct project name and added
information about the CLI.
---------
Co-authored-by: Mathias Bynens
---
README.md | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 913a58687..ebb3a4b09 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,12 @@
-# Chrome DevTools MCP
+# Chrome DevTools for Agents
[](https://npmjs.org/package/chrome-devtools-mcp)
-`chrome-devtools-mcp` lets your coding agent (such as Gemini, Claude, Cursor or Copilot)
+Chrome DevTools for Agents (`chrome-devtools-mcp`) lets your coding agent (such as Gemini, Claude, Cursor or Copilot)
control and inspect a live Chrome browser. It acts as a Model-Context-Protocol
(MCP) server, giving your AI coding assistant access to the full power of
Chrome DevTools for reliable automation, in-depth debugging, and performance analysis.
+A [CLI](docs/cli.md) is also provided for use without MCP.
## [Tool reference](./docs/tool-reference.md) | [Changelog](./CHANGELOG.md) | [Contributing](./CONTRIBUTING.md) | [Troubleshooting](./docs/troubleshooting.md) | [Design Principles](./docs/design-principles.md)
From 2ba12096c37412ad71e1a25bdf34583ac5ee8cbb Mon Sep 17 00:00:00 2001
From: Cocoon-Break <54054995+kuishou68@users.noreply.github.com>
Date: Wed, 15 Apr 2026 20:01:44 +0800
Subject: [PATCH 13/26] fix: remove double space in navigate error message
(#1847)
## Summary
Fixes a double space in the navigate error message in
`src/tools/pages.ts`.
## Problem
Line 225 contains an error message with double space:
```
'Unable to navigate in the selected page: ...'
```
## Fix
Changed to single space:
```
'Unable to navigate in the selected page: ...'
```
Signed-off-by: Cocoon-Break
<54054995+kuishou68@users.noreply.github.com>
Signed-off-by: Cocoon-Break <54054995+kuishou68@users.noreply.github.com>
---
src/tools/pages.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/tools/pages.ts b/src/tools/pages.ts
index fa5a40857..184a51350 100644
--- a/src/tools/pages.ts
+++ b/src/tools/pages.ts
@@ -222,7 +222,7 @@ export const navigatePage = definePageTool({
);
} catch (error) {
response.appendResponseLine(
- `Unable to navigate in the selected page: ${error.message}.`,
+ `Unable to navigate in the selected page: ${error.message}.`,
);
}
break;
From 576ba5975fe31403d51c006fc606ea61a01cf4bb Mon Sep 17 00:00:00 2001
From: Mathias Bynens
Date: Wed, 15 Apr 2026 14:13:35 +0200
Subject: [PATCH 14/26] chore: add guidelines w.r.t. valid security issues
(#1829)
---
SECURITY.md | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/SECURITY.md b/SECURITY.md
index c5bfca281..b8fac9564 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -1,3 +1,9 @@
## Security policy
The Chrome DevTools MCP project takes security very seriously. Please use [Chromium’s process to report security issues](https://www.chromium.org/Home/chromium-security/reporting-security-bugs/).
+
+### Scope
+
+In general, it is the expectation that the AI agent or client using this MCP server validates any input before sending it. The server provides powerful capabilities for browser automation and inspection, and it is the responsibility of the calling agent to ensure these are used safely and as intended.
+
+Several tools in this project have the ability to perform actions such as writing files to disk (e.g., via browser downloads or screenshots) or dynamically loading Chrome extensions. These are intentional, documented features and are not vulnerabilities.
From 7d655dff0e10e2d9ec6c66a3cba3acf1ab83ae97 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 15 Apr 2026 15:07:57 +0200
Subject: [PATCH 15/26] chore(deps-dev): bump the dev-dependencies group with 5
updates (#1865)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps the dev-dependencies group with 5 updates:
| Package | From | To |
| --- | --- | --- |
| [@google/genai](https://github.com/googleapis/js-genai) | `1.49.0` |
`1.50.1` |
|
[@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin)
| `8.58.1` | `8.58.2` |
|
[@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser)
| `8.58.1` | `8.58.2` |
| [prettier](https://github.com/prettier/prettier) | `3.8.2` | `3.8.3` |
|
[typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint)
| `8.58.1` | `8.58.2` |
Updates `@google/genai` from 1.49.0 to 1.50.1
Release notes
Sourced from @google/genai's
releases.
v1.50.1
1.50.1
(2026-04-14)
Bug Fixes
- Refactor Webhook types in GenAI SDKs for easier useage (5100abc)
- Rename
webhooks.retrieve to webhooks.get.
(db6e771)
v1.50.0
1.50.0
(2026-04-13)
[!CAUTION]
CRITICAL WARNING: Do not use this
version if you are implementing or relying on webhooks.
This release contains known issues regarding webhook sdk. Please use
v1.50.1 or later.
Features
- Add "eu" as a supported service location for Vertex AI
platform. (2493f9c)
- Add DeepResearchAgentConfig fields (3615ca2)
- Add Live Avatar new fields (6a0ff96)
- Add support for new audio MIME types: opus, alaw, and mulaw (7137f13)
- add webhook and webhookConfig for js and python sdk (0f89605)
- Add webhook_config to batches.create() and models.generate_videos()
(894bc93)
- Wire the webhook into python and js client. (b6c5d18)
Changelog
Sourced from @google/genai's
changelog.
1.50.1
(2026-04-14)
Bug Fixes
- Refactor Webhook types in GenAI SDKs for easier useage (5100abc)
- Rename
webhooks.retrieve to webhooks.get.
(db6e771)
1.50.0
(2026-04-13)
Features
- Add "eu" as a supported service location for Vertex AI
platform. (2493f9c)
- Add DeepResearchAgentConfig fields (3615ca2)
- Add Live Avatar new fields (6a0ff96)
- Add support for new audio MIME types: opus, alaw, and mulaw (7137f13)
- add webhook and webhookConfig for js and python sdk (0f89605)
- Add webhook_config to batches.create() and models.generate_videos()
(894bc93)
- Wire the webhook into python and js client. (b6c5d18)
Commits
aeb5cd3
chore(main): release 1.50.1 (#1497)
db6e771
fix: Rename webhooks.retrieve to
webhooks.get.
5100abc
fix: Refactor Webhook types in GenAI SDKs for easier useage
53829c4
chore(main): release 1.50.0 (#1481)
4d5e949
chore: internal change
894bc93
feat: Add webhook_config to batches.create() and
models.generate_videos()
b6c5d18
feat: Wire the webhook into python and js client.
0f89605
feat: add webhook and webhookConfig for js and python sdk
70d8f53
chore: support new config mappings and fields for gemini-embedding-2 on
GenAI...
3615ca2
feat: Add DeepResearchAgentConfig fields
- Additional commits viewable in compare
view
Updates `@typescript-eslint/eslint-plugin` from 8.58.1 to 8.58.2
Release notes
Sourced from @typescript-eslint/eslint-plugin's
releases.
v8.58.2
8.58.2 (2026-04-13)
🩹 Fixes
- remove tsbuildinfo cache file from published packages (#12187)
- eslint-plugin: [no-unnecessary-condition] use
assignability checks in checkTypePredicates (#12147)
❤️ Thank You
See GitHub
Releases for more information.
You can read about our versioning
strategy and releases on our
website.
Changelog
Sourced from @typescript-eslint/eslint-plugin's
changelog.
8.58.2 (2026-04-13)
🩹 Fixes
- eslint-plugin: [no-unnecessary-condition] use
assignability checks in checkTypePredicates (#12147)
- remove tsbuildinfo cache file from published packages (#12187)
❤️ Thank You
See GitHub
Releases for more information.
You can read about our versioning
strategy and releases on our
website.
Commits
90c2803
chore(release): publish 8.58.2
7c9e06f
fix(eslint-plugin): [no-unnecessary-condition] use assignability checks
in ch...
dae1732
chore(eslint-plugin): switch auto-generated test cases to hand-written
in unb...
be6b49a
fix: remove tsbuildinfo cache file from published packages (#12187)
- See full diff in compare
view
Updates `@typescript-eslint/parser` from 8.58.1 to 8.58.2
Release notes
Sourced from @typescript-eslint/parser's
releases.
v8.58.2
8.58.2 (2026-04-13)
🩹 Fixes
- remove tsbuildinfo cache file from published packages (#12187)
- eslint-plugin: [no-unnecessary-condition] use
assignability checks in checkTypePredicates (#12147)
❤️ Thank You
See GitHub
Releases for more information.
You can read about our versioning
strategy and releases on our
website.
Changelog
Sourced from @typescript-eslint/parser's
changelog.
8.58.2 (2026-04-13)
🩹 Fixes
- remove tsbuildinfo cache file from published packages (#12187)
❤️ Thank You
See GitHub
Releases for more information.
You can read about our versioning
strategy and releases on our
website.
Commits
Updates `prettier` from 3.8.2 to 3.8.3
Release notes
Sourced from prettier's
releases.
3.8.3
🔗 Changelog
Changelog
Sourced from prettier's
changelog.
3.8.3
diff
SCSS: Prevent trailing comma in if() function (#18471
by @kovsu)
// Input
$value: if(sass(false): 1; else: -1);
// Prettier 3.8.2
$value: if(
sass(false): 1; else: -1,
);
// Prettier 3.8.3
$value: if(sass(false): 1; else: -1);
Commits
Updates `typescript-eslint` from 8.58.1 to 8.58.2
Release notes
Sourced from typescript-eslint's
releases.
v8.58.2
8.58.2 (2026-04-13)
🩹 Fixes
- remove tsbuildinfo cache file from published packages (#12187)
- eslint-plugin: [no-unnecessary-condition] use
assignability checks in checkTypePredicates (#12147)
❤️ Thank You
See GitHub
Releases for more information.
You can read about our versioning
strategy and releases on our
website.
Changelog
Sourced from typescript-eslint's
changelog.
8.58.2 (2026-04-13)
🩹 Fixes
- remove tsbuildinfo cache file from published packages (#12187)
❤️ Thank You
See GitHub
Releases for more information.
You can read about our versioning
strategy and releases on our
website.
Commits
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore ` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore ` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore ` will
remove the ignore condition of the specified dependency and ignore
conditions
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
package-lock.json | 132 +++++++++++++++++++++++-----------------------
1 file changed, 66 insertions(+), 66 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index f317bc4d9..aa3c4c041 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -381,9 +381,9 @@
}
},
"node_modules/@google/genai": {
- "version": "1.49.0",
- "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.49.0.tgz",
- "integrity": "sha512-hO69Zl0H3x+L0KL4stl1pLYgnqnwHoLqtKy6MRlNnW8TAxjqMdOUVafomKd4z1BePkzoxJWbYILny9a2Zk43VQ==",
+ "version": "1.50.1",
+ "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.50.1.tgz",
+ "integrity": "sha512-YbkX7H9+1Pt8wOt7DDREy8XSoiL6fRDzZQRyaVBarFf8MR3zHGqVdvM4cLbDXqPhxqvegZShgfxb8kw9C7YhAQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -2257,17 +2257,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
- "version": "8.58.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.1.tgz",
- "integrity": "sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ==",
+ "version": "8.58.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.2.tgz",
+ "integrity": "sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
- "@typescript-eslint/scope-manager": "8.58.1",
- "@typescript-eslint/type-utils": "8.58.1",
- "@typescript-eslint/utils": "8.58.1",
- "@typescript-eslint/visitor-keys": "8.58.1",
+ "@typescript-eslint/scope-manager": "8.58.2",
+ "@typescript-eslint/type-utils": "8.58.2",
+ "@typescript-eslint/utils": "8.58.2",
+ "@typescript-eslint/visitor-keys": "8.58.2",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.5.0"
@@ -2286,16 +2286,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
- "version": "8.58.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.1.tgz",
- "integrity": "sha512-gGkiNMPqerb2cJSVcruigx9eHBlLG14fSdPdqMoOcBfh+vvn4iCq2C8MzUB89PrxOXk0y3GZ1yIWb9aOzL93bw==",
+ "version": "8.58.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.2.tgz",
+ "integrity": "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/scope-manager": "8.58.1",
- "@typescript-eslint/types": "8.58.1",
- "@typescript-eslint/typescript-estree": "8.58.1",
- "@typescript-eslint/visitor-keys": "8.58.1",
+ "@typescript-eslint/scope-manager": "8.58.2",
+ "@typescript-eslint/types": "8.58.2",
+ "@typescript-eslint/typescript-estree": "8.58.2",
+ "@typescript-eslint/visitor-keys": "8.58.2",
"debug": "^4.4.3"
},
"engines": {
@@ -2311,14 +2311,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
- "version": "8.58.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.1.tgz",
- "integrity": "sha512-gfQ8fk6cxhtptek+/8ZIqw8YrRW5048Gug8Ts5IYcMLCw18iUgrZAEY/D7s4hkI0FxEfGakKuPK/XUMPzPxi5g==",
+ "version": "8.58.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.2.tgz",
+ "integrity": "sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/tsconfig-utils": "^8.58.1",
- "@typescript-eslint/types": "^8.58.1",
+ "@typescript-eslint/tsconfig-utils": "^8.58.2",
+ "@typescript-eslint/types": "^8.58.2",
"debug": "^4.4.3"
},
"engines": {
@@ -2333,14 +2333,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
- "version": "8.58.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.1.tgz",
- "integrity": "sha512-TPYUEqJK6avLcEjumWsIuTpuYODTTDAtoMdt8ZZa93uWMTX13Nb8L5leSje1NluammvU+oI3QRr5lLXPgihX3w==",
+ "version": "8.58.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.2.tgz",
+ "integrity": "sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.58.1",
- "@typescript-eslint/visitor-keys": "8.58.1"
+ "@typescript-eslint/types": "8.58.2",
+ "@typescript-eslint/visitor-keys": "8.58.2"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2351,9 +2351,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
- "version": "8.58.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.1.tgz",
- "integrity": "sha512-JAr2hOIct2Q+qk3G+8YFfqkqi7sC86uNryT+2i5HzMa2MPjw4qNFvtjnw1IiA1rP7QhNKVe21mSSLaSjwA1Olw==",
+ "version": "8.58.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.2.tgz",
+ "integrity": "sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2368,15 +2368,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
- "version": "8.58.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.1.tgz",
- "integrity": "sha512-HUFxvTJVroT+0rXVJC7eD5zol6ID+Sn5npVPWoFuHGg9Ncq5Q4EYstqR+UOqaNRFXi5TYkpXXkLhoCHe3G0+7w==",
+ "version": "8.58.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.2.tgz",
+ "integrity": "sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.58.1",
- "@typescript-eslint/typescript-estree": "8.58.1",
- "@typescript-eslint/utils": "8.58.1",
+ "@typescript-eslint/types": "8.58.2",
+ "@typescript-eslint/typescript-estree": "8.58.2",
+ "@typescript-eslint/utils": "8.58.2",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
},
@@ -2393,9 +2393,9 @@
}
},
"node_modules/@typescript-eslint/types": {
- "version": "8.58.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.1.tgz",
- "integrity": "sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw==",
+ "version": "8.58.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.2.tgz",
+ "integrity": "sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2407,16 +2407,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
- "version": "8.58.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.1.tgz",
- "integrity": "sha512-w4w7WR7GHOjqqPnvAYbazq+Y5oS68b9CzasGtnd6jIeOIeKUzYzupGTB2T4LTPSv4d+WPeccbxuneTFHYgAAWg==",
+ "version": "8.58.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.2.tgz",
+ "integrity": "sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/project-service": "8.58.1",
- "@typescript-eslint/tsconfig-utils": "8.58.1",
- "@typescript-eslint/types": "8.58.1",
- "@typescript-eslint/visitor-keys": "8.58.1",
+ "@typescript-eslint/project-service": "8.58.2",
+ "@typescript-eslint/tsconfig-utils": "8.58.2",
+ "@typescript-eslint/types": "8.58.2",
+ "@typescript-eslint/visitor-keys": "8.58.2",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@@ -2474,16 +2474,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
- "version": "8.58.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.1.tgz",
- "integrity": "sha512-Ln8R0tmWC7pTtLOzgJzYTXSCjJ9rDNHAqTaVONF4FEi2qwce8mD9iSOxOpLFFvWp/wBFlew0mjM1L1ihYWfBdQ==",
+ "version": "8.58.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.2.tgz",
+ "integrity": "sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
- "@typescript-eslint/scope-manager": "8.58.1",
- "@typescript-eslint/types": "8.58.1",
- "@typescript-eslint/typescript-estree": "8.58.1"
+ "@typescript-eslint/scope-manager": "8.58.2",
+ "@typescript-eslint/types": "8.58.2",
+ "@typescript-eslint/typescript-estree": "8.58.2"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2498,13 +2498,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
- "version": "8.58.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.1.tgz",
- "integrity": "sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ==",
+ "version": "8.58.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.2.tgz",
+ "integrity": "sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.58.1",
+ "@typescript-eslint/types": "8.58.2",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
@@ -7364,9 +7364,9 @@
}
},
"node_modules/prettier": {
- "version": "3.8.2",
- "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.2.tgz",
- "integrity": "sha512-8c3mgTe0ASwWAJK+78dpviD+A8EqhndQPUBpNUIPt6+xWlIigCwfN01lWr9MAede4uqXGTEKeQWTvzb3vjia0Q==",
+ "version": "3.8.3",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz",
+ "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==",
"dev": true,
"license": "MIT",
"bin": {
@@ -8885,16 +8885,16 @@
}
},
"node_modules/typescript-eslint": {
- "version": "8.58.1",
- "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.1.tgz",
- "integrity": "sha512-gf6/oHChByg9HJvhMO1iBexJh12AqqTfnuxscMDOVqfJW3htsdRJI/GfPpHTTcyeB8cSTUY2JcZmVgoyPqcrDg==",
+ "version": "8.58.2",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.2.tgz",
+ "integrity": "sha512-V8iSng9mRbdZjl54VJ9NKr6ZB+dW0J3TzRXRGcSbLIej9jV86ZRtlYeTKDR/QLxXykocJ5icNzbsl2+5TzIvcQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/eslint-plugin": "8.58.1",
- "@typescript-eslint/parser": "8.58.1",
- "@typescript-eslint/typescript-estree": "8.58.1",
- "@typescript-eslint/utils": "8.58.1"
+ "@typescript-eslint/eslint-plugin": "8.58.2",
+ "@typescript-eslint/parser": "8.58.2",
+ "@typescript-eslint/typescript-estree": "8.58.2",
+ "@typescript-eslint/utils": "8.58.2"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
From 49ca1a2410df9fe56c427056e23e5af75a56ce7c Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 15 Apr 2026 15:56:57 +0200
Subject: [PATCH 16/26] chore(deps-dev): bump chrome-devtools-frontend from
1.0.1611825 to 1.0.1613625 in the bundled-devtools group across 1 directory
(#1866)
Bumps the bundled-devtools group with 1 update in the / directory:
[chrome-devtools-frontend](https://github.com/ChromeDevTools/devtools-frontend).
Updates `chrome-devtools-frontend` from 1.0.1611825 to 1.0.1613625
Commits
842a3f3
AI: ensure users who enable AI with V2 do not see opt in
ee63ce8
Update DevTools DEPS (trusted)
6c08c9f
[webmcp] Extend JSON editor for non-CDP commands
146f5b5
Add missing ve logging to Ai features
7cd6f77
Small style fixes
6c3a98c
Allow specifying worker URL
18a1c68
Update DevTools DEPS (trusted)
9d1b094
Fix bug with how suggestion text was being sliced
7b13d85
[AI] README for the performance agent initial context and tool data
a2a6d23
Add an image diff tool to view screenshot changes locally
- Additional commits viewable in compare
view
---------
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nikolay Vitkov
---
package-lock.json | 8 ++++----
package.json | 2 +-
tsconfig.json | 4 ++--
3 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index aa3c4c041..d1fbf4cf4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -27,7 +27,7 @@
"@types/yargs": "^17.0.33",
"@typescript-eslint/eslint-plugin": "^8.43.0",
"@typescript-eslint/parser": "^8.43.0",
- "chrome-devtools-frontend": "1.0.1611825",
+ "chrome-devtools-frontend": "1.0.1613625",
"core-js": "3.49.0",
"debug": "4.4.3",
"eslint": "^9.35.0",
@@ -3452,9 +3452,9 @@
}
},
"node_modules/chrome-devtools-frontend": {
- "version": "1.0.1611825",
- "resolved": "https://registry.npmjs.org/chrome-devtools-frontend/-/chrome-devtools-frontend-1.0.1611825.tgz",
- "integrity": "sha512-xp7EQPurkgJgYiSjIyLc3d7+BMevetrVeXHm5zEK0Zbr99/XjOlUzMnj18twLsrb/fYXYnMD4g5SjzcJkYATfQ==",
+ "version": "1.0.1613625",
+ "resolved": "https://registry.npmjs.org/chrome-devtools-frontend/-/chrome-devtools-frontend-1.0.1613625.tgz",
+ "integrity": "sha512-Ao1y2Nq6A2XWJPEX00encO/uzt7dssmAAz5EViySpC/8bqoKM6CDNQdM1U++Gh7+qOXGM9DIMU31BQK47Krd5A==",
"dev": true,
"license": "BSD-3-Clause"
},
diff --git a/package.json b/package.json
index 3b794054b..2925e659b 100644
--- a/package.json
+++ b/package.json
@@ -60,7 +60,7 @@
"@types/yargs": "^17.0.33",
"@typescript-eslint/eslint-plugin": "^8.43.0",
"@typescript-eslint/parser": "^8.43.0",
- "chrome-devtools-frontend": "1.0.1611825",
+ "chrome-devtools-frontend": "1.0.1613625",
"core-js": "3.49.0",
"debug": "4.4.3",
"eslint": "^9.35.0",
diff --git a/tsconfig.json b/tsconfig.json
index ab5d0f00b..eb629e3f8 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -5,8 +5,8 @@
"ES2023",
"DOM",
"ES2024.Promise",
- "ES2025.Iterator",
- "ES2025.Collection"
+ "ESNext.Iterator",
+ "ESNext.Collection"
],
"types": ["node", "filesystem"],
"moduleResolution": "bundler",
From 5305ecff34a5476480cdc176d4111574bb18fe03 Mon Sep 17 00:00:00 2001
From: yulunz <11618243+yulunz@users.noreply.github.com>
Date: Wed, 15 Apr 2026 11:21:19 -0700
Subject: [PATCH 17/26] chore: turn arg name into snake case in sanitization
(#1862)
This fixes the casing of the tool call params. We don't need any server
side fix since they are already converted to snake case in the proto
definition. This is needed nevertheless since the sanitizeParams()
function will be called when we log the params (see the next PR: #1863
1863).
---
src/telemetry/ClearcutLogger.ts | 10 ++-
src/telemetry/tool_call_metrics.json | 90 ++++++++++++------------
tests/telemetry/ClearcutLogger.test.ts | 10 +--
tests/telemetry/toolMetricsUtils.test.ts | 4 +-
4 files changed, 59 insertions(+), 55 deletions(-)
diff --git a/src/telemetry/ClearcutLogger.ts b/src/telemetry/ClearcutLogger.ts
index 82f766cd4..c2e2b5b75 100644
--- a/src/telemetry/ClearcutLogger.ts
+++ b/src/telemetry/ClearcutLogger.ts
@@ -60,12 +60,16 @@ export function getZodType(zodType: zod.ZodTypeAny): ZodType {
type LoggedToolCallArgValue = string | number | boolean;
export function transformArgName(zodType: ZodType, name: string): string {
+ const snakeCaseName = name.replace(
+ /[A-Z]/g,
+ letter => `_${letter.toLowerCase()}`,
+ );
if (zodType === 'ZodString') {
- return `${name}_length`;
+ return `${snakeCaseName}_length`;
} else if (zodType === 'ZodArray') {
- return `${name}_count`;
+ return `${snakeCaseName}_count`;
} else {
- return name;
+ return snakeCaseName;
}
}
diff --git a/src/telemetry/tool_call_metrics.json b/src/telemetry/tool_call_metrics.json
index 496045ff6..743f0eb1d 100644
--- a/src/telemetry/tool_call_metrics.json
+++ b/src/telemetry/tool_call_metrics.json
@@ -3,11 +3,11 @@
"name": "click",
"args": [
{
- "name": "dblClick",
+ "name": "dbl_click",
"argType": "boolean"
},
{
- "name": "includeSnapshot",
+ "name": "include_snapshot",
"argType": "boolean"
}
]
@@ -24,11 +24,11 @@
"argType": "number"
},
{
- "name": "dblClick",
+ "name": "dbl_click",
"argType": "boolean"
},
{
- "name": "includeSnapshot",
+ "name": "include_snapshot",
"argType": "boolean"
}
]
@@ -37,7 +37,7 @@
"name": "close_page",
"args": [
{
- "name": "pageId",
+ "name": "page_id",
"argType": "number"
}
]
@@ -54,7 +54,7 @@
"argType": "number"
},
{
- "name": "includeSnapshot",
+ "name": "include_snapshot",
"argType": "boolean"
}
]
@@ -63,11 +63,11 @@
"name": "emulate",
"args": [
{
- "name": "networkConditions",
+ "name": "network_conditions",
"argType": "string"
},
{
- "name": "cpuThrottlingRate",
+ "name": "cpu_throttling_rate",
"argType": "number"
},
{
@@ -75,11 +75,11 @@
"argType": "number"
},
{
- "name": "userAgent_length",
+ "name": "user_agent_length",
"argType": "number"
},
{
- "name": "colorScheme",
+ "name": "color_scheme",
"argType": "string"
},
{
@@ -114,7 +114,7 @@
"name": "execute_in_page_tool",
"args": [
{
- "name": "toolName_length",
+ "name": "tool_name_length",
"argType": "number"
},
{
@@ -131,7 +131,7 @@
"argType": "number"
},
{
- "name": "includeSnapshot",
+ "name": "include_snapshot",
"argType": "boolean"
}
]
@@ -144,7 +144,7 @@
"argType": "number"
},
{
- "name": "includeSnapshot",
+ "name": "include_snapshot",
"argType": "boolean"
}
]
@@ -157,11 +157,11 @@
"name": "get_network_request",
"args": [
{
- "name": "requestFilePath_length",
+ "name": "request_file_path_length",
"argType": "number"
},
{
- "name": "responseFilePath_length",
+ "name": "response_file_path_length",
"argType": "number"
}
]
@@ -170,7 +170,7 @@
"name": "get_tab_id",
"args": [
{
- "name": "pageId",
+ "name": "page_id",
"argType": "number"
}
]
@@ -183,7 +183,7 @@
"argType": "string"
},
{
- "name": "promptText_length",
+ "name": "prompt_text_length",
"argType": "number"
}
]
@@ -192,7 +192,7 @@
"name": "hover",
"args": [
{
- "name": "includeSnapshot",
+ "name": "include_snapshot",
"argType": "boolean"
}
]
@@ -218,7 +218,7 @@
"argType": "string"
},
{
- "name": "outputDirPath_length",
+ "name": "output_dir_path_length",
"argType": "number"
}
]
@@ -227,11 +227,11 @@
"name": "list_console_messages",
"args": [
{
- "name": "pageSize",
+ "name": "page_size",
"argType": "number"
},
{
- "name": "pageIdx",
+ "name": "page_idx",
"argType": "number"
},
{
@@ -239,7 +239,7 @@
"argType": "number"
},
{
- "name": "includePreservedMessages",
+ "name": "include_preserved_messages",
"argType": "boolean"
}
]
@@ -256,19 +256,19 @@
"name": "list_network_requests",
"args": [
{
- "name": "pageSize",
+ "name": "page_size",
"argType": "number"
},
{
- "name": "pageIdx",
+ "name": "page_idx",
"argType": "number"
},
{
- "name": "resourceTypes_count",
+ "name": "resource_types_count",
"argType": "number"
},
{
- "name": "includePreservedRequests",
+ "name": "include_preserved_requests",
"argType": "boolean"
}
]
@@ -298,15 +298,15 @@
"argType": "number"
},
{
- "name": "ignoreCache",
+ "name": "ignore_cache",
"argType": "boolean"
},
{
- "name": "handleBeforeUnload",
+ "name": "handle_before_unload",
"argType": "string"
},
{
- "name": "initScript_length",
+ "name": "init_script_length",
"argType": "number"
},
{
@@ -327,7 +327,7 @@
"argType": "boolean"
},
{
- "name": "isolatedContext_length",
+ "name": "isolated_context_length",
"argType": "number"
},
{
@@ -340,11 +340,11 @@
"name": "performance_analyze_insight",
"args": [
{
- "name": "insightSetId_length",
+ "name": "insight_set_id_length",
"argType": "number"
},
{
- "name": "insightName_length",
+ "name": "insight_name_length",
"argType": "number"
}
]
@@ -357,11 +357,11 @@
"argType": "boolean"
},
{
- "name": "autoStop",
+ "name": "auto_stop",
"argType": "boolean"
},
{
- "name": "filePath_length",
+ "name": "file_path_length",
"argType": "number"
}
]
@@ -370,7 +370,7 @@
"name": "performance_stop_trace",
"args": [
{
- "name": "filePath_length",
+ "name": "file_path_length",
"argType": "number"
}
]
@@ -383,7 +383,7 @@
"argType": "number"
},
{
- "name": "includeSnapshot",
+ "name": "include_snapshot",
"argType": "boolean"
}
]
@@ -431,11 +431,11 @@
"name": "select_page",
"args": [
{
- "name": "pageId",
+ "name": "page_id",
"argType": "number"
},
{
- "name": "bringToFront",
+ "name": "bring_to_front",
"argType": "boolean"
}
]
@@ -444,7 +444,7 @@
"name": "take_memory_snapshot",
"args": [
{
- "name": "filePath_length",
+ "name": "file_path_length",
"argType": "number"
}
]
@@ -461,11 +461,11 @@
"argType": "number"
},
{
- "name": "fullPage",
+ "name": "full_page",
"argType": "boolean"
},
{
- "name": "filePath_length",
+ "name": "file_path_length",
"argType": "number"
}
]
@@ -478,7 +478,7 @@
"argType": "boolean"
},
{
- "name": "filePath_length",
+ "name": "file_path_length",
"argType": "number"
}
]
@@ -500,7 +500,7 @@
"argType": "number"
},
{
- "name": "submitKey_length",
+ "name": "submit_key_length",
"argType": "number"
}
]
@@ -518,11 +518,11 @@
"name": "upload_file",
"args": [
{
- "name": "filePath_length",
+ "name": "file_path_length",
"argType": "number"
},
{
- "name": "includeSnapshot",
+ "name": "include_snapshot",
"argType": "boolean"
}
]
diff --git a/tests/telemetry/ClearcutLogger.test.ts b/tests/telemetry/ClearcutLogger.test.ts
index 792202856..3f49d6dec 100644
--- a/tests/telemetry/ClearcutLogger.test.ts
+++ b/tests/telemetry/ClearcutLogger.test.ts
@@ -191,11 +191,11 @@ describe('ClearcutLogger', () => {
const sanitized = sanitizeParams(params, schema);
assert.deepStrictEqual(sanitized, {
- myString_length: 5,
- myArray_count: 2,
- myNumber: 42,
- myBool: true,
- myEnum: 'a',
+ my_string_length: 5,
+ my_array_count: 2,
+ my_number: 42,
+ my_bool: true,
+ my_enum: 'a',
});
});
diff --git a/tests/telemetry/toolMetricsUtils.test.ts b/tests/telemetry/toolMetricsUtils.test.ts
index 0c369aaea..2a3fa53bf 100644
--- a/tests/telemetry/toolMetricsUtils.test.ts
+++ b/tests/telemetry/toolMetricsUtils.test.ts
@@ -55,7 +55,7 @@ describe('toolMetricsUtils', () => {
assert.strictEqual(metrics.length, 1);
assert.strictEqual(metrics[0].name, 'test_tool');
assert.strictEqual(metrics[0].args.length, 1); // uid is blocked
- assert.strictEqual(metrics[0].args[0].name, 'argStr_length');
+ assert.strictEqual(metrics[0].args[0].name, 'arg_str_length');
assert.strictEqual(metrics[0].args[0].argType, 'number');
});
@@ -77,7 +77,7 @@ describe('toolMetricsUtils', () => {
const metrics = generateToolMetrics([mockTool]);
assert.strictEqual(metrics.length, 1);
- assert.strictEqual(metrics[0].args[0].name, 'argEnum');
+ assert.strictEqual(metrics[0].args[0].name, 'arg_enum');
assert.strictEqual(metrics[0].args[0].argType, 'string');
});
});
From 4bb511ef13aa2e396c1dfcf72e57c9ef985b69d3 Mon Sep 17 00:00:00 2001
From: yulunz <11618243+yulunz@users.noreply.github.com>
Date: Wed, 15 Apr 2026 13:19:41 -0700
Subject: [PATCH 18/26] feat: add tool call params logging (#1863)
This will enable logging for tool params. The server side changes should
all be ready now.
parent pr: #1862
---
src/index.ts | 2 ++
src/telemetry/ClearcutLogger.ts | 20 ++++++++++----
src/telemetry/types.ts | 1 +
tests/telemetry/ClearcutLogger.test.ts | 36 ++++++++++++++++++++++++++
4 files changed, 54 insertions(+), 5 deletions(-)
diff --git a/src/index.ts b/src/index.ts
index 8fed38b83..105a311e4 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -254,6 +254,8 @@ export async function createMcpServer(
} finally {
void clearcutLogger?.logToolInvocation({
toolName: tool.name,
+ params,
+ schema,
success,
latencyMs: bucketizeLatency(Date.now() - startTime),
});
diff --git a/src/telemetry/ClearcutLogger.ts b/src/telemetry/ClearcutLogger.ts
index c2e2b5b75..27a499d91 100644
--- a/src/telemetry/ClearcutLogger.ts
+++ b/src/telemetry/ClearcutLogger.ts
@@ -17,6 +17,7 @@ import {
type FlagUsage,
WatchdogMessageType,
OsType,
+ type ToolInvocation,
} from './types.js';
import {WatchdogClient} from './WatchdogClient.js';
@@ -207,18 +208,27 @@ export class ClearcutLogger {
async logToolInvocation(args: {
toolName: string;
+ params: ShapeOutput;
+ schema: zod.ZodRawShape;
success: boolean;
latencyMs: number;
}): Promise {
+ const tool_invocation: ToolInvocation = {
+ tool_name: args.toolName,
+ success: args.success,
+ latency_ms: args.latencyMs,
+ };
+ if (Object.keys(args.params).length > 0) {
+ tool_invocation.tool_params = {
+ [`${args.toolName}_params`]: sanitizeParams(args.params, args.schema),
+ };
+ }
+
this.#watchdog.send({
type: WatchdogMessageType.LOG_EVENT,
payload: {
mcp_client: this.#mcpClient,
- tool_invocation: {
- tool_name: args.toolName,
- success: args.success,
- latency_ms: args.latencyMs,
- },
+ tool_invocation: tool_invocation,
},
});
}
diff --git a/src/telemetry/types.ts b/src/telemetry/types.ts
index 8acb32922..d3caf3934 100644
--- a/src/telemetry/types.ts
+++ b/src/telemetry/types.ts
@@ -22,6 +22,7 @@ export interface ToolInvocation {
tool_name: string;
success: boolean;
latency_ms: number;
+ tool_params?: object;
}
export interface ServerStart {
diff --git a/tests/telemetry/ClearcutLogger.test.ts b/tests/telemetry/ClearcutLogger.test.ts
index 3f49d6dec..8ef061bdd 100644
--- a/tests/telemetry/ClearcutLogger.test.ts
+++ b/tests/telemetry/ClearcutLogger.test.ts
@@ -46,6 +46,8 @@ describe('ClearcutLogger', () => {
});
await logger.logToolInvocation({
toolName: 'test_tool',
+ params: {},
+ schema: {},
success: true,
latencyMs: 123,
});
@@ -57,6 +59,40 @@ describe('ClearcutLogger', () => {
assert.strictEqual(msg.payload.tool_invocation?.success, true);
assert.strictEqual(msg.payload.tool_invocation?.latency_ms, 123);
});
+ it('sends sanitized params', async () => {
+ const logger = new ClearcutLogger({
+ persistence: mockPersistence,
+ appVersion: '1.0.0',
+ watchdogClient: mockWatchdogClient,
+ });
+
+ const schema = {
+ uid: zod.string(),
+ myString: zod.string(),
+ };
+
+ const params = {
+ uid: 'sensitive',
+ myString: 'hello',
+ };
+
+ await logger.logToolInvocation({
+ toolName: 'test_tool',
+ params,
+ schema,
+ success: true,
+ latencyMs: 123,
+ });
+
+ assert(mockWatchdogClient.send.calledOnce);
+ const msg = mockWatchdogClient.send.firstCall.args[0];
+ assert.strictEqual(msg.type, WatchdogMessageType.LOG_EVENT);
+ assert.deepStrictEqual(msg.payload.tool_invocation?.tool_params, {
+ test_tool_params: {
+ my_string_length: 5,
+ },
+ });
+ });
});
describe('setClientName', () => {
From 345deac7e13b86293e94a4739ca81be1c8edfb8d Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 16 Apr 2026 05:57:05 +0000
Subject: [PATCH 19/26] chore(deps-dev): bump puppeteer from 24.40.0 to 24.41.0
in the bundled group (#1869)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps the bundled group with 1 update:
[puppeteer](https://github.com/puppeteer/puppeteer).
Updates `puppeteer` from 24.40.0 to 24.41.0
Release notes
Sourced from puppeteer's
releases.
puppeteer-core: v24.41.0
🎉 Features
🛠️ Fixes
📄 Documentation
puppeteer: v24.41.0
🎉 Features
Dependencies
- The following workspace dependencies were updated
... (truncated)
Changelog
Sourced from puppeteer's
changelog.
🎉 Features
Dependencies
- The following workspace dependencies were updated
- dependencies
- puppeteer-core bumped from 24.40.0 to 24.41.0
🛠️ Fixes
📄 Documentation
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore ` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore ` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore ` will
remove the ignore condition of the specified dependency and ignore
conditions
---------
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Alex Rudenko
---
package-lock.json | 26 ++++++++++-----------
package.json | 2 +-
src/McpContext.ts | 2 +-
src/PageCollector.ts | 12 +++++-----
tests/PageCollector.test.ts | 45 +++++++------------------------------
5 files changed, 29 insertions(+), 58 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index d1fbf4cf4..e1eb9deed 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -36,7 +36,7 @@
"globals": "^17.0.0",
"lighthouse": "13.1.0",
"prettier": "^3.6.2",
- "puppeteer": "24.40.0",
+ "puppeteer": "24.41.0",
"rollup": "4.60.1",
"rollup-plugin-cleanup": "^3.2.1",
"rollup-plugin-license": "^3.6.0",
@@ -3870,9 +3870,9 @@
}
},
"node_modules/devtools-protocol": {
- "version": "0.0.1581282",
- "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz",
- "integrity": "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==",
+ "version": "0.0.1595872",
+ "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1595872.tgz",
+ "integrity": "sha512-kRfgp8vWVjBu/fbYCiVFiOqsCk3CrMKEo3WbgGT2NXK2dG7vawWPBljixajVgGK9II8rDO9G0oD0zLt3I1daRg==",
"dev": true,
"license": "BSD-3-Clause"
},
@@ -7477,9 +7477,9 @@
}
},
"node_modules/puppeteer": {
- "version": "24.40.0",
- "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.40.0.tgz",
- "integrity": "sha512-IxQbDq93XHVVLWHrAkFP7F7iHvb9o0mgfsSIMlhHb+JM+JjM1V4v4MNSQfcRWJopx9dsNOr9adYv0U5fm9BJBQ==",
+ "version": "24.41.0",
+ "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.41.0.tgz",
+ "integrity": "sha512-W6Fk0J3TPjjtwjXOyR/qf+YaL0H/Uq8HIgHcXG4mNM/IgbKMCH/HPyK0Fi2qbTU/QpSl9bCte2yBpGHKejTpIw==",
"dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
@@ -7487,8 +7487,8 @@
"@puppeteer/browsers": "2.13.0",
"chromium-bidi": "14.0.0",
"cosmiconfig": "^9.0.0",
- "devtools-protocol": "0.0.1581282",
- "puppeteer-core": "24.40.0",
+ "devtools-protocol": "0.0.1595872",
+ "puppeteer-core": "24.41.0",
"typed-query-selector": "^2.12.1"
},
"bin": {
@@ -7499,16 +7499,16 @@
}
},
"node_modules/puppeteer-core": {
- "version": "24.40.0",
- "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.40.0.tgz",
- "integrity": "sha512-MWL3XbUCfVgGR0gRsidzT6oKJT2QydPLhMITU6HoVWiiv4gkb6gJi3pcdAa8q4HwjBTbqISOWVP4aJiiyUJvag==",
+ "version": "24.41.0",
+ "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.41.0.tgz",
+ "integrity": "sha512-rLIUri7E/NQ3APSEYCCozaSJx0u8Tu9wxO6BJwnvXmIgILSK3L0TombaVh3izp1njAGrO6H2ru0hcIrLF+gWLw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@puppeteer/browsers": "2.13.0",
"chromium-bidi": "14.0.0",
"debug": "^4.4.3",
- "devtools-protocol": "0.0.1581282",
+ "devtools-protocol": "0.0.1595872",
"typed-query-selector": "^2.12.1",
"webdriver-bidi-protocol": "0.4.1",
"ws": "^8.19.0"
diff --git a/package.json b/package.json
index 2925e659b..3ea6bfc5d 100644
--- a/package.json
+++ b/package.json
@@ -69,7 +69,7 @@
"globals": "^17.0.0",
"lighthouse": "13.1.0",
"prettier": "^3.6.2",
- "puppeteer": "24.40.0",
+ "puppeteer": "24.41.0",
"rollup": "4.60.1",
"rollup-plugin-cleanup": "^3.2.1",
"rollup-plugin-license": "^3.6.0",
diff --git a/src/McpContext.ts b/src/McpContext.ts
index cd11fc13d..7e0e76907 100644
--- a/src/McpContext.ts
+++ b/src/McpContext.ts
@@ -121,7 +121,7 @@ export class McpContext implements Context {
uncaughtError: event => {
collect(event);
},
- issue: event => {
+ devtoolsAggregatedIssue: event => {
collect(event);
},
} as ListenerMap;
diff --git a/src/PageCollector.ts b/src/PageCollector.ts
index d943baa54..ce6a792e6 100644
--- a/src/PageCollector.ts
+++ b/src/PageCollector.ts
@@ -11,6 +11,7 @@ import type {
CDPSession,
ConsoleMessage,
Protocol,
+ Issue,
} from './third_party/index.js';
import {DevTools} from './third_party/index.js';
import {
@@ -33,7 +34,7 @@ export class UncaughtError {
}
interface PageEvents extends PuppeteerPageEvents {
- issue: DevTools.AggregatedIssue;
+ devtoolsAggregatedIssue: DevTools.AggregatedIssue;
uncaughtError: UncaughtError;
}
@@ -285,7 +286,7 @@ class PageEventSubscriber {
async subscribe() {
this.#resetIssueAggregator();
this.#page.on('framenavigated', this.#onFrameNavigated);
- this.#session.on('Audits.issueAdded', this.#onIssueAdded);
+ this.#page.on('issue', this.#onIssueAdded);
this.#session.on('Runtime.exceptionThrown', this.#onExceptionThrown);
try {
await this.#session.send('Audits.enable');
@@ -298,7 +299,7 @@ class PageEventSubscriber {
this.#seenKeys.clear();
this.#seenIssues.clear();
this.#page.off('framenavigated', this.#onFrameNavigated);
- this.#session.off('Audits.issueAdded', this.#onIssueAdded);
+ this.#page.off('issue', this.#onIssueAdded);
this.#session.off('Runtime.exceptionThrown', this.#onExceptionThrown);
if (this.#issueAggregator) {
this.#issueAggregator.removeEventListener(
@@ -318,7 +319,7 @@ class PageEventSubscriber {
return;
}
this.#seenIssues.add(event.data);
- this.#page.emit('issue', event.data);
+ this.#page.emit('devtoolsAggregatedIssue', event.data);
};
#onExceptionThrown = (event: Protocol.Runtime.ExceptionThrownEvent) => {
@@ -339,9 +340,8 @@ class PageEventSubscriber {
this.#resetIssueAggregator();
};
- #onIssueAdded = (data: Protocol.Audits.IssueAddedEvent) => {
+ #onIssueAdded = (inspectorIssue: Issue) => {
try {
- const inspectorIssue = data.issue;
const issue = DevTools.createIssuesFromProtocolIssue(
null,
// @ts-expect-error Protocol types diverge.
diff --git a/tests/PageCollector.test.ts b/tests/PageCollector.test.ts
index 6814002a5..be5b0b0ef 100644
--- a/tests/PageCollector.test.ts
+++ b/tests/PageCollector.test.ts
@@ -329,40 +329,13 @@ describe('ConsoleCollector', () => {
sinon.restore();
});
- it('emits issues on page', async () => {
- const browser = getMockBrowser();
- const page = (await browser.pages())[0];
- // @ts-expect-error internal API.
- const cdpSession = page._client();
- const onIssuesListener = sinon.spy();
-
- page.on('issue', onIssuesListener);
-
- const collector = new ConsoleCollector(browser, collect => {
- return {
- issue: issue => {
- collect(issue as DevTools.AggregatedIssue);
- },
- } as ListenerMap;
- });
- await collector.init([page]);
- cdpSession.emit('Audits.issueAdded', {issue});
- sinon.assert.calledOnce(onIssuesListener);
-
- const issueArgument = onIssuesListener.getCall(0).args[0];
- assert(issueArgument instanceof DevTools.AggregatedIssue);
- });
-
it('collects issues', async () => {
const browser = getMockBrowser();
const page = (await browser.pages())[0];
- // @ts-expect-error internal API.
- const cdpSession = page._client();
-
const collector = new ConsoleCollector(browser, collect => {
return {
- issue: issue => {
- collect(issue as DevTools.AggregatedIssue);
+ devtoolsAggregatedIssue: issue => {
+ collect(issue);
},
} as ListenerMap;
});
@@ -379,8 +352,8 @@ describe('ConsoleCollector', () => {
},
} satisfies Protocol.Audits.InspectorIssue;
- cdpSession.emit('Audits.issueAdded', {issue});
- cdpSession.emit('Audits.issueAdded', {issue: issue2});
+ page.emit('issue', issue);
+ page.emit('issue', issue2);
const data = collector.getData(page);
assert.equal(data.length, 2);
});
@@ -388,20 +361,18 @@ describe('ConsoleCollector', () => {
it('filters duplicated issues', async () => {
const browser = getMockBrowser();
const page = (await browser.pages())[0];
- // @ts-expect-error internal API.
- const cdpSession = page._client();
const collector = new ConsoleCollector(browser, collect => {
return {
- issue: issue => {
- collect(issue as DevTools.AggregatedIssue);
+ devtoolsAggregatedIssue: issue => {
+ collect(issue);
},
} as ListenerMap;
});
await collector.init([page]);
- cdpSession.emit('Audits.issueAdded', {issue});
- cdpSession.emit('Audits.issueAdded', {issue});
+ page.emit('issue', issue);
+ page.emit('issue', issue);
const data = collector.getData(page);
assert.equal(data.length, 1);
const collectedIssue = data[0];
From 60303e1d7c3120d880cb00855239a75f50f6c2e8 Mon Sep 17 00:00:00 2001
From: Alex Rudenko
Date: Thu, 16 Apr 2026 08:08:22 +0200
Subject: [PATCH 20/26] feat: ensure extensions for file outputs (#1867)
This PR ensures the extensions for the file outputs of different types
minimizing the chance of misuse. The input filePath, thus, might be
modified but it should not be an issue for clients as the final output
path is returned to the clients in the response.
Closes https://github.com/ChromeDevTools/chrome-devtools-mcp/issues/1864
---------
Co-authored-by: Nikolay Vitkov <34244704+Lightning00Blade@users.noreply.github.com>
---
src/McpContext.ts | 11 +++++---
src/McpResponse.ts | 11 +++++---
src/formatters/NetworkFormatter.ts | 14 +++++++---
src/tools/ToolDefinition.ts | 15 ++++++++++-
src/tools/lighthouse.ts | 8 ++++--
src/tools/memory.ts | 3 ++-
src/tools/performance.ts | 6 ++++-
src/tools/screencast.ts | 3 ++-
src/tools/screenshot.ts | 8 ++++--
src/utils/files.ts | 8 ++++++
tests/McpResponse.test.ts | 2 +-
tests/utils/files.test.ts | 43 ++++++++++++++++++++++++++++++
12 files changed, 112 insertions(+), 20 deletions(-)
create mode 100644 tests/utils/files.test.ts
diff --git a/src/McpContext.ts b/src/McpContext.ts
index 7e0e76907..0e10e2231 100644
--- a/src/McpContext.ts
+++ b/src/McpContext.ts
@@ -37,6 +37,7 @@ import type {
Context,
DevToolsData,
ContextPage,
+ SupportedExtensions,
} from './tools/ToolDefinition.js';
import type {TraceResult} from './trace-processing/parse.js';
import type {
@@ -50,7 +51,7 @@ import {
ExtensionRegistry,
type InstalledExtension,
} from './utils/ExtensionRegistry.js';
-import {saveTemporaryFile} from './utils/files.js';
+import {ensureExtension, saveTemporaryFile} from './utils/files.js';
import {getNetworkMultiplierFromString} from './WaitForHelper.js';
interface McpContextOptions {
@@ -954,10 +955,14 @@ export class McpContext implements Context {
}
async saveFile(
data: Uint8Array,
- filename: string,
+ clientProvidedFilePath: string,
+ extension: SupportedExtensions,
): Promise<{filename: string}> {
try {
- const filePath = path.resolve(filename);
+ const filePath = ensureExtension(
+ path.resolve(clientProvidedFilePath),
+ extension,
+ );
await fs.mkdir(path.dirname(filePath), {recursive: true});
await fs.writeFile(filePath, data);
return {filename: filePath};
diff --git a/src/McpResponse.ts b/src/McpResponse.ts
index 719c04626..c424401af 100644
--- a/src/McpResponse.ts
+++ b/src/McpResponse.ts
@@ -403,11 +403,12 @@ export class McpResponse implements Response {
if (textSnapshot) {
const formatter = new SnapshotFormatter(textSnapshot);
if (this.#snapshotParams.filePath) {
- await context.saveFile(
+ const result = await context.saveFile(
new TextEncoder().encode(formatter.toString()),
this.#snapshotParams.filePath,
+ '.txt',
);
- snapshot = this.#snapshotParams.filePath;
+ snapshot = result.filename;
} else {
snapshot = formatter;
}
@@ -429,7 +430,8 @@ export class McpResponse implements Response {
fetchData: true,
requestFilePath: this.#attachedNetworkRequestOptions?.requestFilePath,
responseFilePath: this.#attachedNetworkRequestOptions?.responseFilePath,
- saveFile: (data, filename) => context.saveFile(data, filename),
+ saveFile: (data, filename, extension) =>
+ context.saveFile(data, filename, extension),
redactNetworkHeaders: this.#redactNetworkHeaders,
});
detailedNetworkRequest = formatter;
@@ -573,7 +575,8 @@ export class McpResponse implements Response {
context.getNetworkRequestStableId(request) ===
this.#networkRequestsOptions?.networkRequestIdInDevToolsUI,
fetchData: false,
- saveFile: (data, filename) => context.saveFile(data, filename),
+ saveFile: (data, filename, extension) =>
+ context.saveFile(data, filename, extension),
redactNetworkHeaders: this.#redactNetworkHeaders,
}),
),
diff --git a/src/formatters/NetworkFormatter.ts b/src/formatters/NetworkFormatter.ts
index 045d28855..9e21d2c60 100644
--- a/src/formatters/NetworkFormatter.ts
+++ b/src/formatters/NetworkFormatter.ts
@@ -24,6 +24,7 @@ export interface NetworkFormatterOptions {
saveFile?: (
data: Uint8Array,
filename: string,
+ extension: '.network-request' | '.network-response',
) => Promise<{filename: string}>;
redactNetworkHeaders: boolean;
}
@@ -88,11 +89,12 @@ export class NetworkFormatter {
throw new Error('saveFile is not provided');
}
if (data) {
- await this.#options.saveFile(
+ const result = await this.#options.saveFile(
Buffer.from(data),
this.#options.requestFilePath,
+ '.network-request',
);
- this.#requestBodyFilePath = this.#options.requestFilePath;
+ this.#requestBodyFilePath = result.filename;
} else {
this.#requestBody = requestBodyNotAvailableMessage;
}
@@ -119,8 +121,12 @@ export class NetworkFormatter {
if (!this.#options.saveFile) {
throw new Error('saveFile is not provided');
}
- await this.#options.saveFile(buffer, this.#options.responseFilePath);
- this.#responseBodyFilePath = this.#options.responseFilePath;
+ const result = await this.#options.saveFile(
+ buffer,
+ this.#options.responseFilePath,
+ '.network-response',
+ );
+ this.#responseBodyFilePath = result.filename;
} catch {
// Flatten error handling for buffer() failure and save failure
}
diff --git a/src/tools/ToolDefinition.ts b/src/tools/ToolDefinition.ts
index 2d1b52e58..855ad7dc1 100644
--- a/src/tools/ToolDefinition.ts
+++ b/src/tools/ToolDefinition.ts
@@ -138,6 +138,18 @@ export interface Response {
setListInPageTools(): void;
}
+export type SupportedExtensions =
+ | '.png'
+ | '.jpeg'
+ | '.webp'
+ | '.json'
+ | '.network-response'
+ | '.network-request'
+ | '.html'
+ | '.txt'
+ | '.csv'
+ | '.json.gz';
+
/**
* Only add methods required by tools/*.
*/
@@ -172,7 +184,8 @@ export type Context = Readonly<{
): Promise<{filepath: string}>;
saveFile(
data: Uint8Array,
- filename: string,
+ clientProvidedFilePath: string,
+ extension: SupportedExtensions,
): Promise<{filename: string}>;
waitForTextOnPage(
text: string[],
diff --git a/src/tools/lighthouse.ts b/src/tools/lighthouse.ts
index 606abf1ad..4356f1e62 100644
--- a/src/tools/lighthouse.ts
+++ b/src/tools/lighthouse.ts
@@ -107,8 +107,12 @@ export const lighthouseAudit = definePageTool({
const report = generateReport(lhr, format);
const data = encoder.encode(report);
if (outputDirPath) {
- const reportPath = path.join(outputDirPath, `report.${format}`);
- const {filename} = await context.saveFile(data, reportPath);
+ const reportPath = path.join(outputDirPath, `report`);
+ const {filename} = await context.saveFile(
+ data,
+ reportPath,
+ `.${format}`,
+ );
reportPaths.push(filename);
} else {
const {filepath} = await context.saveTemporaryFile(
diff --git a/src/tools/memory.ts b/src/tools/memory.ts
index b1f302ae1..91fb73a17 100644
--- a/src/tools/memory.ts
+++ b/src/tools/memory.ts
@@ -5,6 +5,7 @@
*/
import {zod} from '../third_party/index.js';
+import {ensureExtension} from '../utils/files.js';
import {ToolCategory} from './categories.js';
import {definePageTool} from './ToolDefinition.js';
@@ -25,7 +26,7 @@ export const takeMemorySnapshot = definePageTool({
const page = request.page;
await page.pptrPage.captureHeapSnapshot({
- path: request.params.filePath,
+ path: ensureExtension(request.params.filePath, '.heapsnapshot'),
});
response.appendResponseLine(
diff --git a/src/tools/performance.ts b/src/tools/performance.ts
index c02b627b9..acc588655 100644
--- a/src/tools/performance.ts
+++ b/src/tools/performance.ts
@@ -197,7 +197,11 @@ async function stopTracingAndAppendOutput(
});
});
}
- const file = await context.saveFile(dataToWrite, filePath);
+ const file = await context.saveFile(
+ dataToWrite,
+ filePath,
+ filePath.endsWith('.gz') ? '.json.gz' : '.json',
+ );
response.appendResponseLine(
`The raw trace data was saved to ${file.filename}.`,
);
diff --git a/src/tools/screencast.ts b/src/tools/screencast.ts
index a5ab1c38e..9f6f6a5f8 100644
--- a/src/tools/screencast.ts
+++ b/src/tools/screencast.ts
@@ -10,6 +10,7 @@ import path from 'node:path';
import {zod} from '../third_party/index.js';
import type {ScreenRecorder} from '../third_party/index.js';
+import {ensureExtension} from '../utils/files.js';
import {ToolCategory} from './categories.js';
import {definePageTool} from './ToolDefinition.js';
@@ -46,7 +47,7 @@ export const startScreencast = definePageTool({
}
const filePath = request.params.path ?? (await generateTempFilePath());
- const resolvedPath = path.resolve(filePath);
+ const resolvedPath = ensureExtension(path.resolve(filePath), '.mp4');
const page = request.page;
diff --git a/src/tools/screenshot.ts b/src/tools/screenshot.ts
index 2e648531c..f740fda4e 100644
--- a/src/tools/screenshot.ts
+++ b/src/tools/screenshot.ts
@@ -87,8 +87,12 @@ export const screenshot = definePageTool({
}
if (request.params.filePath) {
- const file = await context.saveFile(screenshot, request.params.filePath);
- response.appendResponseLine(`Saved screenshot to ${file.filename}.`);
+ const result = await context.saveFile(
+ screenshot,
+ request.params.filePath,
+ `.${format}`,
+ );
+ response.appendResponseLine(`Saved screenshot to ${result.filename}.`);
} else if (screenshot.length >= 2_000_000) {
const {filepath} = await context.saveTemporaryFile(
screenshot,
diff --git a/src/utils/files.ts b/src/utils/files.ts
index 03ddfc45f..abdba3ed8 100644
--- a/src/utils/files.ts
+++ b/src/utils/files.ts
@@ -24,3 +24,11 @@ export async function saveTemporaryFile(
throw new Error('Could not save a file', {cause: err});
}
}
+
+export function ensureExtension(
+ filepath: string,
+ extension: `.${string}`,
+): string {
+ const ext = path.extname(filepath);
+ return filepath.slice(0, filepath.length - ext.length) + extension;
+}
diff --git a/tests/McpResponse.test.ts b/tests/McpResponse.test.ts
index 915377197..8bebe52ef 100644
--- a/tests/McpResponse.test.ts
+++ b/tests/McpResponse.test.ts
@@ -156,7 +156,7 @@ describe('McpResponse', () => {
});
it('saves snapshot to file and returns structured content', async t => {
- const filePath = join(tmpdir(), 'test-screenshot.png');
+ const filePath = join(tmpdir(), 'test-snapshot.txt');
try {
await withMcpContext(async (response, context) => {
const page = context.getSelectedPptrPage();
diff --git a/tests/utils/files.test.ts b/tests/utils/files.test.ts
new file mode 100644
index 000000000..44052642e
--- /dev/null
+++ b/tests/utils/files.test.ts
@@ -0,0 +1,43 @@
+/**
+ * @license
+ * Copyright 2026 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import assert from 'node:assert';
+import {describe, it} from 'node:test';
+
+import {ensureExtension} from '../../src/utils/files.js';
+
+describe('ensureExtension', () => {
+ it('should add an extension to a filename without one', () => {
+ assert.strictEqual(ensureExtension('filename', '.txt'), 'filename.txt');
+ });
+
+ it('should replace an existing extension', () => {
+ assert.strictEqual(ensureExtension('filename.jpg', '.txt'), 'filename.txt');
+ });
+
+ it('should handle extension without a leading dot', () => {
+ assert.strictEqual(ensureExtension('filename', '.txt'), 'filename.txt');
+ });
+
+ it('should not add a second dot if already present', () => {
+ assert.strictEqual(ensureExtension('filename.txt', '.txt'), 'filename.txt');
+ });
+
+ it('should handle paths with directories', () => {
+ assert.strictEqual(
+ ensureExtension('/path/to/file.jpg', '.png'),
+ '/path/to/file.png',
+ );
+ });
+
+ it('should handle hidden files (starting with dot)', () => {
+ assert.strictEqual(ensureExtension('.bashrc', '.txt'), '.bashrc.txt');
+ });
+
+ it('should handle complex extensions (like .tar.gz) - path.extname only gets the last one', () => {
+ assert.strictEqual(ensureExtension('file.tar.gz', '.zip'), 'file.tar.zip');
+ });
+});
From 11057dfa9d23ce3240d3c0a2bf0377d65d90aa33 Mon Sep 17 00:00:00 2001
From: yulunz <11618243+yulunz@users.noreply.github.com>
Date: Wed, 15 Apr 2026 23:08:56 -0700
Subject: [PATCH 21/26] chore: better log watchdog entry (#1871)
this would log the entire message including the session id, app version
and os type which will help debugging.
---
src/telemetry/watchdog/ClearcutSender.ts | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/telemetry/watchdog/ClearcutSender.ts b/src/telemetry/watchdog/ClearcutSender.ts
index ba55c80fa..0a3678e02 100644
--- a/src/telemetry/watchdog/ClearcutSender.ts
+++ b/src/telemetry/watchdog/ClearcutSender.ts
@@ -70,14 +70,14 @@ export class ClearcutSender {
this.#sessionCreated = Date.now();
}
- logger('Enqueing telemetry event', JSON.stringify(event, null, 2));
-
- this.#addToBuffer({
+ const eventToSend = {
...event,
session_id: this.#sessionId,
app_version: this.#appVersion,
os_type: this.#osType,
- });
+ };
+ logger('Enqueing telemetry event', JSON.stringify(eventToSend, null, 2));
+ this.#addToBuffer(eventToSend);
if (!this.#timerStarted) {
this.#timerStarted = true;
From 83f911f9d8885f3f0b55e9660b04f157971a4972 Mon Sep 17 00:00:00 2001
From: Alex Rudenko
Date: Thu, 16 Apr 2026 08:22:16 +0200
Subject: [PATCH 22/26] build: update to node24 for development (#1868)
---
.github/workflows/publish-to-npm-on-tag.yml | 4 ----
.nvmrc | 2 +-
2 files changed, 1 insertion(+), 5 deletions(-)
diff --git a/.github/workflows/publish-to-npm-on-tag.yml b/.github/workflows/publish-to-npm-on-tag.yml
index 015e9b04b..c83debc67 100644
--- a/.github/workflows/publish-to-npm-on-tag.yml
+++ b/.github/workflows/publish-to-npm-on-tag.yml
@@ -36,10 +36,6 @@ jobs:
node-version-file: '.nvmrc'
registry-url: 'https://registry.npmjs.org'
- # Ensure npm 11.5.1 or later is installed
- - name: Update npm
- run: npm install -g npm@latest
-
- name: Install dependencies
run: npm ci
diff --git a/.nvmrc b/.nvmrc
index 92f279e3e..18c92ea98 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-v22
\ No newline at end of file
+v24
\ No newline at end of file
From b01e13cd1e62b4d178d4f7f90043448f629d44a5 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 16 Apr 2026 09:33:35 +0200
Subject: [PATCH 23/26] chore(deps): bump hono from 4.12.12 to 4.12.14 (#1872)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [hono](https://github.com/honojs/hono) from 4.12.12 to 4.12.14.
Release notes
Sourced from hono's
releases.
v4.12.14
Security fixes
This release includes fixes for the following security issues:
Improper handling of JSX attribute names in hono/jsx SSR
Affects: hono/jsx. Fixes missing validation of JSX attribute names
during server-side rendering, which could allow malformed attribute keys
to corrupt the generated HTML output and inject unintended attributes or
elements. GHSA-458j-xx4x-4375
Other changes
- fix(aws-lambda): handle invalid header names in request processing
(#4883)
fa2c74fe
v4.12.13
What's Changed
New Contributors
Full Changelog: https://github.com/honojs/hono/compare/v4.12.12...v4.12.13
Commits
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
package-lock.json | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index e1eb9deed..f6892ec12 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5456,9 +5456,9 @@
}
},
"node_modules/hono": {
- "version": "4.12.12",
- "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.12.tgz",
- "integrity": "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==",
+ "version": "4.12.14",
+ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.14.tgz",
+ "integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==",
"dev": true,
"license": "MIT",
"engines": {
From 4d484cea5387473820d98f7a3cae03bc4acb6f2c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fran=C3=A7ois=20Beaufort?=
Date: Thu, 16 Apr 2026 09:42:32 +0200
Subject: [PATCH 24/26] feat(webmcp): Add experimental tool to list WebMCP
tools page exposes (#1845)
```
npx @modelcontextprotocol/inspector node build/src/bin/chrome-devtools-mcp.js --channel=canary --experimentalWebmcp --chrome-arg='--enable-features=WebMCPTesting,DevToolsWebMCPSupport'
```
cc @OrKoN
---
src/McpPage.ts | 5 +
src/McpResponse.ts | 44 +++++++
src/bin/chrome-devtools-mcp-cli-options.ts | 5 +
src/bin/chrome-devtools.ts | 1 +
src/index.ts | 6 +
src/telemetry/tool_call_metrics.json | 4 +
src/tools/ToolDefinition.ts | 1 +
src/tools/pages.ts | 3 +
src/tools/tools.ts | 2 +
src/tools/webmcp.ts | 22 ++++
tests/McpResponse.test.js.snapshot | 110 ++++++++++++++++++
tests/McpResponse.test.ts | 127 +++++++++++++++++++++
tests/tools/webmcp.test.ts | 40 +++++++
tests/utils.ts | 4 +-
14 files changed, 373 insertions(+), 1 deletion(-)
create mode 100644 src/tools/webmcp.ts
create mode 100644 tests/tools/webmcp.test.ts
diff --git a/src/McpPage.ts b/src/McpPage.ts
index f31dd0b8d..419356a97 100644
--- a/src/McpPage.ts
+++ b/src/McpPage.ts
@@ -9,6 +9,7 @@ import type {
ElementHandle,
Page,
Viewport,
+ WebMCPTool,
} from './third_party/index.js';
import type {ToolGroup, ToolDefinition} from './tools/inPage.js';
import {takeSnapshot} from './tools/snapshot.js';
@@ -79,6 +80,10 @@ export class McpPage implements ContextPage {
return this.inPageTools;
}
+ getWebMcpTools(): WebMCPTool[] {
+ return this.pptrPage.webmcp.tools();
+ }
+
get networkConditions(): string | null {
return this.emulationSettings.networkConditions ?? null;
}
diff --git a/src/McpResponse.ts b/src/McpResponse.ts
index c424401af..21c13c3f5 100644
--- a/src/McpResponse.ts
+++ b/src/McpResponse.ts
@@ -4,6 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
+import type {WebMCPTool} from 'puppeteer-core';
+
import type {ParsedArguments} from './bin/chrome-devtools-mcp-cli-options.js';
import {ConsoleFormatter} from './formatters/ConsoleFormatter.js';
import {IssueFormatter} from './formatters/IssueFormatter.js';
@@ -181,6 +183,7 @@ export class McpResponse implements Response {
};
#listExtensions?: boolean;
#listInPageTools?: boolean;
+ #listWebMcpTools?: boolean;
#devToolsData?: DevToolsData;
#tabId?: string;
#args: ParsedArguments;
@@ -232,6 +235,10 @@ export class McpResponse implements Response {
}
}
+ setListWebMcpTools(): void {
+ this.#listWebMcpTools = true;
+ }
+
setIncludeNetworkRequests(
value: boolean,
options?: PaginationOptions & {
@@ -374,6 +381,10 @@ export class McpResponse implements Response {
return this.#snapshotParams;
}
+ get listWebMcpTools(): boolean | undefined {
+ return this.#listWebMcpTools;
+ }
+
async handle(
toolName: string,
context: McpContext,
@@ -490,6 +501,12 @@ export class McpResponse implements Response {
page.inPageTools = inPageTools;
}
+ let webmcpTools: WebMCPTool[] | undefined;
+ if (this.#listWebMcpTools && this.#args.experimentalWebmcp) {
+ const page = this.#page ?? context.getSelectedMcpPage();
+ webmcpTools = page.getWebMcpTools();
+ }
+
let consoleMessages: Array | undefined;
if (this.#consoleDataOptions?.include) {
if (!this.#page) {
@@ -595,6 +612,7 @@ export class McpResponse implements Response {
extensions,
lighthouseResult: this.#attachedLighthouseResult,
inPageTools,
+ webmcpTools,
});
}
@@ -612,6 +630,7 @@ export class McpResponse implements Response {
extensions?: InstalledExtension[];
lighthouseResult?: LighthouseData;
inPageTools?: ToolGroup;
+ webmcpTools?: WebMCPTool[];
},
): {content: Array; structuredContent: object} {
const structuredContent: {
@@ -627,6 +646,7 @@ export class McpResponse implements Response {
lighthouseResult?: object;
extensions?: object[];
inPageTools?: object;
+ webmcpTools?: object[];
message?: string;
networkConditions?: string;
navigationTimeout?: number;
@@ -884,6 +904,30 @@ Call ${handleDialog.name} to handle it before continuing.`);
}
}
+ if (this.#listWebMcpTools && data.webmcpTools) {
+ structuredContent.webmcpTools = data.webmcpTools.map(
+ ({name, description, inputSchema, annotations}) => ({
+ name,
+ description,
+ inputSchema,
+ annotations,
+ }),
+ );
+ response.push('## WebMCP tools');
+ if (data.webmcpTools.length === 0) {
+ response.push('No WebMCP tools available.');
+ } else {
+ const webmcpToolsMessage = data.webmcpTools
+ .map(tool => {
+ return `name="${tool.name}", description="${tool.description}", inputSchema=${JSON.stringify(
+ tool.inputSchema,
+ )}, annotations=${JSON.stringify(tool.annotations)}`;
+ })
+ .join('\n');
+ response.push(webmcpToolsMessage);
+ }
+ }
+
if (this.#networkRequestsOptions?.include && data.networkRequests) {
const requests = data.networkRequests;
diff --git a/src/bin/chrome-devtools-mcp-cli-options.ts b/src/bin/chrome-devtools-mcp-cli-options.ts
index 3c8bcf07d..2b2a50c6b 100644
--- a/src/bin/chrome-devtools-mcp-cli-options.ts
+++ b/src/bin/chrome-devtools-mcp-cli-options.ts
@@ -185,6 +185,11 @@ export const cliOptions = {
describe:
'Exposes experimental screencast tools (requires ffmpeg). Install ffmpeg https://www.ffmpeg.org/download.html and ensure it is available in the MCP server PATH.',
},
+ experimentalWebmcp: {
+ type: 'boolean',
+ describe: 'Set to true to enable debugging WebMCP tools.',
+ hidden: true,
+ },
chromeArg: {
type: 'array',
describe:
diff --git a/src/bin/chrome-devtools.ts b/src/bin/chrome-devtools.ts
index e3aab5f45..979533e84 100644
--- a/src/bin/chrome-devtools.ts
+++ b/src/bin/chrome-devtools.ts
@@ -51,6 +51,7 @@ delete startCliOptions.viewport;
// tools, they need to be enabled during CLI generation.
delete startCliOptions.experimentalPageIdRouting;
delete startCliOptions.experimentalVision;
+delete startCliOptions.experimentalWebmcp;
delete startCliOptions.experimentalInteropTools;
delete startCliOptions.experimentalScreencast;
delete startCliOptions.categoryEmulation;
diff --git a/src/index.ts b/src/index.ts
index 105a311e4..415c6a031 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -164,6 +164,12 @@ export async function createMcpServer(
) {
return;
}
+ if (
+ tool.annotations.conditions?.includes('experimentalWebmcp') &&
+ !serverArgs.experimentalWebmcp
+ ) {
+ return;
+ }
const schema =
'pageScoped' in tool &&
tool.pageScoped &&
diff --git a/src/telemetry/tool_call_metrics.json b/src/telemetry/tool_call_metrics.json
index 743f0eb1d..c3e2c80fe 100644
--- a/src/telemetry/tool_call_metrics.json
+++ b/src/telemetry/tool_call_metrics.json
@@ -539,5 +539,9 @@
"argType": "number"
}
]
+ },
+ {
+ "name": "list_webmcp_tools",
+ "args": []
}
]
diff --git a/src/tools/ToolDefinition.ts b/src/tools/ToolDefinition.ts
index 855ad7dc1..d7ee9ff82 100644
--- a/src/tools/ToolDefinition.ts
+++ b/src/tools/ToolDefinition.ts
@@ -136,6 +136,7 @@ export interface Response {
setListExtensions(): void;
attachLighthouseResult(result: LighthouseData): void;
setListInPageTools(): void;
+ setListWebMcpTools(): void;
}
export type SupportedExtensions =
diff --git a/src/tools/pages.ts b/src/tools/pages.ts
index 184a51350..9cc776c81 100644
--- a/src/tools/pages.ts
+++ b/src/tools/pages.ts
@@ -28,6 +28,7 @@ export const listPages = defineTool(args => {
handler: async (_request, response) => {
response.setIncludePages(true);
response.setListInPageTools();
+ response.setListWebMcpTools();
},
};
});
@@ -55,6 +56,7 @@ export const selectPage = defineTool({
context.selectPage(page);
response.setIncludePages(true);
response.setListInPageTools();
+ response.setListWebMcpTools();
if (request.params.bringToFront) {
await page.pptrPage.bringToFront();
}
@@ -280,6 +282,7 @@ export const navigatePage = definePageTool({
response.setIncludePages(true);
response.setListInPageTools();
+ response.setListWebMcpTools();
},
});
diff --git a/src/tools/tools.ts b/src/tools/tools.ts
index 3c74115c3..b3477b906 100644
--- a/src/tools/tools.ts
+++ b/src/tools/tools.ts
@@ -22,6 +22,7 @@ import * as scriptTools from './script.js';
import * as slimTools from './slim/tools.js';
import * as snapshotTools from './snapshot.js';
import type {ToolDefinition} from './ToolDefinition.js';
+import * as webmcpTools from './webmcp.js';
export const createTools = (args: ParsedArguments) => {
const rawTools = args.slim
@@ -41,6 +42,7 @@ export const createTools = (args: ParsedArguments) => {
...Object.values(screenshotTools),
...Object.values(scriptTools),
...Object.values(snapshotTools),
+ ...Object.values(webmcpTools),
];
const tools = [];
diff --git a/src/tools/webmcp.ts b/src/tools/webmcp.ts
new file mode 100644
index 000000000..e52a5ac60
--- /dev/null
+++ b/src/tools/webmcp.ts
@@ -0,0 +1,22 @@
+/**
+ * @license
+ * Copyright 2026 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {ToolCategory} from './categories.js';
+import {definePageTool} from './ToolDefinition.js';
+
+export const listWebMcpTools = definePageTool({
+ name: 'list_webmcp_tools',
+ description: `Lists all WebMCP tools the page exposes.`,
+ annotations: {
+ category: ToolCategory.DEBUGGING,
+ readOnlyHint: true,
+ conditions: ['experimentalWebmcp'],
+ },
+ schema: {},
+ handler: async (_request, response, _context) => {
+ response.setListWebMcpTools();
+ },
+});
diff --git a/tests/McpResponse.test.js.snapshot b/tests/McpResponse.test.js.snapshot
index a0eb9bd1f..641187d2f 100644
--- a/tests/McpResponse.test.js.snapshot
+++ b/tests/McpResponse.test.js.snapshot
@@ -1281,3 +1281,113 @@ exports[`lighthouse > includes lighthouse report paths 2`] = `
}
}
`;
+
+exports[`webmcp > includes webmcp tools in list_pages response 1`] = `
+## Pages
+1: about:blank [selected]
+## WebMCP tools
+name="test_tool", description="A test tool", inputSchema={}, annotations=undefined
+`;
+
+exports[`webmcp > includes webmcp tools in list_pages response 2`] = `
+{
+ "pages": [
+ {
+ "id": 1,
+ "url": "about:blank",
+ "selected": true
+ }
+ ],
+ "webmcpTools": [
+ {
+ "name": "test_tool",
+ "description": "A test tool",
+ "inputSchema": {}
+ }
+ ]
+}
+`;
+
+exports[`webmcp > includes webmcp tools in navigate_page response 1`] = `
+Successfully navigated to about:blank.
+## Pages
+1: about:blank [selected]
+## WebMCP tools
+name="test_tool", description="A test tool", inputSchema={}, annotations=undefined
+`;
+
+exports[`webmcp > includes webmcp tools in navigate_page response 2`] = `
+{
+ "message": "Successfully navigated to about:blank.",
+ "pages": [
+ {
+ "id": 1,
+ "url": "about:blank",
+ "selected": true
+ }
+ ],
+ "webmcpTools": [
+ {
+ "name": "test_tool",
+ "description": "A test tool",
+ "inputSchema": {}
+ }
+ ]
+}
+`;
+
+exports[`webmcp > includes webmcp tools in select_page response 1`] = `
+## Pages
+1: about:blank [selected]
+## WebMCP tools
+name="test_tool", description="A test tool", inputSchema={}, annotations=undefined
+`;
+
+exports[`webmcp > includes webmcp tools in select_page response 2`] = `
+{
+ "pages": [
+ {
+ "id": 1,
+ "url": "about:blank",
+ "selected": true
+ }
+ ],
+ "webmcpTools": [
+ {
+ "name": "test_tool",
+ "description": "A test tool",
+ "inputSchema": {}
+ }
+ ]
+}
+`;
+
+exports[`webmcp > list no webmcp tools if experimentalWebmcp is false 1`] = `
+Successfully navigated to about:blank.
+## Pages
+1: about:blank [selected]
+`;
+
+exports[`webmcp > list no webmcp tools if experimentalWebmcp is false 2`] = `
+{
+ "message": "Successfully navigated to about:blank.",
+ "pages": [
+ {
+ "id": 1,
+ "url": "about:blank",
+ "selected": true
+ }
+ ]
+}
+`;
+
+exports[`webmcp > list no webmcp tools if there are none 1`] = `
+## WebMCP tools
+No WebMCP tools available.
+`;
+
+exports[`webmcp > list no webmcp tools if there are none 2`] = `
+{
+ "webmcpTools": []
+}
+`;
diff --git a/tests/McpResponse.test.ts b/tests/McpResponse.test.ts
index 8bebe52ef..30ba25d83 100644
--- a/tests/McpResponse.test.ts
+++ b/tests/McpResponse.test.ts
@@ -1455,3 +1455,130 @@ describe('replaceHtmlElementsWithUids', () => {
}
});
});
+
+describe('webmcp', () => {
+ async function testIncludesWebmcpTools(
+ t: it.TestContext,
+ parseArguments: ParsedArguments,
+ handlerAction: (
+ response: McpResponse,
+ context: McpContext,
+ ) => Promise,
+ toolName: string,
+ ) {
+ await withMcpContext(
+ async (response, context) => {
+ response.setListWebMcpTools();
+
+ await handlerAction(response, context);
+
+ const page = context.getSelectedMcpPage().pptrPage;
+ await page.setContent(
+ html``,
+ );
+
+ const {content, structuredContent} = await response.handle(
+ toolName,
+ context,
+ );
+ assert.ok(getTextContent(content[0]));
+ t.assert.snapshot?.(getTextContent(content[0]));
+ t.assert.snapshot?.(
+ JSON.stringify(
+ stabilizeStructuredContent(structuredContent),
+ null,
+ 2,
+ ),
+ );
+ },
+ {args: ['--enable-features=WebMCPTesting,DevToolsWebMCPSupport']},
+ parseArguments,
+ );
+ }
+
+ it('includes webmcp tools in list_pages response', async t => {
+ await testIncludesWebmcpTools(
+ t,
+ {experimentalWebmcp: true} as ParsedArguments,
+ async (response, context) => {
+ await listPages().handler({params: {}}, response, context);
+ },
+ 'list_pages',
+ );
+ });
+
+ it('includes webmcp tools in select_page response', async t => {
+ await testIncludesWebmcpTools(
+ t,
+ {experimentalWebmcp: true} as ParsedArguments,
+ async (response, context) => {
+ const pageId =
+ context.getPageId(context.getSelectedMcpPage().pptrPage) ?? 1;
+ await selectPage.handler({params: {pageId}}, response, context);
+ },
+ 'select_page',
+ );
+ });
+
+ it('includes webmcp tools in navigate_page response', async t => {
+ await testIncludesWebmcpTools(
+ t,
+ {experimentalWebmcp: true} as ParsedArguments,
+ async (response, context) => {
+ await navigatePage.handler(
+ {
+ params: {type: 'url', url: 'about:blank'},
+ page: context.getSelectedMcpPage(),
+ },
+ response,
+ context,
+ );
+ },
+ 'navigate_page',
+ );
+ });
+
+ it('list no webmcp tools if there are none', async t => {
+ await withMcpContext(
+ async (response, context) => {
+ response.setListWebMcpTools();
+ const {content, structuredContent} = await response.handle(
+ 'test',
+ context,
+ );
+ assert.ok(getTextContent(content[0]));
+ t.assert.snapshot?.(getTextContent(content[0]));
+ t.assert.snapshot?.(
+ JSON.stringify(
+ stabilizeStructuredContent(structuredContent),
+ null,
+ 2,
+ ),
+ );
+ },
+ {args: ['--enable-features=WebMCPTesting,DevToolsWebMCPSupport']},
+ {experimentalWebmcp: true} as ParsedArguments,
+ );
+ });
+
+ it('list no webmcp tools if experimentalWebmcp is false', async t => {
+ await testIncludesWebmcpTools(
+ t,
+ {experimentalWebmcp: false} as ParsedArguments,
+ async (response, context) => {
+ await navigatePage.handler(
+ {
+ params: {type: 'url', url: 'about:blank'},
+ page: context.getSelectedMcpPage(),
+ },
+ response,
+ context,
+ );
+ },
+ 'navigate_page',
+ );
+ });
+});
diff --git a/tests/tools/webmcp.test.ts b/tests/tools/webmcp.test.ts
new file mode 100644
index 000000000..e2a7c6a45
--- /dev/null
+++ b/tests/tools/webmcp.test.ts
@@ -0,0 +1,40 @@
+/**
+ * @license
+ * Copyright 2026 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import assert from 'node:assert';
+import {describe, it} from 'node:test';
+
+import {listPages, navigatePage, selectPage} from '../../src/tools/pages.js';
+import {withMcpContext} from '../utils.js';
+
+describe('webmcp', () => {
+ it('list webmcp tools in navigate_page response', async () => {
+ await withMcpContext(async (response, context) => {
+ await navigatePage.handler(
+ {params: {url: 'about:blank'}, page: context.getSelectedMcpPage()},
+ response,
+ context,
+ );
+ assert.ok(response.listWebMcpTools);
+ });
+ });
+
+ it('list webmcp tools in list_pages response', async () => {
+ await withMcpContext(async (response, context) => {
+ await listPages().handler({params: {}}, response, context);
+ assert.ok(response.listWebMcpTools);
+ });
+ });
+
+ it('list webmcp tools in select_page response', async () => {
+ await withMcpContext(async (response, context) => {
+ const pageId =
+ context.getPageId(context.getSelectedMcpPage().pptrPage) ?? 1;
+ await selectPage.handler({params: {pageId}}, response, context);
+ assert.ok(response.listWebMcpTools);
+ });
+ });
+});
diff --git a/tests/utils.ts b/tests/utils.ts
index f764fa9ce..6d4668fbf 100644
--- a/tests/utils.ts
+++ b/tests/utils.ts
@@ -64,6 +64,7 @@ export async function withBrowser(
debug?: boolean;
autoOpenDevTools?: boolean;
executablePath?: string;
+ args?: string[];
} = {},
) {
const launchOptions: LaunchOptions = {
@@ -74,7 +75,7 @@ export async function withBrowser(
devtools: options.autoOpenDevTools ?? false,
pipe: true,
handleDevToolsAsPage: true,
- args: ['--screen-info={3840x2160}'],
+ args: [...(options.args || []), '--screen-info={3840x2160}'],
enableExtensions: true,
};
const key = JSON.stringify(launchOptions);
@@ -104,6 +105,7 @@ export async function withMcpContext(
autoOpenDevTools?: boolean;
performanceCrux?: boolean;
executablePath?: string;
+ args?: string[];
} = {},
args: ParsedArguments = {} as ParsedArguments,
) {
From 7a487816514f6d635e0e09295140e2a7245e888d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fran=C3=A7ois=20Beaufort?=
Date: Thu, 16 Apr 2026 11:33:32 +0200
Subject: [PATCH 25/26] feat(webmcp): Add experimental tool to execute WebMCP
tool (#1873)
@OrKoN
---
README.md | 4 +
src/bin/chrome-devtools-mcp-cli-options.ts | 4 +-
src/telemetry/tool_call_metrics.json | 13 ++
src/tools/webmcp.ts | 48 ++++++++
tests/index.test.ts | 15 +++
tests/tools/webmcp.test.ts | 132 +++++++++++++++++----
6 files changed, 194 insertions(+), 22 deletions(-)
diff --git a/README.md b/README.md
index ebb3a4b09..cdc842d46 100644
--- a/README.md
+++ b/README.md
@@ -584,6 +584,10 @@ The Chrome DevTools MCP server supports the following configuration option:
Exposes experimental screencast tools (requires ffmpeg). Install ffmpeg https://www.ffmpeg.org/download.html and ensure it is available in the MCP server PATH.
- **Type:** boolean
+- **`--experimentalWebmcp`/ `--experimental-webmcp`**
+ Set to true to enable debugging WebMCP tools. Requires Chrome 149+ with the following flags: `--enable-features=WebMCPTesting,DevToolsWebMCPSupport`
+ - **Type:** boolean
+
- **`--chromeArg`/ `--chrome-arg`**
Additional arguments for Chrome. Only applies when Chrome is launched by chrome-devtools-mcp.
- **Type:** array
diff --git a/src/bin/chrome-devtools-mcp-cli-options.ts b/src/bin/chrome-devtools-mcp-cli-options.ts
index 2b2a50c6b..c08038d12 100644
--- a/src/bin/chrome-devtools-mcp-cli-options.ts
+++ b/src/bin/chrome-devtools-mcp-cli-options.ts
@@ -187,8 +187,8 @@ export const cliOptions = {
},
experimentalWebmcp: {
type: 'boolean',
- describe: 'Set to true to enable debugging WebMCP tools.',
- hidden: true,
+ describe:
+ 'Set to true to enable debugging WebMCP tools. Requires Chrome 149+ with the following flags: `--enable-features=WebMCPTesting,DevToolsWebMCPSupport`',
},
chromeArg: {
type: 'array',
diff --git a/src/telemetry/tool_call_metrics.json b/src/telemetry/tool_call_metrics.json
index c3e2c80fe..cdaff57d5 100644
--- a/src/telemetry/tool_call_metrics.json
+++ b/src/telemetry/tool_call_metrics.json
@@ -543,5 +543,18 @@
{
"name": "list_webmcp_tools",
"args": []
+ },
+ {
+ "name": "execute_webmcp_tool",
+ "args": [
+ {
+ "name": "tool_name_length",
+ "argType": "number"
+ },
+ {
+ "name": "input_length",
+ "argType": "number"
+ }
+ ]
}
]
diff --git a/src/tools/webmcp.ts b/src/tools/webmcp.ts
index e52a5ac60..d7f06afec 100644
--- a/src/tools/webmcp.ts
+++ b/src/tools/webmcp.ts
@@ -4,6 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
+import {zod} from '../third_party/index.js';
+
import {ToolCategory} from './categories.js';
import {definePageTool} from './ToolDefinition.js';
@@ -20,3 +22,49 @@ export const listWebMcpTools = definePageTool({
response.setListWebMcpTools();
},
});
+
+export const executeWebMcpTool = definePageTool({
+ name: 'execute_webmcp_tool',
+ description: `Executes a WebMCP tool exposed by the page.`,
+ annotations: {
+ category: ToolCategory.DEBUGGING,
+ readOnlyHint: false,
+ conditions: ['experimentalWebmcp'],
+ },
+ schema: {
+ toolName: zod.string().describe('The name of the WebMCP tool to execute'),
+ input: zod
+ .string()
+ .optional()
+ .describe('The JSON-stringified parameters to pass to the WebMCP tool'),
+ },
+ handler: async (request, response) => {
+ const toolName = request.params.toolName;
+
+ let input: Record = {};
+ if (request.params.input) {
+ try {
+ const parsed = JSON.parse(request.params.input);
+ if (typeof parsed === 'object' && parsed !== null) {
+ input = parsed;
+ } else {
+ throw new Error('Parsed input is not an object');
+ }
+ } catch (e) {
+ const errorMessage = e instanceof Error ? e.message : String(e);
+ throw new Error(`Failed to parse input as JSON: ${errorMessage}`);
+ }
+ }
+
+ const tools = request.page.pptrPage.webmcp.tools();
+ const tool = tools.find(t => t.name === toolName);
+ if (!tool) {
+ throw new Error(`Tool ${toolName} not found`);
+ }
+
+ const {status, output, errorText} = await tool.execute(input);
+ response.appendResponseLine(
+ JSON.stringify({status, output, errorText}, null, 2),
+ );
+ },
+});
diff --git a/tests/index.test.ts b/tests/index.test.ts
index f08350c08..6fc08dc66 100644
--- a/tests/index.test.ts
+++ b/tests/index.test.ts
@@ -162,4 +162,19 @@ describe('e2e', () => {
['--experimental-interop-tools'],
);
});
+
+ it('has experimental webmcp', async () => {
+ await withClient(
+ async client => {
+ const {tools} = await client.listTools();
+ const listWebMcpTools = tools.find(t => t.name === 'list_webmcp_tools');
+ const executeWebMcpTool = tools.find(
+ t => t.name === 'execute_webmcp_tool',
+ );
+ assert.ok(listWebMcpTools);
+ assert.ok(executeWebMcpTool);
+ },
+ ['--experimental-webmcp'],
+ );
+ });
});
diff --git a/tests/tools/webmcp.test.ts b/tests/tools/webmcp.test.ts
index e2a7c6a45..fcbe5f3e1 100644
--- a/tests/tools/webmcp.test.ts
+++ b/tests/tools/webmcp.test.ts
@@ -7,34 +7,126 @@
import assert from 'node:assert';
import {describe, it} from 'node:test';
+import type {ParsedArguments} from '../../src/bin/chrome-devtools-mcp-cli-options.js';
+import type {McpPage} from '../../src/McpPage.js';
import {listPages, navigatePage, selectPage} from '../../src/tools/pages.js';
-import {withMcpContext} from '../utils.js';
+import {executeWebMcpTool} from '../../src/tools/webmcp.js';
+import {html, withMcpContext} from '../utils.js';
describe('webmcp', () => {
- it('list webmcp tools in navigate_page response', async () => {
- await withMcpContext(async (response, context) => {
- await navigatePage.handler(
- {params: {url: 'about:blank'}, page: context.getSelectedMcpPage()},
- response,
- context,
- );
- assert.ok(response.listWebMcpTools);
+ describe('list_webmcp_tools', () => {
+ it('list webmcp tools in navigate_page response', async () => {
+ await withMcpContext(async (response, context) => {
+ await navigatePage.handler(
+ {params: {url: 'about:blank'}, page: context.getSelectedMcpPage()},
+ response,
+ context,
+ );
+ assert.ok(response.listWebMcpTools);
+ });
+ });
+
+ it('list webmcp tools in list_pages response', async () => {
+ await withMcpContext(async (response, context) => {
+ await listPages().handler({params: {}}, response, context);
+ assert.ok(response.listWebMcpTools);
+ });
});
- });
- it('list webmcp tools in list_pages response', async () => {
- await withMcpContext(async (response, context) => {
- await listPages().handler({params: {}}, response, context);
- assert.ok(response.listWebMcpTools);
+ it('list webmcp tools in select_page response', async () => {
+ await withMcpContext(async (response, context) => {
+ const pageId =
+ context.getPageId(context.getSelectedMcpPage().pptrPage) ?? 1;
+ await selectPage.handler({params: {pageId}}, response, context);
+ assert.ok(response.listWebMcpTools);
+ });
});
});
- it('list webmcp tools in select_page response', async () => {
- await withMcpContext(async (response, context) => {
- const pageId =
- context.getPageId(context.getSelectedMcpPage().pptrPage) ?? 1;
- await selectPage.handler({params: {pageId}}, response, context);
- assert.ok(response.listWebMcpTools);
+ describe('execute_webmcp_tool', () => {
+ async function setupWebMcpTool(page: McpPage) {
+ await page.pptrPage.setContent(
+ html``,
+ );
+ }
+
+ // TODO: Remove `.skip` once Chrome 149 reaches stable channel.
+ it.skip('executes a tool successfully', async () => {
+ await withMcpContext(
+ async (response, context) => {
+ const page = context.getSelectedMcpPage();
+ await setupWebMcpTool(page);
+
+ await executeWebMcpTool.handler(
+ {params: {toolName: 'test_tool', input: JSON.stringify({})}, page},
+ response,
+ context,
+ );
+ assert.strictEqual(
+ response.responseLines[0],
+ JSON.stringify({status: 'Completed', output: 'hello'}, null, 2),
+ );
+ },
+ {args: ['--enable-features=WebMCPTesting,DevToolsWebMCPSupport']},
+ {experimentalWebmcp: true} as ParsedArguments,
+ );
+ });
+
+ it('throws if tool is not found', async () => {
+ await withMcpContext(
+ async (response, context) => {
+ await assert.rejects(
+ async () => {
+ await executeWebMcpTool.handler(
+ {
+ params: {toolName: 'missing-tool', input: JSON.stringify({})},
+ page: context.getSelectedMcpPage(),
+ },
+ response,
+ context,
+ );
+ },
+ {message: /Tool missing-tool not found/},
+ );
+ },
+ {args: ['--enable-features=WebMCPTesting,DevToolsWebMCPSupport']},
+ {experimentalWebmcp: true} as ParsedArguments,
+ );
+ });
+
+ it('throws if input is invalid', async () => {
+ await withMcpContext(
+ async (response, context) => {
+ await assert.rejects(
+ async () => {
+ const page = context.getSelectedMcpPage();
+ await setupWebMcpTool(page);
+
+ await executeWebMcpTool.handler(
+ {params: {toolName: 'test_tool', input: 'invalid'}, page},
+ response,
+ context,
+ );
+ },
+ {
+ message:
+ /Failed to parse input as JSON: Unexpected token 'i', "invalid" is not valid JSON/,
+ },
+ );
+ },
+ {args: ['--enable-features=WebMCPTesting,DevToolsWebMCPSupport']},
+ {experimentalWebmcp: true} as ParsedArguments,
+ );
});
});
});
From b17473865f6298d3872f0d0487aae3813c02e4b6 Mon Sep 17 00:00:00 2001
From: Wolfgang Beyer
Date: Thu, 9 Apr 2026 08:47:47 +0000
Subject: [PATCH 26/26] in-page tool output: handle DOM elements and limit
depth
---
src/McpContext.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/McpContext.ts b/src/McpContext.ts
index 0e10e2231..d98376bd0 100644
--- a/src/McpContext.ts
+++ b/src/McpContext.ts
@@ -38,6 +38,7 @@ import type {
DevToolsData,
ContextPage,
SupportedExtensions,
+ ContextPage,
} from './tools/ToolDefinition.js';
import type {TraceResult} from './trace-processing/parse.js';
import type {