Skip to content
Draft
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -548,9 +548,10 @@ 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)
- [`get_css_styles`](docs/tool-reference.md#get_css_styles)
- [`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)
Expand Down
14 changes: 13 additions & 1 deletion docs/tool-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@
- **[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)
- [`get_css_styles`](#get_css_styles)
- [`lighthouse_audit`](#lighthouse_audit)
- [`list_console_messages`](#list_console_messages)
- [`take_screenshot`](#take_screenshot)
Expand Down Expand Up @@ -379,6 +380,17 @@

---

### `get_css_styles`

**Description:** Retrieve matched CSS rules, inline styles, inherited styles, and cascade information for an element identified by its UID.
Use this tool to debug why specific CSS properties are applied, overridden, or conflicting. Requires a UID from [`take_snapshot`](#take_snapshot).

**Parameters:**

- **uid** (string) **(required)**: The uid of the element on the page from the page content snapshot to inspect CSS styles for

---

### `lighthouse_audit`

**Description:** Get Lighthouse score and reports for accessibility, SEO, best practices, and agentic browsing. This excludes performance. For performance audits, run [`performance_start_trace`](#performance_start_trace)
Expand Down
91 changes: 86 additions & 5 deletions src/McpPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ import {
type Page,
type ConsoleMessage,
type HTTPRequest,
type DevTools,
DevTools,
type JSONSchema7Definition,
} from './third_party/index.js';
import {takeSnapshot} from './tools/snapshot.js';
Expand All @@ -87,6 +87,7 @@ const NAVIGATION_TIMEOUT = 10_000;
import type {
ContextPage,
DevToolsData,
MatchedStyles,
Response,
} from './tools/ToolDefinition.js';
import type {
Expand All @@ -102,6 +103,12 @@ import {
type DialogAction,
} from './utils/WaitForHelper.js';

function isBackendNodeId(
id: number,
): id is DevTools.Protocol.DOM.BackendNodeId {
return typeof id === 'number';
}

/**
* Per-page state wrapper. Consolidates dialog, snapshot, emulation,
* and metadata that were previously scattered across Maps in McpContext.
Expand All @@ -125,6 +132,7 @@ export class McpPage implements ContextPage {
// Metadata
isolatedContextName?: string;
#devtoolsUniverse?: TargetUniverse;
#initDevToolsPromise?: Promise<TargetUniverse>;

// Dialog
#dialog?: Dialog;
Expand Down Expand Up @@ -190,13 +198,29 @@ export class McpPage implements ContextPage {
});
}

async #initDevToolsUniverseNoThrow(): Promise<void> {
async ensureDevToolsUniverse(): Promise<TargetUniverse> {
if (this.#devtoolsUniverse) {
return undefined;
return this.#devtoolsUniverse;
}
try {
if (this.#initDevToolsPromise) {
return await this.#initDevToolsPromise;
}
this.#initDevToolsPromise = (async () => {
const session = await this.pptrPage.createCDPSession();
this.#devtoolsUniverse = await createTargetUniverse(session);
const universe = await createTargetUniverse(session);
this.#devtoolsUniverse = universe;
return universe;
})();
try {
return await this.#initDevToolsPromise;
} finally {
this.#initDevToolsPromise = undefined;
}
}

async #initDevToolsUniverseNoThrow(): Promise<void> {
try {
await this.ensureDevToolsUniverse();
} catch (e) {
logger?.('Failed to initialize DevTools universe', e);
}
Expand Down Expand Up @@ -671,6 +695,63 @@ export class McpPage implements ContextPage {
return this.textSnapshot?.idToNode.get(uid);
}

async getMatchedStylesForUid(uid: string): Promise<MatchedStyles> {
if (!this.textSnapshot) {
throw new Error(
`No snapshot found for page ${this.id ?? '?'}. Use ${takeSnapshot.name} to capture one.`,
);
}
const node = this.textSnapshot.idToNode.get(uid);
if (!node) {
throw new Error(`Element uid "${uid}" not found on page ${this.id}.`);
}

let backendNodeId = node.backendNodeId;
if (!backendNodeId) {
using handle = await this.#resolveElementHandle(node, uid);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we resolve without creating a handle?

backendNodeId = await handle.backendNodeId();
}
if (!backendNodeId || !isBackendNodeId(backendNodeId)) {
throw new Error(
`Failed to resolve backend node ID for element with uid "${uid}".`,
);
}

const devtools = await this.ensureDevToolsUniverse();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to ensure the universe here? I think it should automatically exist when the page is available. Let's throw an error if it does not exist instead?

const domModel = devtools.target.model(DevTools.DOMModel.DOMModel);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uid might belong to iframes for we need to find the right target instance

const cssModel = devtools.target.model(DevTools.CSSModel.CSSModel);
if (!domModel || !cssModel) {
throw new Error('DevTools DOMModel or CSSModel is not available.');
}

await domModel.requestDocument();
const nodeMap = await domModel.pushNodesByBackendIdsToFrontend(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should not need to push nodes to the frontend. Can we just use the CSS logic relying on backend node IDs only?

new Set([backendNodeId]),
);
const domNode = nodeMap?.get(backendNodeId);
if (!domNode) {
throw new Error(
`Element with uid "${uid}" was detached or no longer exists on the page. Please take a new snapshot with ${takeSnapshot.name}.`,
);
}

const targetElement = domNode.enclosingElementOrSelf();
if (!targetElement) {
throw new Error(
`Element with uid "${uid}" is not an element node and has no parent element.`,
);
}

const matchedStyles = await cssModel.getMatchedStyles(targetElement.id);
if (!matchedStyles) {
throw new Error(
`Could not retrieve matched styles for element with uid "${uid}".`,
);
}

return matchedStyles;
}

async getDevToolsData(): Promise<DevToolsData> {
try {
logger?.('Getting DevTools UI data');
Expand Down
14 changes: 14 additions & 0 deletions src/config/cli-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,20 @@ export const commands: Commands = {
},
},
},
get_css_styles: {
description:
'Retrieve matched CSS rules, inline styles, inherited styles, and cascade information for an element identified by its UID.\nUse this tool to debug why specific CSS properties are applied, overridden, or conflicting. Requires a UID from take_snapshot.',
category: 'Debugging',
args: {
uid: {
name: 'uid',
type: 'string',
description:
'The uid of the element on the page from the page content snapshot to inspect CSS styles for',
required: true,
},
},
},
get_heapsnapshot_class_nodes: {
description:
'Loads a memory heapsnapshot and returns instances of a specific class with their IDs. (requires flag: --memoryDebugging=true)',
Expand Down
4 changes: 4 additions & 0 deletions src/telemetry/tool_call_metrics.json
Original file line number Diff line number Diff line change
Expand Up @@ -1048,5 +1048,9 @@
"argType": "number"
}
]
},
{
"name": "get_css_styles",
"args": []
}
]
5 changes: 5 additions & 0 deletions src/tools/ToolDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,10 @@ export type Context = Readonly<{
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.ItemsRange>;
}>;

export type MatchedStyles = NonNullable<
Awaited<ReturnType<DevTools.CSSModel.CSSModel['getMatchedStyles']>>
>;

/**
* Only add methods used by tools/*.
*/
Expand All @@ -331,6 +335,7 @@ export type ContextPage = Readonly<{
readonly networkConditions: string | null;
getAXNodeByUid(uid: string): TextSnapshotNode | undefined;
getElementByUid(uid: string): Promise<ElementHandle<Element>>;
getMatchedStylesForUid(uid: string): Promise<MatchedStyles>;

/**
* Returns a reqid for a cdpRequestId.
Expand Down
Loading
Loading