Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -515,13 +515,14 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles
- **Network** (2 tools)
- [`get_network_request`](docs/tool-reference.md#get_network_request)
- [`list_network_requests`](docs/tool-reference.md#list_network_requests)
- **Debugging** (8 tools)
- **Debugging** (9 tools)
- [`evaluate_script`](docs/tool-reference.md#evaluate_script)
- [`get_console_message`](docs/tool-reference.md#get_console_message)
- [`lighthouse_audit`](docs/tool-reference.md#lighthouse_audit)
- [`list_console_messages`](docs/tool-reference.md#list_console_messages)
- [`take_screenshot`](docs/tool-reference.md#take_screenshot)
- [`take_snapshot`](docs/tool-reference.md#take_snapshot)
- [`list_dedicated_workers`](docs/tool-reference.md#list_dedicated_workers)
- [`screencast_start`](docs/tool-reference.md#screencast_start)
- [`screencast_stop`](docs/tool-reference.md#screencast_stop)
- **Memory** (12 tools)
Expand Down Expand Up @@ -654,6 +655,11 @@ The Chrome DevTools MCP server supports the following configuration option:
- **Type:** boolean
- **Default:** `false`

- **`--experimentalWorkers`/ `--experimental-workers`**
Whether to expose tools for enumerating dedicated Web Workers of the selected page and evaluating scripts inside them. Off by default so that worker execution contexts do not add to the baseline token usage.
- **Type:** boolean
- **Default:** `false`

- **`--experimentalScreencast`/ `--experimental-screencast`**
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
Expand Down
11 changes: 10 additions & 1 deletion docs/tool-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,14 @@
- **[Network](#network)** (2 tools)
- [`get_network_request`](#get_network_request)
- [`list_network_requests`](#list_network_requests)
- **[Debugging](#debugging)** (8 tools)
- **[Debugging](#debugging)** (9 tools)
- [`evaluate_script`](#evaluate_script)
- [`get_console_message`](#get_console_message)
- [`lighthouse_audit`](#lighthouse_audit)
- [`list_console_messages`](#list_console_messages)
- [`take_screenshot`](#take_screenshot)
- [`take_snapshot`](#take_snapshot)
- [`list_dedicated_workers`](#list_dedicated_workers)
- [`screencast_start`](#screencast_start)
- [`screencast_stop`](#screencast_stop)
- **[Memory](#memory)** (12 tools)
Expand Down Expand Up @@ -425,6 +426,14 @@ in the DevTools Elements panel (if any).

---

### `list_dedicated_workers`

**Description:** List the dedicated Web Workers running in the currently selected page. Returns a worker id for each one that can be passed as the 'workerId' argument to [`evaluate_script`](#evaluate_script) to run a script inside that worker's execution context. (requires flag: --experimentalWorkers=true)

**Parameters:** None

---

### `screencast_start`

**Description:** Starts recording a screencast (video) of the selected page in specified format. (requires flag: --experimentalScreencast=true)
Expand Down
44 changes: 43 additions & 1 deletion src/McpContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,14 @@ import {
type Extension,
type Root,
type DevTools,
type WebWorker,
} from './third_party/index.js';
import {listPages} from './tools/pages.js';
import {CLOSE_PAGE_ERROR} from './tools/ToolDefinition.js';
import type {Context, SupportedExtensions} from './tools/ToolDefinition.js';
import type {TraceResult} from './trace-processing/parse.js';
import type {Logger} from './types.js';
import type {ExtensionServiceWorker} from './types.js';
import type {DedicatedWorker, ExtensionServiceWorker} from './types.js';
import {getTempFilePath, resolveCanonicalPath} from './utils/files.js';
interface McpContextOptions {
// Whether the DevTools windows are exposed as pages for debugging of DevTools.
Expand Down Expand Up @@ -93,6 +94,10 @@ export class McpContext implements Context {
#extensionServiceWorkerMap = new WeakMap<Target, string>();
#nextExtensionServiceWorkerId = 1;

#dedicatedWorkers: DedicatedWorker[] = [];
#dedicatedWorkerMap = new WeakMap<WebWorker, string>();
#nextDedicatedWorkerId = 1;

#traceResults: TraceResult[] = [];

#locatorClass: typeof Locator;
Expand Down Expand Up @@ -572,6 +577,43 @@ export class McpContext implements Context {
return this.#extensionServiceWorkerMap.get(extensionServiceWorker.target);
}

/**
* Creates a snapshot of the dedicated Web Workers of the currently selected
* page. This is intentionally not part of the default page snapshot: worker
* execution contexts are only enumerated when a tool explicitly asks for
* them, so they don't add to the baseline token usage.
*/
createDedicatedWorkersSnapshot(): DedicatedWorker[] {
const workers = this.getSelectedMcpPage().pptrPage.workers();

for (const worker of workers) {
if (!this.#dedicatedWorkerMap.has(worker)) {
this.#dedicatedWorkerMap.set(
worker,
'worker-' + this.#nextDedicatedWorkerId++,
);
}
}

this.#dedicatedWorkers = workers.map(worker => {
return {
worker,
id: this.#dedicatedWorkerMap.get(worker)!,
url: worker.url(),
};
});

return this.#dedicatedWorkers;
}

getDedicatedWorkers(): DedicatedWorker[] {
return this.#dedicatedWorkers;
}

getDedicatedWorkerId(dedicatedWorker: DedicatedWorker): string | undefined {
return this.#dedicatedWorkerMap.get(dedicatedWorker.worker);
}

async saveTemporaryFile(
data: Uint8Array<ArrayBufferLike>,
filename: string,
Expand Down
6 changes: 6 additions & 0 deletions src/bin/chrome-devtools-cli-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -767,6 +767,12 @@ export const commands: Commands = {
},
},
},
list_dedicated_workers: {
description:
"List the dedicated Web Workers running in the currently selected page. Returns a worker id for each one that can be passed as the 'workerId' argument to evaluate_script to run a script inside that worker's execution context. (requires flag: --experimentalWorkers=true)",
category: 'Debugging',
args: {},
},
list_extensions: {
description:
'Lists all the Chrome extensions installed in the browser. This includes their name, ID, version, and enabled status. (requires flag: --categoryExtensions=true)',
Expand Down
5 changes: 5 additions & 0 deletions src/bin/chrome-devtools-mcp-cli-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,11 @@ export const cliOptions = {
describe: 'Whether to enable interoperability tools',
hidden: true,
},
experimentalWorkers: {
type: 'boolean',
describe:
'Whether to expose tools for enumerating dedicated Web Workers of the selected page and evaluating scripts inside them. Off by default so that worker execution contexts do not add to the baseline token usage.',
},
experimentalScreencast: {
type: 'boolean',
describe:
Expand Down
8 changes: 8 additions & 0 deletions src/telemetry/flag_usage_metrics.json
Original file line number Diff line number Diff line change
Expand Up @@ -371,5 +371,13 @@
{
"name": "allow_unrestricted_paths",
"flagType": "boolean"
},
{
"name": "experimental_workers_present",
"flagType": "boolean"
},
{
"name": "experimental_workers",
"flagType": "boolean"
}
]
4 changes: 4 additions & 0 deletions src/telemetry/tool_call_metrics.json
Original file line number Diff line number Diff line change
Expand Up @@ -893,6 +893,10 @@
}
]
},
{
"name": "list_dedicated_workers",
"args": []
},
{
"name": "get_heapsnapshot_object_details",
"args": [
Expand Down
4 changes: 4 additions & 0 deletions src/tools/ToolDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import type {
TextSnapshotNode,
GeolocationOptions,
ExtensionServiceWorker,
DedicatedWorker,
} from '../types.js';
import type {PaginationOptions} from '../types.js';
import type {WaitForEventsResult, DialogAction} from '../WaitForHelper.js';
Expand Down Expand Up @@ -234,6 +235,9 @@ export type Context = Readonly<{
getExtensionServiceWorkerId(
extensionServiceWorker: ExtensionServiceWorker,
): string | undefined;
createDedicatedWorkersSnapshot(): DedicatedWorker[];
getDedicatedWorkers(): DedicatedWorker[];
getDedicatedWorkerId(dedicatedWorker: DedicatedWorker): string | undefined;
getHeapSnapshotAggregates(
filePath: string,
filterName?: string,
Expand Down
84 changes: 83 additions & 1 deletion src/tools/script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export type Evaluatable = Page | Frame | WebWorker;
export const evaluateScript = defineTool(cliArgs => {
return {
name: 'evaluate_script',
description: `Evaluate a JavaScript function inside the currently selected page${cliArgs?.categoryExtensions ? ' or service worker' : ''}. Returns the response as JSON, so returned values have to be JSON-serializable.`,
description: `Evaluate a JavaScript function inside the currently selected page${cliArgs?.categoryExtensions ? ' or service worker' : ''}${cliArgs?.experimentalWorkers ? ' or dedicated worker' : ''}. Returns the response as JSON, so returned values have to be JSON-serializable.`,
annotations: {
category: ToolCategory.DEBUGGING,
readOnlyHint: false,
Expand Down Expand Up @@ -62,19 +62,59 @@ Example with arguments: \`(el) => el.innerText\`
),
}
: {}),
...(cliArgs?.experimentalWorkers
? {
workerId: zod
.string()
.optional()
.describe(
`The optional dedicated worker id to evaluate the script in. Call list_dedicated_workers to obtain the available worker ids. If provided, 'pageId' should be omitted. Note: 'args' (element UIDs) cannot be used when evaluating in a worker.`,
),
}
: {}),
},
blockedByDialog: true,
verifyFilesSchema: ['filePath'],
handler: async (request, response, context) => {
const {
serviceWorkerId,
workerId,
args: uidArgs,
function: fnString,
pageId,
dialogAction,
filePath,
} = request.params;

if (cliArgs?.experimentalWorkers && workerId) {
if (uidArgs && uidArgs.length > 0) {
throw new Error(
'args (element uids) cannot be used when evaluating in a worker.',
);
}
if (pageId) {
throw new Error('specify either a pageId or a workerId.');
}

const worker = getDedicatedWorker(context, workerId);
const result = await context
.getSelectedMcpPage()
.waitForEventsAfterAction(
async () => {
await performEvaluation(worker, fnString, [], response, {
filePath,
context,
});
},
{handleDialog: dialogAction ?? 'accept'},
);
if (result.dialogHandled) {
context.getSelectedMcpPage().clearDialog();
}
response.attachWaitForResult(result);
return;
}

if (cliArgs?.categoryExtensions && serviceWorkerId) {
if (uidArgs && uidArgs.length > 0) {
throw new Error(
Expand Down Expand Up @@ -137,6 +177,34 @@ Example with arguments: \`(el) => el.innerText\`
};
});

export const listDedicatedWorkers = defineTool({
name: 'list_dedicated_workers',
description: `List the dedicated Web Workers running in the currently selected page. Returns a worker id for each one that can be passed as the 'workerId' argument to evaluate_script to run a script inside that worker's execution context.`,
annotations: {
category: ToolCategory.DEBUGGING,
readOnlyHint: true,
conditions: ['experimentalWorkers'],
},
schema: {},
blockedByDialog: false,
verifyFilesSchema: [],
handler: async (_request, response, context) => {
const workers = context.createDedicatedWorkersSnapshot();

if (!workers.length) {
response.appendResponseLine(
'No dedicated workers found in the selected page.',
);
return;
}

response.appendResponseLine('## Dedicated Workers');
for (const worker of workers) {
response.appendResponseLine(`${worker.id}: ${worker.url}`);
}
},
});

const performEvaluation = async (
evaluatable: Evaluatable,
fnString: string,
Expand Down Expand Up @@ -192,6 +260,20 @@ const getPageOrFrame = async (
return pageOrFrame;
};

const getDedicatedWorker = (context: Context, workerId: string): WebWorker => {
const dedicatedWorkers = context.createDedicatedWorkersSnapshot();

const dedicatedWorker = dedicatedWorkers.find(
worker => context.getDedicatedWorkerId(worker) === workerId,
);

if (!dedicatedWorker) {
throw new Error('Dedicated worker not found.');
}

return dedicatedWorker.worker;
};

const getWebWorker = async (
context: Context,
serviceWorkerId: string,
Expand Down
13 changes: 12 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,25 @@
* SPDX-License-Identifier: Apache-2.0
*/

import type {SerializedAXNode, Viewport, Target} from './third_party/index.js';
import type {
SerializedAXNode,
Viewport,
Target,
WebWorker,
} from './third_party/index.js';

export interface ExtensionServiceWorker {
url: string;
target: Target;
id: string;
}

export interface DedicatedWorker {
url: string;
worker: WebWorker;
id: string;
}

export interface TextSnapshotNode extends SerializedAXNode {
id: string;
backendNodeId?: number;
Expand Down
Loading