Skip to content

Commit 83046c8

Browse files
authored
Merge branch 'main' into feat/webperf-core-web-vitals-skill
2 parents 24e4171 + 06b331f commit 83046c8

32 files changed

Lines changed: 673 additions & 1053 deletions

package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"typecheck": "tsc --noEmit",
1717
"format": "eslint --cache --fix . && prettier --write --cache .",
1818
"check-format": "eslint --cache . && prettier --check --cache .;",
19-
"gen": "npm run build && npm run docs:generate && npm run cli:generate && npm run update-tool-call-metrics && npm run update-flag-usage-metrics && npm run format",
19+
"gen": "npm run build && npm run docs:generate && npm run cli:generate && npm run update-metrics && npm run format",
2020
"docs:generate": "node --experimental-strip-types scripts/generate-docs.ts",
2121
"start": "npm run build && node build/src/index.js",
2222
"start-debug": "DEBUG=mcp:* DEBUG_COLORS=false npm run build && node build/src/index.js",
@@ -27,8 +27,7 @@
2727
"prepare": "node --experimental-strip-types scripts/prepare.ts",
2828
"verify-server-json-version": "node --experimental-strip-types scripts/verify-server-json-version.ts",
2929
"update-lighthouse": "node --experimental-strip-types scripts/update-lighthouse.ts",
30-
"update-tool-call-metrics": "node --experimental-strip-types scripts/update_tool_call_metrics.ts",
31-
"update-flag-usage-metrics": "node --experimental-strip-types scripts/update_flag_usage_metrics.ts",
30+
"update-metrics": "node --experimental-strip-types scripts/update_metrics.ts",
3231
"verify-npm-package": "node scripts/verify-npm-package.mjs",
3332
"eval": "npm run build && node --experimental-strip-types scripts/eval_gemini.ts",
3433
"count-tokens": "node --experimental-strip-types scripts/count_tokens.ts"
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import assert from 'node:assert';
8+
9+
import type {TestScenario} from '../eval_gemini.ts';
10+
11+
export const scenario: TestScenario = {
12+
prompt:
13+
'Go to <TEST_URL>, fill the form with size = 2 CPUs and components = [docker, nginx].',
14+
maxTurns: 3,
15+
htmlRoute: {
16+
path: '/input_test.html',
17+
htmlContent: `
18+
<form action="/post" method="POST">
19+
<div>
20+
<label for="size">CPU/Memory size:</label>
21+
<select id="size" name="size" required>
22+
<option value="small">1 vCPU, 2GB RAM</option>
23+
<option value="medium">2 vCPU, 4GB RAM</option>
24+
<option value="large">4 vCPU, 8GB RAM</option>
25+
</select>
26+
</div>
27+
<br>
28+
<div>
29+
<p>Pre-installed components:</p>
30+
<input type="checkbox" id="docker" name="components" value="docker">
31+
<label for="docker">Docker</label><br>
32+
<input type="checkbox" id="nodejs" name="components" value="nodejs">
33+
<label for="nodejs">Node.js</label><br>
34+
<input type="checkbox" id="python" name="components" value="python">
35+
<label for="python">Python</label><br>
36+
<input type="checkbox" id="nginx" name="components" value="nginx">
37+
<label for="nginx">Nginx</label>
38+
</div>
39+
<button type="submit">Spawn Server</button>
40+
</form>
41+
`,
42+
},
43+
expectations: calls => {
44+
assert.strictEqual(calls.length, 3);
45+
assert.ok(
46+
calls[0].name === 'navigate_page' || calls[0].name === 'new_page',
47+
);
48+
assert.strictEqual(calls[1].name, 'take_snapshot');
49+
assert.strictEqual(calls[2].name, 'fill_form');
50+
51+
const elements = calls[2].args.elements as Array<{
52+
uid: string;
53+
value: string;
54+
}>;
55+
assert.strictEqual(elements.length, 3);
56+
57+
const uids = new Set(elements.map(e => e.uid));
58+
assert.strictEqual(
59+
uids.size,
60+
3,
61+
'fill_form should target three distinct elements',
62+
);
63+
64+
const values = elements.map(e => e.value).sort();
65+
assert.deepStrictEqual(values, ['2 vCPU, 4GB RAM', 'true', 'true']);
66+
},
67+
};

scripts/generate-cli.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {createTools} from '../build/src/tools/tools.js';
1616

1717
const OUTPUT_PATH = path.join(
1818
import.meta.dirname,
19-
'../src/bin/cliDefinitions.ts',
19+
'../src/bin/chrome-devtools-cli-options.ts',
2020
);
2121

