Skip to content
Merged
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
92 changes: 91 additions & 1 deletion src/McpResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ import {SnapshotFormatter} from './formatters/SnapshotFormatter.js';
import type {McpContext} from './McpContext.js';
import type {McpPage} from './McpPage.js';
import {UncaughtError} from './PageCollector.js';
import {DevTools} from './third_party/index.js';
import {DevTools, type Protocol} from './third_party/index.js';
import type {
ConsoleMessage,
ImageContent,
Page,
ResourceType,
TextContent,
} from './third_party/index.js';
import type {ToolGroup, ToolDefinition} from './tools/inPage.js';
import {handleDialog} from './tools/pages.js';
import type {
DevToolsData,
Expand All @@ -40,6 +41,59 @@ interface TraceInsightData {
insightName: InsightName;
}

async function getToolGroup(
page: McpPage,
): Promise<ToolGroup<ToolDefinition> | undefined> {
// Check if there is a `devtoolstooldiscovery` event listener
const windowHandle = await page.pptrPage.evaluateHandle(() => window);
// @ts-expect-error internal API
const client = page.pptrPage._client();
const {listeners}: {listeners: Protocol.DOMDebugger.EventListener[]} =
await client.send('DOMDebugger.getEventListeners', {
objectId: windowHandle.remoteObject().objectId,
});
if (listeners.find(l => l.type === 'devtoolstooldiscovery') === undefined) {
return;
}

const toolGroup = await page.pptrPage.evaluate(() => {
return new Promise<ToolGroup<ToolDefinition> | undefined>(resolve => {
const event = new CustomEvent('devtoolstooldiscovery');
// @ts-expect-error Adding custom property
event.respondWith = (toolGroup: ToolGroup) => {
if (!window.__dtmcp) {
window.__dtmcp = {};
}
window.__dtmcp.toolGroup = toolGroup;

// When receiving a toolGroup for the first time, expose a simple execution helper
if (!window.__dtmcp.executeTool) {
window.__dtmcp.executeTool = async (toolName, args) => {
if (!window.__dtmcp?.toolGroup) {
throw new Error('No tools found on the page');
}
const tool = window.__dtmcp.toolGroup.tools.find(
t => t.name === toolName,
);
if (!tool) {
throw new Error(`Tool ${toolName} not found`);
}
return await tool.execute(args);
};
}

resolve(toolGroup);
};
window.dispatchEvent(event);
// If the page does not synchronously call `event.respondWith`, return instead of timing out
setTimeout(() => {
resolve(undefined);
}, 0);
});
});
return toolGroup;
}

export class McpResponse implements Response {
#includePages = false;
#includeExtensionServiceWorkers = false;
Expand Down Expand Up @@ -70,6 +124,7 @@ export class McpResponse implements Response {
includePreservedMessages?: boolean;
};
#listExtensions?: boolean;
#listInPageTools?: boolean;
#devToolsData?: DevToolsData;
#tabId?: string;
#args: ParsedArguments;
Expand Down Expand Up @@ -110,6 +165,12 @@ export class McpResponse implements Response {
this.#listExtensions = true;
}

setListInPageTools(): void {
if (this.#args.categoryInPageTools) {
this.#listInPageTools = true;
}
}

setIncludeNetworkRequests(
value: boolean,
options?: PaginationOptions & {
Expand Down Expand Up @@ -357,6 +418,12 @@ export class McpResponse implements Response {
if (this.#listExtensions) {
extensions = context.listExtensions();
}

let inPageTools: ToolGroup<ToolDefinition> | undefined;
if (this.#listInPageTools) {
inPageTools = await getToolGroup(context.getSelectedMcpPage());
Comment thread
wolfib marked this conversation as resolved.
}

let consoleMessages: Array<ConsoleFormatter | IssueFormatter> | undefined;
if (this.#consoleDataOptions?.include) {
if (!this.#page) {
Expand Down Expand Up @@ -459,6 +526,7 @@ export class McpResponse implements Response {
traceSummary: this.#attachedTraceSummary,
extensions,
lighthouseResult: this.#attachedLighthouseResult,
inPageTools,
});
}

Expand All @@ -475,6 +543,7 @@ export class McpResponse implements Response {
traceInsight?: TraceInsightData;
extensions?: InstalledExtension[];
lighthouseResult?: LighthouseData;
inPageTools?: ToolGroup<ToolDefinition>;
},
): {content: Array<TextContent | ImageContent>; structuredContent: object} {
const structuredContent: {
Expand All @@ -489,6 +558,7 @@ export class McpResponse implements Response {
traceInsights?: Array<{insightName: string; insightKey: string}>;
lighthouseResult?: object;
extensions?: object[];
inPageTools?: object;
message?: string;
networkConditions?: string;
navigationTimeout?: number;
Expand Down Expand Up @@ -726,6 +796,26 @@ Call ${handleDialog.name} to handle it before continuing.`);
}
}

if (this.#listInPageTools) {
structuredContent.inPageTools = data.inPageTools ?? undefined;
response.push('## In-page tools');
if (!data.inPageTools || !data.inPageTools.tools) {
response.push('No in-page tools available.');
} else {
const toolGroup = data.inPageTools;
response.push(`${toolGroup.name}: ${toolGroup.description}`);
response.push('Available tools:');
const toolDefinitionsMessage = toolGroup.tools
.map(tool => {
return `name="${tool.name}", description="${tool.description}", inputSchema=${JSON.stringify(
tool.inputSchema,
)}`;
})
.join('\n');
response.push(toolDefinitionsMessage);
}
}

if (this.#networkRequestsOptions?.include && data.networkRequests) {
const requests = data.networkRequests;

Expand Down
6 changes: 6 additions & 0 deletions src/bin/chrome-devtools-mcp-cli-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,12 @@ export const cliOptions = {
describe:
'Set to true to include tools related to extensions. Note: This feature is only supported with a pipe connection. autoConnect is not supported.',
},
categoryInPageTools: {
type: 'boolean',
hidden: true,
describe:
'Set to true to enable tools exposed by the inspected page itself',
},
performanceCrux: {
type: 'boolean',
default: true,
Expand Down
2 changes: 1 addition & 1 deletion src/bin/chrome-devtools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ delete startCliOptions.autoConnect;
// Missing CLI serialization.
delete startCliOptions.viewport;
// CLI is generated based on the default tool definitions. To enable conditional
// tools, they needs to be enabled during CLI generation.
// tools, they need to be enabled during CLI generation.
delete startCliOptions.experimentalPageIdRouting;
delete startCliOptions.experimentalVision;
delete startCliOptions.experimentalInteropTools;
Expand Down
6 changes: 6 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,12 @@ export async function createMcpServer(
) {
return;
}
if (
tool.annotations.category === ToolCategory.IN_PAGE &&
!serverArgs.categoryInPageTools
) {
return;
}
if (
tool.annotations.conditions?.includes('computerVision') &&
!serverArgs.experimentalVision
Expand Down
1 change: 1 addition & 0 deletions src/third_party/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export {default as puppeteer} from 'puppeteer-core';
export type * from 'puppeteer-core';
export {PipeTransport} from 'puppeteer-core/internal/node/PipeTransport.js';
export type {CdpPage} from 'puppeteer-core/internal/cdp/Page.js';
export type {JSONSchema7} from 'json-schema';
export {
resolveDefaultUserDataDir,
detectBrowserPlatform,
Expand Down
1 change: 1 addition & 0 deletions src/tools/ToolDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ export interface Response {
): void;
setListExtensions(): void;
attachLighthouseResult(result: LighthouseData): void;
setListInPageTools(): void;
}

/**
Expand Down
2 changes: 2 additions & 0 deletions src/tools/categories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export enum ToolCategory {
NETWORK = 'network',
DEBUGGING = 'debugging',
EXTENSIONS = 'extensions',
IN_PAGE = 'in-page',
}

export const labels = {
Expand All @@ -22,4 +23,5 @@ export const labels = {
[ToolCategory.NETWORK]: 'Network',
[ToolCategory.DEBUGGING]: 'Debugging',
[ToolCategory.EXTENSIONS]: 'Extensions',
[ToolCategory.IN_PAGE]: 'In-page tools',
};
52 changes: 52 additions & 0 deletions src/tools/inPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {type JSONSchema7} from '../third_party/index.js';

import {ToolCategory} from './categories.js';
import {definePageTool} from './ToolDefinition.js';

export interface ToolDefinition {
name: string;
description: string;
inputSchema: JSONSchema7;
}

export interface ToolGroup<T extends ToolDefinition> {
name: string;
description: string;
tools: T[];
}

declare global {
interface Window {
__dtmcp?: {
toolGroup?: ToolGroup<
ToolDefinition & {execute: (args: Record<string, unknown>) => unknown}
>;
executeTool?: (
toolName: string,
args: Record<string, unknown>,
) => unknown;
};
}
}

export const listInPageTools = definePageTool({
name: 'list_in_page_tools',
description: `Lists all in-page-tools the page exposes for providing runtime information.
To call 'list_in_page_tools', call 'evaluate_script' with
'window.__dtmcp.executeTool("list_in_page_tools", {})'.`,
annotations: {
category: ToolCategory.IN_PAGE,
readOnlyHint: true,
conditions: ['inPageTools'],
},
schema: {},
handler: async (_request, response, _context) => {
response.setListInPageTools();
},
});
2 changes: 2 additions & 0 deletions src/tools/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {ParsedArguments} from '../bin/chrome-devtools-mcp-cli-options.js';
import * as consoleTools from './console.js';
import * as emulationTools from './emulation.js';
import * as extensionTools from './extensions.js';
import * as inPageTools from './inPage.js';
import * as inputTools from './input.js';
import * as lighthouseTools from './lighthouse.js';
import * as memoryTools from './memory.js';
Expand All @@ -29,6 +30,7 @@ export const createTools = (args: ParsedArguments) => {
...Object.values(consoleTools),
...Object.values(emulationTools),
...Object.values(extensionTools),
...Object.values(inPageTools),
...Object.values(inputTools),
...Object.values(lighthouseTools),
...Object.values(memoryTools),
Expand Down
30 changes: 30 additions & 0 deletions tests/McpResponse.test.js.snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -1206,6 +1206,36 @@ exports[`extensions > lists extensions 2`] = `
}
`;

exports[`inPage tools > lists in-page tools 1`] = `
## In-page tools
My Tool Group: A group of tools
Available tools:
name="myTool", description="Does something", inputSchema={"type":"object","properties":{"foo":{"type":"string"}}}
`;

exports[`inPage tools > lists in-page tools 2`] = `
{
"inPageTools": {
"name": "My Tool Group",
"description": "A group of tools",
"tools": [
{
"name": "myTool",
"description": "Does something",
"inputSchema": {
"type": "object",
"properties": {
"foo": {
"type": "string"
}
}
}
}
]
}
}
`;

exports[`lighthouse > includes lighthouse report paths 1`] = `
## Lighthouse Audit Results
Mode: navigation
Expand Down
Loading
Loading