Skip to content

Commit c85a268

Browse files
authored
Merge branch 'main' into orkon/release-cli
2 parents 8263f84 + d95e9ba commit c85a268

15 files changed

Lines changed: 244 additions & 74 deletions

docs/cli.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Chrome DevTools CLI
22

3-
The `chrome-devtools-mcp` package includes a CLI interface that allows you to interact with the browser directly from your terminal. This is particularly useful for debugging or when you want an agent to generate scripts that automate browser actions.
3+
The `chrome-devtools-mcp` package includes an **experimental** CLI interface that allows you to interact with the browser directly from your terminal. This is particularly useful for debugging or when you want an agent to generate scripts that automate browser actions.
44

55
## Getting started
66

@@ -13,7 +13,7 @@ chrome-devtools status # check if install worked.
1313

1414
## How it works
1515

16-
The CLI acts as a client to a background `chrome-devtools-mcp` daemon.
16+
The CLI acts as a client to a background `chrome-devtools-mcp` daemon (uses Unix sockets on Linux/Mac and named pipes on Windows).
1717

1818
- **Automatic Start**: The first time you call a tool (e.g., `list_pages`), the CLI automatically starts the MCP server and the browser in the background if they aren't already running.
1919
- **Persistence**: The same background instance is reused for subsequent commands, preserving the browser state (open pages, cookies, etc.).

docs/tool-reference.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<!-- AUTO GENERATED DO NOT EDIT - run 'npm run docs' to update-->
22

3-
# Chrome DevTools MCP Tool Reference (~6915 cl100k_base tokens)
3+
# Chrome DevTools MCP Tool Reference (~6927 cl100k_base tokens)
44