2222
async function fetchTools() {

scripts/update_flag_usage_metrics.ts

Lines changed: 0 additions & 36 deletions
This file was deleted.
Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,23 @@
77
import * as fs from 'node:fs';
88
import * as path from 'node:path';
99

10-
import type {ParsedArguments} from '../build/src/bin/chrome-devtools-mcp-cli-options.js';
1110
import {
11+
cliOptions,
12+
parseArguments,
13+
} from '../build/src/bin/chrome-devtools-mcp-cli-options.js';
14+
import {
15+
getPossibleFlagMetrics,
16+
type FlagMetric,
17+
} from '../build/src/telemetry/flagUtils.js';
18+
import {
19+
applyToExisting,
1220
applyToExistingMetrics,
1321
generateToolMetrics,
1422
type ToolMetric,
1523
} from '../build/src/telemetry/toolMetricsUtils.js';
16-
import type {ToolDefinition} from '../build/src/tools/ToolDefinition.js';
1724
import {createTools} from '../build/src/tools/tools.js';
1825

19-
export function HaveUniqueNames(tools: ToolDefinition[]): boolean {
26+
export function HaveUniqueNames(tools: Array<{name: string}>): boolean {
2027
const toolNames = tools.map(tool => tool.name);
2128
const toolNamesSet = new Set(toolNames);
2229
return toolNamesSet.size === toolNames.length;
@@ -30,8 +37,9 @@ function writeToolCallMetricsConfig() {
3037
throw new Error(`Error: Directory ${dir} does not exist.`);
3138
}
3239

33-
const fullTools = createTools({slim: false} as ParsedArguments);
34-
const slimTools = createTools({slim: true} as ParsedArguments);
40+
// Avoid 'as ParsedArguments' by using parseArguments
41+
const fullTools = createTools(parseArguments('0.0.0', ['', '']));
42+
const slimTools = createTools(parseArguments('0.0.0', ['', '', '--slim']));
3543

3644
const allTools = [...fullTools, ...slimTools];
3745

@@ -62,4 +70,43 @@ function writeToolCallMetricsConfig() {
6270
);
6371
}
6472

65-
writeToolCallMetricsConfig();
73+
function writeFlagUsageMetrics() {
74+
const outputPath = path.resolve('src/telemetry/flag_usage_metrics.json');
75+
76+
const dir = path.dirname(outputPath);
77+
if (!fs.existsSync(dir)) {
78+
throw new Error(`Error: Directory ${dir} does not exist.`);
79+
}
80+
81+
let existingMetrics: FlagMetric[] = [];
82+
if (fs.existsSync(outputPath)) {
83+
try {
84+
existingMetrics = JSON.parse(
85+
fs.readFileSync(outputPath, 'utf8'),
86+
) as FlagMetric[];
87+
} catch {
88+
console.warn(
89+
`Warning: Failed to parse existing metrics from ${outputPath}. Starting fresh.`,
90+
);
91+
}
92+
}
93+
94+
const newMetrics = getPossibleFlagMetrics(cliOptions);
95+
const mergedMetrics = applyToExisting<FlagMetric>(
96+
existingMetrics,
97+
newMetrics,
98+
);
99+
100+
fs.writeFileSync(outputPath, JSON.stringify(mergedMetrics, null, 2) + '\n');
101+
102+
console.log(
103+
`Successfully wrote ${mergedMetrics.length} flag usage metrics to ${outputPath}`,
104+
);
105+
}
106+
107+
function main() {
108+
writeToolCallMetricsConfig();
109+
writeFlagUsageMetrics();
110+
}
111+
112+
main();

src/McpContext.ts

Lines changed: 64 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import fs from 'node:fs/promises';
88
import path from 'node:path';
9+
import {fileURLToPath} from 'node:url';
910

1011
import type {TargetUniverse} from './DevtoolsUtils.js';
1112
import {UniverseManager} from './DevtoolsUtils.js';
@@ -18,21 +19,22 @@ import {
1819
type ListenerMap,
1920
type UncaughtError,
2021
} from './PageCollector.js';
21-
import type {
22-
Browser,
23-
BrowserContext,
24-
ConsoleMessage,
25-
Debugger,
26-
HTTPRequest,
27-
Page,
28-
ScreenRecorder,
29-
Viewport,
30-
Target,
31-
Extension,
22+
import {
23+
Locator,
24+
PredefinedNetworkConditions,
25+
type Browser,
26+
type BrowserContext,
27+
type ConsoleMessage,
28+
type Debugger,
29+
type HTTPRequest,
30+
type Page,
31+
type ScreenRecorder,
32+
type Viewport,
33+
type Target,
34+
type Extension,
35+
type Root,
36+
type DevTools,
3237
} from './third_party/index.js';
33-
import type {DevTools} from './third_party/index.js';
34-
import {Locator} from './third_party/index.js';
35-
import {PredefinedNetworkConditions} from './third_party/index.js';
3638
import {listPages} from './tools/pages.js';
3739
import {CLOSE_PAGE_ERROR} from './tools/ToolDefinition.js';
3840
import type {Context, SupportedExtensions} from './tools/ToolDefinition.js';
@@ -42,7 +44,7 @@ import type {
4244
GeolocationOptions,
4345
ExtensionServiceWorker,
4446
} from './types.js';
45-
import {ensureExtension, saveTemporaryFile} from './utils/files.js';
47+
import {ensureExtension, getTempFilePath} from './utils/files.js';
4648
import {getNetworkMultiplierFromString} from './WaitForHelper.js';
4749

4850
interface McpContextOptions {
@@ -90,6 +92,7 @@ export class McpContext implements Context {
9092
#locatorClass: typeof Locator;
9193
#options: McpContextOptions;
9294
#heapSnapshotManager = new HeapSnapshotManager();
95+
#roots: Root[] | undefined = undefined;
9396

9497
private constructor(
9598
browser: Browser,
@@ -154,6 +157,37 @@ export class McpContext implements Context {
154157
return context;
155158
}
156159

160+
roots(): Root[] | undefined {
161+
return this.#roots;
162+
}
163+
164+
setRoots(roots: Root[] | undefined): void {
165+
this.#roots = roots;
166+
}
167+
168+
validatePath(filePath?: string): void {
169+
if (filePath === undefined) {
170+
return;
171+
}
172+
const roots = this.roots();
173+
if (roots === undefined) {
174+
return;
175+
}
176+
const absolutePath = path.resolve(filePath);
177+
for (const root of roots) {
178+
const rootPath = path.resolve(fileURLToPath(root.uri));
179+
if (
180+
absolutePath === rootPath ||
181+
absolutePath.startsWith(rootPath + path.sep)
182+
) {
183+
return;
184+
}
185+
}
186+
throw new Error(
187+
`Access denied: path ${filePath} is not within any of the workspace roots ${JSON.stringify(roots)}.`,
188+
);
189+
}
190+
157191
resolveCdpRequestId(page: McpPage, cdpRequestId: string): number | undefined {
158192
if (!cdpRequestId) {
159193
this.logger('no network request');
@@ -643,13 +677,22 @@ export class McpContext implements Context {
643677
data: Uint8Array<ArrayBufferLike>,
644678
filename: string,
645679
): Promise<{filepath: string}> {
646-
return await saveTemporaryFile(data, filename);
680+
const filepath = await getTempFilePath(filename);
681+
this.validatePath(filepath);
682+
try {
683+
await fs.writeFile(filepath, data);
684+
} catch (err) {
685+
throw new Error('Could not save a file', {cause: err});
686+
}
687+
return {filepath};
647688
}
689+
648690
async saveFile(
649691
data: Uint8Array<ArrayBufferLike>,
650692
clientProvidedFilePath: string,
651693
extension: SupportedExtensions,
652694
): Promise<{filename: string}> {
695+
this.validatePath(clientProvidedFilePath);
653696
try {
654697
const filePath = ensureExtension(
655698
path.resolve(clientProvidedFilePath),
@@ -721,6 +764,7 @@ export class McpContext implements Context {
721764
}
722765

723766
async installExtension(extensionPath: string): Promise<string> {
767+
this.validatePath(extensionPath);
724768
const id = await this.browser.installExtension(extensionPath);
725769
return id;
726770
}
@@ -751,25 +795,29 @@ export class McpContext implements Context {
751795
async getHeapSnapshotAggregates(
752796
filePath: string,
753797
): Promise<Record<string, AggregatedInfoWithUid>> {
798+
this.validatePath(filePath);
754799
return await this.#heapSnapshotManager.getAggregates(filePath);
755800
}
756801

757802
async getHeapSnapshotStats(
758803
filePath: string,
759804
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.Statistics> {
805+
this.validatePath(filePath);
760806
return await this.#heapSnapshotManager.getStats(filePath);
761807
}
762808

763809
async getHeapSnapshotStaticData(
764810
filePath: string,
765811
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.StaticData | null> {
812+
this.validatePath(filePath);
766813
return await this.#heapSnapshotManager.getStaticData(filePath);
767814
}
768815

769816
async getHeapSnapshotNodesByUid(
770817
filePath: string,
771818
uid: number,
772819
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.ItemsRange> {
820+
this.validatePath(filePath);
773821
return await this.#heapSnapshotManager.getNodesByUid(filePath, uid);
774822
}
775823
}

0 commit comments

Comments
 (0)