55
- **[Input automation](#input-automation)** (9 tools)
66
- [`click`](#click)
@@ -165,7 +165,7 @@
165165

166166
### `navigate_page`
167167

168-
**Description:** Navigates the currently selected page to a URL.
168+
**Description:** Go to a URL, or back, forward, or reload. Use project URL if not specified otherwise.
169169

170170
**Parameters:**
171171

@@ -180,7 +180,7 @@
180180

181181
### `new_page`
182182

183-
**Description:** Creates a new page
183+
**Description:** Open a new tab and load a URL. Use project URL if not specified otherwise.
184184

185185
**Parameters:**
186186

@@ -256,7 +256,7 @@
256256

257257
### `performance_start_trace`
258258

259-
**Description:** Starts a performance trace recording on the selected page. This can be used to look for performance problems and insights to improve the performance of the page. It will also report Core Web Vital (CWV) scores for the page.
259+
**Description:** Start a performance trace on the selected webpage. Use to find frontend performance issues, Core Web Vitals (LCP, INP, CLS), and improve page load speed.
260260

261261
**Parameters:**
262262

@@ -268,7 +268,7 @@
268268

269269
### `performance_stop_trace`
270270

271-
**Description:** Stops the active performance trace recording on the selected page.
271+
**Description:** Stop the active performance trace recording on the selected webpage.
272272

273273
**Parameters:**
274274

package-lock.json

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*
6+
* Eval scenario: user asks to fix issues with their webpage (no URL given).
7+
* When no URL is provided, the model should pick the current frontend and run
8+
* and inspect it. Verifies the MCP server is invoked and the model opens the
9+
* frontend and inspects it (snapshot, console, or network).
10+
*
11+
* Note: Tools like performance_start_trace, take_snapshot, list_console_messages,
12+
* and list_network_requests do not require a URL in the prompt—they operate on
13+
* the currently selected page. Only navigate_page/new_page need a URL to open
14+
* a page; the eval runner injects the test URL when htmlRoute is set.
15+
*/
16+
17+
import assert from 'node:assert';
18+
19+
import type {TestScenario} from '../eval_gemini.ts';
20+
21+
const INSPECTION_TOOLS = [
22+
'take_snapshot',
23+
'list_console_messages',
24+
'list_network_requests',
25+
];
26+
27+
export const scenario: TestScenario = {
28+
prompt: 'Can you fix issues with my webpage?',
29+
maxTurns: 4,
30+
htmlRoute: {
31+
path: '/fix_issues_test.html',
32+
htmlContent: `
33+
<h1>Test Page</h1>
34+
<p>Some content</p>
35+
<script>
36+
console.error('Intentional error for testing');
37+
</script>
38+
`,
39+
},
40+
expectations: calls => {
41+
const NAVIGATION_TOOLS = ['navigate_page', 'new_page'];
42+
assert.ok(
43+
calls.length >= 2,
44+
'Expected at least navigation and one inspection',
45+
);
46+
const navigationIndex = calls.findIndex(c =>
47+
NAVIGATION_TOOLS.includes(c.name),
48+
);
49+
assert.ok(
50+
navigationIndex !== -1,
51+
`Expected a navigation call (${NAVIGATION_TOOLS.join(' or ')}), got: ${calls.map(c => c.name).join(', ')}`,
52+
);
53+
const afterNavigation = calls.slice(navigationIndex + 1);
54+
const inspectionCalls = afterNavigation.filter(c =>
55+
INSPECTION_TOOLS.includes(c.name),
56+
);
57+
assert.ok(
58+
inspectionCalls.length >= 1,
59+
`Expected at least one inspection tool (${INSPECTION_TOOLS.join(', ')}) after navigation, got: ${calls.map(c => c.name).join(', ')}`,
60+
);
61+
},
62+
};
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*
6+
* Eval scenario using "website"/"webpage" wording to verify the model invokes
7+
* the right tools when users ask to open a site and read its content.
8+
*/
9+
10+
import assert from 'node:assert';
11+
12+
import type {TestScenario} from '../eval_gemini.ts';
13+
14+
export const scenario: TestScenario = {
15+
prompt:
16+
'Open the website at <TEST_URL> and tell me what content is on the page.',
17+
maxTurns: 3,
18+
htmlRoute: {
19+
path: '/frontend_snapshot.html',
20+
htmlContent: '<h1>Frontend Test</h1><p>This is a test webpage.</p>',
21+
},
22+
expectations: calls => {
23+
assert.strictEqual(calls.length, 2);
24+
assert.ok(
25+
calls[0].name === 'navigate_page' || calls[0].name === 'new_page',
26+
'First call should be navigation',
27+
);
28+
assert.strictEqual(
29+
calls[1].name,
30+
'take_snapshot',
31+
'Second call should be take_snapshot to read page content',
32+
);
33+
},
34+
};

src/McpContext.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
*/
66

77
import fs from 'node:fs/promises';
8-
import os from 'node:os';
98
import path from 'node:path';
109

1110
import type {TargetUniverse} from './DevtoolsUtils.js';
@@ -47,6 +46,7 @@ import {
4746
ExtensionRegistry,
4847
type InstalledExtension,
4948
} from './utils/ExtensionRegistry.js';
49+
import {saveTemporaryFile} from './utils/files.js';
5050
import {WaitForHelper} from './WaitForHelper.js';
5151

5252
export type {
@@ -799,18 +799,7 @@ export class McpContext implements Context {
799799
data: Uint8Array<ArrayBufferLike>,
800800
filename: string,
801801
): Promise<{filepath: string}> {
802-
try {
803-
const dir = await fs.mkdtemp(
804-
path.join(os.tmpdir(), 'chrome-devtools-mcp-'),
805-
);
806-
807-
const filepath = path.join(dir, filename);
808-
await fs.writeFile(filepath, data);
809-
return {filepath};
810-
} catch (err) {
811-
this.logger(err);
812-
throw new Error('Could not save a file', {cause: err});
813-
}
802+
return await saveTemporaryFile(data, filename);
814803
}
815804
async saveFile(
816805
data: Uint8Array<ArrayBufferLike>,

src/bin/chrome-devtools.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,23 @@ y.command(
7171
y.command('status', 'Checks if chrome-devtools-mcp is running', async () => {
7272
if (isDaemonRunning()) {
7373
console.log('chrome-devtools-mcp daemon is running.');
74+
const response = await sendCommand({
75+
method: 'status',
76+
});
77+
if (response.success) {
78+
const data = JSON.parse(response.result) as {
79+
pid: number | null;
80+
socketPath: string;
81+
startDate: string;
82+
version: string;
83+
};
84+
console.log(
85+
`pid=${data.pid} socket=${data.socketPath} start-date=${data.startDate} version=${data.version}`,
86+
);
87+
} else {
88+
console.error('Error:', response.error);
89+
process.exit(1);
90+
}
7491
} else {
7592
console.log('chrome-devtools-mcp daemon is not running.');
7693
}
@@ -108,9 +125,9 @@ for (const [commandName, commandDef] of Object.entries(commands)) {
108125
commandStr,
109126
commandDef.description,
110127
y => {
111-
y.option('format', {
112-
choices: ['text', 'json'],
113-
default: 'text',
128+
y.option('output-format', {
129+
choices: ['md', 'json'],
130+
default: 'md',
114131
});
115132
for (const [argName, opt] of Object.entries(args)) {
116133
const type =
@@ -170,9 +187,9 @@ for (const [commandName, commandDef] of Object.entries(commands)) {
170187

171188
if (response.success) {
172189
console.log(
173-
handleResponse(
190+
await handleResponse(
174191
JSON.parse(response.result) as unknown as CallToolResult,
175-
argv['format'] as 'json' | 'text',
192+
argv['output-format'] as 'json' | 'md',
176193
),
177194
);
178195
} else {

src/daemon/client.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import net from 'node:net';
1111
import {logger} from '../logger.js';
1212
import type {CallToolResult} from '../third_party/index.js';
1313
import {PipeTransport} from '../third_party/index.js';
14+
import {saveTemporaryFile} from '../utils/files.js';
1415

1516
import type {DaemonMessage, DaemonResponse} from './types.js';
1617
import {
@@ -144,10 +145,10 @@ export async function stopDaemon() {
144145
await waitForFile(pidFilePath, /*removed=*/ true);
145146
}
146147

147-
export function handleResponse(
148+
export async function handleResponse(
148149
response: CallToolResult,
149-
format: 'json' | 'text',
150-
): string {
150+
format: 'json' | 'md',
151+
): Promise<string> {
151152
if (response.isError) {
152153
return JSON.stringify(response.content);
153154
}
@@ -161,9 +162,26 @@ export function handleResponse(
161162
for (const content of response.content) {
162163
if (content.type === 'text') {
163164
chunks.push(content.text);
165+
} else if (content.type === 'image') {
166+
const imageData = content.data;
167+
const mimeType = content.mimeType;
168+
let extension = '.png';
169+
switch (mimeType) {
170+
case 'image/jpg':
171+
case 'image/jpeg':
172+
extension = '.jpeg';
173+
break;
174+
case 'webp':
175+
extension = '.webp';
176+
break;
177+
}
178+
const data = Buffer.from(imageData, 'base64');
179+
const name = crypto.randomUUID();
180+
const {filepath} = await saveTemporaryFile(data, `${name}${extension}`);
181+
chunks.push(`Saved to ${filepath}.`);
164182
} else {
165183
throw new Error('Not supported response content type');
166184
}
167185
}
168-
return format === 'text' ? chunks.join('') : JSON.stringify(chunks);
186+
return format === 'md' ? chunks.join(' ') : JSON.stringify(chunks);
169187
}

src/daemon/daemon.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ logger(`Writing ${process.pid.toString()} to ${pidFilePath}`);
4343

4444
const socketPath = getSocketPath();
4545

46+
const startDate = new Date();
47+
4648
let mcpClient: Client | null = null;
4749
let mcpTransport: StdioClientTransport | null = null;
4850
let server: Server | null = null;
@@ -108,7 +110,18 @@ async function handleRequest(msg: DaemonMessage) {
108110
success: true,
109111
message: 'stopping',
110112
};
111-
} else {
113+
} else if (msg.method === 'status') {
114+
return {
115+
success: true,
116+
result: JSON.stringify({
117+
pid: process.pid,
118+
socketPath,
119+
startDate: startDate.toISOString(),
120+
version: VERSION,
121+
}),
122+
};
123+
}
124+
{
112125
return {
113126
success: false,
114127
error: `Unknown method: ${JSON.stringify(msg, null, 2)}`,

src/daemon/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ export type DaemonMessage =
88
| {
99
method: 'stop';
1010
}
11+
| {
12+
method: 'status';
13+
}
1114
| {
1215
method: 'invoke_tool';
1316
tool: string;

0 commit comments

Comments
 (0)