From 06670281c165ca6ff242ec61162370aa69a0b284 Mon Sep 17 00:00:00 2001 From: Wolfgang Beyer Date: Wed, 25 Mar 2026 14:12:20 +0000 Subject: [PATCH 1/5] tool for listing in-page tools --- src/McpResponse.ts | 90 ++++++++++++++++++++- src/bin/chrome-devtools-mcp-cli-options.ts | 6 ++ src/bin/chrome-devtools.ts | 2 +- src/index.ts | 6 ++ src/third_party/index.ts | 1 + src/tools/ToolDefinition.ts | 1 + src/tools/categories.ts | 2 + src/tools/inPage.ts | 50 ++++++++++++ src/tools/tools.ts | 2 + tests/McpResponse.test.js.snapshot | 30 +++++++ tests/McpResponse.test.ts | 77 ++++++++++++++++++ tests/cli.test.ts | 2 + tests/index.test.ts | 73 +++++++++-------- tests/tools/inPage.test.ts | 91 ++++++++++++++++++++++ 14 files changed, 397 insertions(+), 36 deletions(-) create mode 100644 src/tools/inPage.ts create mode 100644 tests/tools/inPage.test.ts diff --git a/src/McpResponse.ts b/src/McpResponse.ts index b6162c27c..5aef87754 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -12,7 +12,7 @@ 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, @@ -20,6 +20,7 @@ import type { ResourceType, TextContent, } from './third_party/index.js'; +import type {ToolGroup} from './tools/inPage.js'; import {handleDialog} from './tools/pages.js'; import type { DevToolsData, @@ -40,6 +41,57 @@ interface TraceInsightData { insightName: InsightName; } +async function getToolGroup(page: McpPage): Promise { + // 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(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 call `event.respondWith`, return instead of timing out + setTimeout(() => { + resolve(undefined); + }, 0); + }); + }); + return toolGroup; +} + export class McpResponse implements Response { #includePages = false; #includeExtensionServiceWorkers = false; @@ -70,6 +122,7 @@ export class McpResponse implements Response { includePreservedMessages?: boolean; }; #listExtensions?: boolean; + #listInPageTools?: boolean; #devToolsData?: DevToolsData; #tabId?: string; #args: ParsedArguments; @@ -110,6 +163,12 @@ export class McpResponse implements Response { this.#listExtensions = true; } + setListInPageTools(): void { + if (this.#args.categoryInPageTools) { + this.#listInPageTools = true; + } + } + setIncludeNetworkRequests( value: boolean, options?: PaginationOptions & { @@ -357,6 +416,12 @@ export class McpResponse implements Response { if (this.#listExtensions) { extensions = context.listExtensions(); } + + let inPageTools: ToolGroup | undefined; + if (this.#listInPageTools) { + inPageTools = await getToolGroup(context.getSelectedMcpPage()); + } + let consoleMessages: Array | undefined; if (this.#consoleDataOptions?.include) { if (!this.#page) { @@ -459,6 +524,7 @@ export class McpResponse implements Response { traceSummary: this.#attachedTraceSummary, extensions, lighthouseResult: this.#attachedLighthouseResult, + inPageTools, }); } @@ -475,6 +541,7 @@ export class McpResponse implements Response { traceInsight?: TraceInsightData; extensions?: InstalledExtension[]; lighthouseResult?: LighthouseData; + inPageTools?: ToolGroup; }, ): {content: Array; structuredContent: object} { const structuredContent: { @@ -489,6 +556,7 @@ export class McpResponse implements Response { traceInsights?: Array<{insightName: string; insightKey: string}>; lighthouseResult?: object; extensions?: object[]; + inPageTools?: object; message?: string; networkConditions?: string; navigationTimeout?: number; @@ -726,6 +794,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) { + 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; diff --git a/src/bin/chrome-devtools-mcp-cli-options.ts b/src/bin/chrome-devtools-mcp-cli-options.ts index b4792629c..83c399f3c 100644 --- a/src/bin/chrome-devtools-mcp-cli-options.ts +++ b/src/bin/chrome-devtools-mcp-cli-options.ts @@ -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', + default: false, + describe: + 'Set to true to enable tools exposed by the inspected page itself', + }, performanceCrux: { type: 'boolean', default: true, diff --git a/src/bin/chrome-devtools.ts b/src/bin/chrome-devtools.ts index 09f4def51..53a8eba54 100644 --- a/src/bin/chrome-devtools.ts +++ b/src/bin/chrome-devtools.ts @@ -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; diff --git a/src/index.ts b/src/index.ts index 2689e34a6..362f2348a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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 diff --git a/src/third_party/index.ts b/src/third_party/index.ts index f719f4df8..0acd5ca8e 100644 --- a/src/third_party/index.ts +++ b/src/third_party/index.ts @@ -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, diff --git a/src/tools/ToolDefinition.ts b/src/tools/ToolDefinition.ts index 170198803..9a0ef4355 100644 --- a/src/tools/ToolDefinition.ts +++ b/src/tools/ToolDefinition.ts @@ -129,6 +129,7 @@ export interface Response { ): void; setListExtensions(): void; attachLighthouseResult(result: LighthouseData): void; + setListInPageTools(): void; } /** diff --git a/src/tools/categories.ts b/src/tools/categories.ts index 9e3512689..c8e92c17c 100644 --- a/src/tools/categories.ts +++ b/src/tools/categories.ts @@ -12,6 +12,7 @@ export enum ToolCategory { NETWORK = 'network', DEBUGGING = 'debugging', EXTENSIONS = 'extensions', + IN_PAGE = 'in-page', } export const labels = { @@ -22,4 +23,5 @@ export const labels = { [ToolCategory.NETWORK]: 'Network', [ToolCategory.DEBUGGING]: 'Debugging', [ToolCategory.EXTENSIONS]: 'Extensions', + [ToolCategory.IN_PAGE]: 'In-page tools', }; diff --git a/src/tools/inPage.ts b/src/tools/inPage.ts new file mode 100644 index 000000000..71e17989d --- /dev/null +++ b/src/tools/inPage.ts @@ -0,0 +1,50 @@ +/** + * @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; + execute: (input: Record) => unknown; +} + +export interface ToolGroup { + name: string; + description: string; + tools: ToolDefinition[]; +} + +declare global { + interface Window { + __dtmcp?: { + toolGroup?: ToolGroup; + executeTool?: ( + toolName: string, + args: Record, + ) => unknown; + }; + } +} + +export const listInPageTools = definePageTool({ + name: 'list_in_page_tools', + description: `Lists all in-page-tools the page exposes for providing runtime information. + In-page-tools are exposed on the page via the 'window.__dtmcp.executeTool(toolName, params)' + function where they can be called by 'evaluate_script'.`, + annotations: { + category: ToolCategory.IN_PAGE, + readOnlyHint: true, + }, + schema: {}, + handler: async (_request, response, _context) => { + response.setListInPageTools(); + }, +}); diff --git a/src/tools/tools.ts b/src/tools/tools.ts index d448552b0..3c74115c3 100644 --- a/src/tools/tools.ts +++ b/src/tools/tools.ts @@ -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'; @@ -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), diff --git a/tests/McpResponse.test.js.snapshot b/tests/McpResponse.test.js.snapshot index a5b2e8265..2347603a9 100644 --- a/tests/McpResponse.test.js.snapshot +++ b/tests/McpResponse.test.js.snapshot @@ -1206,6 +1206,36 @@ exports[`extensions > lists extensions 2`] = ` } `; +exports[`in-page 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[`in-page 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 diff --git a/tests/McpResponse.test.ts b/tests/McpResponse.test.ts index e2db88c50..7189959c8 100644 --- a/tests/McpResponse.test.ts +++ b/tests/McpResponse.test.ts @@ -10,6 +10,9 @@ import {tmpdir} from 'node:os'; import {join} from 'node:path'; import {describe, it} from 'node:test'; +import sinon from 'sinon'; + +import type {ParsedArguments} from '../src/bin/chrome-devtools-mcp-cli-options.js'; import type {InsightName} from '../src/trace-processing/parse.js'; import { parseRawTraceBuffer, @@ -1013,3 +1016,77 @@ describe('lighthouse', () => { }); }); }); + +describe('in-page tools', () => { + function stubToolDiscovery(page: object) { + // @ts-expect-error Internal API + const client = page._client(); + const originalSend = client.send.bind(client); + sinon.stub(client, 'send').callsFake(async (method: string, params?: Record) => { + if (method === 'DOMDebugger.getEventListeners') { + return { + listeners: [ + { + type: 'devtoolstooldiscovery', + useCapture: false, + passive: false, + once: false, + scriptId: '0', + lineNumber: 0, + columnNumber: 0, + }, + ], + }; + } + return originalSend(method, params); + }); + } + + it('lists in-page tools', async t => { + await withMcpContext( + async (response, context) => { + response.setListInPageTools(); + const emptyResult = await response.handle('test', context); + const emptyText = getTextContent(emptyResult.content[0]); + assert.ok( + emptyText.includes('No in-page tools available.'), + 'Should show message for empty in-page tools', + ); + + response.resetResponseLineForTesting(); + const mcpPage = context.getSelectedMcpPage(); + stubToolDiscovery(mcpPage.pptrPage); + sinon.stub(mcpPage.pptrPage, 'evaluate').resolves({ + name: 'My Tool Group', + description: 'A group of tools', + tools: [ + { + name: 'myTool', + description: 'Does something', + inputSchema: { + type: 'object', + properties: { + foo: {type: 'string'}, + }, + }, + }, + ], + }); + response.setListInPageTools(); + const {content, structuredContent} = await response.handle( + 'test', + context, + ); + const responseText = getTextContent(content[0]); + t.assert.snapshot?.(responseText); + assert.ok( + responseText.includes('inputSchema={"type":"object"'), + 'Response should include inputSchema', + ); + t.assert.snapshot?.(JSON.stringify(structuredContent, null, 2)); + }, + undefined, + {categoryInPageTools: true} as ParsedArguments, + ); + }); +}); diff --git a/tests/cli.test.ts b/tests/cli.test.ts index b18a4532f..1096b1d44 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -23,6 +23,8 @@ describe('cli args parsing', () => { performanceCrux: true, 'usage-statistics': true, usageStatistics: true, + 'category-in-page-tools': false, + categoryInPageTools: false, }; it('parses with default args', async () => { diff --git a/tests/index.test.ts b/tests/index.test.ts index 4df17bb68..c16fec34b 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -72,47 +72,52 @@ describe('e2e', () => { }); it('has all tools', async () => { - await withClient(async client => { - const {tools} = await client.listTools(); - const exposedNames = tools.map(t => t.name).sort(); - const files = fs.readdirSync('build/src/tools'); - const definedNames = []; - for (const file of files) { - if ( - file === 'ToolDefinition.js' || - file === 'tools.js' || - file === 'slim' - ) { - continue; - } - const fileTools = await import(`../src/tools/${file}`); - for (const maybeTool of Object.values(fileTools)) { - if (typeof maybeTool === 'function') { - const tool = (maybeTool as (val: boolean) => ToolDefinition)(false); - if (tool && typeof tool === 'object' && 'name' in tool) { + await withClient( + async client => { + const {tools} = await client.listTools(); + const exposedNames = tools.map(t => t.name).sort(); + const files = fs.readdirSync('build/src/tools'); + const definedNames = []; + for (const file of files) { + if ( + file === 'ToolDefinition.js' || + file === 'tools.js' || + file === 'slim' + ) { + continue; + } + const fileTools = await import(`../src/tools/${file}`); + for (const maybeTool of Object.values(fileTools)) { + if (typeof maybeTool === 'function') { + const tool = (maybeTool as (val: boolean) => ToolDefinition)( + false, + ); + if (tool && typeof tool === 'object' && 'name' in tool) { + if (tool.annotations?.conditions) { + continue; + } + definedNames.push(tool.name); + } + continue; + } + if ( + typeof maybeTool === 'object' && + maybeTool !== null && + 'name' in maybeTool + ) { + const tool = maybeTool as ToolDefinition; if (tool.annotations?.conditions) { continue; } definedNames.push(tool.name); } - continue; - } - if ( - typeof maybeTool === 'object' && - maybeTool !== null && - 'name' in maybeTool - ) { - const tool = maybeTool as ToolDefinition; - if (tool.annotations?.conditions) { - continue; - } - definedNames.push(tool.name); } } - } - definedNames.sort(); - assert.deepStrictEqual(exposedNames, definedNames); - }); + definedNames.sort(); + assert.deepStrictEqual(exposedNames, definedNames); + }, + ['--category-in-page-tools'], + ); }); it('has experimental extensions tools', async () => { diff --git a/tests/tools/inPage.test.ts b/tests/tools/inPage.test.ts new file mode 100644 index 000000000..70718f4dc --- /dev/null +++ b/tests/tools/inPage.test.ts @@ -0,0 +1,91 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'node:assert'; +import {describe, it} from 'node:test'; + +import sinon from 'sinon'; + +import type {ParsedArguments} from '../../src/bin/chrome-devtools-mcp-cli-options.js'; +import type {ToolGroup} from '../../src/tools/inPage.js'; +import {listInPageTools} from '../../src/tools/inPage.js'; +import {withMcpContext} from '../utils.js'; + +describe('inPage', () => { + describe('list_in_page_tools', () => { + it('lists tools', async () => { + await withMcpContext( + async (response, context) => { + const page = await context.newPage(); + const toolGroup: ToolGroup = { + name: 'test-group', + description: 'test description', + tools: [ + { + name: 'test-tool', + description: 'test tool description', + inputSchema: { + type: 'object', + properties: { + arg: {type: 'string'}, + }, + }, + execute: () => 'result', + }, + ], + }; + + await page.pptrPage.evaluate(() => { + window.addEventListener('devtoolstooldiscovery', () => { + // No-op + }); + }); + + const evaluateStub = sinon.stub(page.pptrPage, 'evaluate'); + evaluateStub.resolves(toolGroup); + + await listInPageTools.handler({params: {}, page}, response, context); + + const result = await response.handle('list_in_page_tools', context); + assert.ok('inPageTools' in result.structuredContent); + assert.deepEqual( + (result.structuredContent as {inPageTools: ToolGroup}).inPageTools, + toolGroup, + ); + }, + undefined, + {categoryInPageTools: true} as ParsedArguments, + ); + }); + + it('handles no tools', async () => { + await withMcpContext( + async (response, context) => { + const page = await context.newPage(); + await page.pptrPage.evaluate(() => { + window.addEventListener('devtoolstooldiscovery', () => { + // No-op + }); + }); + + const evaluateStub = sinon.stub(page.pptrPage, 'evaluate'); + evaluateStub.resolves(undefined); + + await listInPageTools.handler({params: {}, page}, response, context); + + const result = await response.handle('list_in_page_tools', context); + assert.ok('inPageTools' in result.structuredContent); + assert.strictEqual( + (result.structuredContent as {inPageTools: undefined}).inPageTools, + undefined, + ); + }, + undefined, + {categoryInPageTools: true} as ParsedArguments, + ); + }); + }); +}); From 532add2112540f686c9e7c13d8147a1940e64b70 Mon Sep 17 00:00:00 2001 From: Wolfgang Beyer Date: Wed, 25 Mar 2026 14:12:20 +0000 Subject: [PATCH 2/5] tool for listing in-page tools --- README.md | 7 +++++++ docs/tool-reference.md | 14 ++++++++++++++ tests/McpResponse.test.ts | 38 ++++++++++++++++++++------------------ 3 files changed, 41 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 7f6afebdd..678394af6 100644 --- a/README.md +++ b/README.md @@ -464,6 +464,8 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles - [`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) +- **In-page tools** (1 tools) + - [`list_in_page_tools`](docs/tool-reference.md#list_in_page_tools) @@ -555,6 +557,11 @@ The Chrome DevTools MCP server supports the following configuration option: - **Type:** boolean - **Default:** `true` +- **`--categoryInPageTools`/ `--category-in-page-tools`** + Set to true to enable tools exposed by the inspected page itself + - **Type:** boolean + - **Default:** `false` + - **`--performanceCrux`/ `--performance-crux`** Set to false to disable sending URLs from performance traces to CrUX API to get field performance data. - **Type:** boolean diff --git a/docs/tool-reference.md b/docs/tool-reference.md index 50b4c02c5..6697b3dd0 100644 --- a/docs/tool-reference.md +++ b/docs/tool-reference.md @@ -37,6 +37,8 @@ - [`list_console_messages`](#list_console_messages) - [`take_screenshot`](#take_screenshot) - [`take_snapshot`](#take_snapshot) +- **[In-page tools](#in-page-tools)** (1 tools) + - [`list_in_page_tools`](#list_in_page_tools) ## Input automation @@ -397,3 +399,15 @@ in the DevTools Elements panel (if any). - **verbose** (boolean) _(optional)_: Whether to include all possible information available in the full a11y tree. Default is false. --- + +## In-page tools + +### `list_in_page_tools` + +**Description:** Lists all in-page-tools the page exposes for providing runtime information. +In-page-tools are exposed on the page via the 'window.\_\_dtmcp.executeTool(toolName, params)' +function where they can be called by '[`evaluate_script`](#evaluate_script)'. + +**Parameters:** None + +--- diff --git a/tests/McpResponse.test.ts b/tests/McpResponse.test.ts index 7189959c8..e27d6a1d1 100644 --- a/tests/McpResponse.test.ts +++ b/tests/McpResponse.test.ts @@ -1022,24 +1022,26 @@ describe('in-page tools', () => { // @ts-expect-error Internal API const client = page._client(); const originalSend = client.send.bind(client); - sinon.stub(client, 'send').callsFake(async (method: string, params?: Record) => { - if (method === 'DOMDebugger.getEventListeners') { - return { - listeners: [ - { - type: 'devtoolstooldiscovery', - useCapture: false, - passive: false, - once: false, - scriptId: '0', - lineNumber: 0, - columnNumber: 0, - }, - ], - }; - } - return originalSend(method, params); - }); + sinon + .stub(client, 'send') + .callsFake(async (method: string, params?: Record) => { + if (method === 'DOMDebugger.getEventListeners') { + return { + listeners: [ + { + type: 'devtoolstooldiscovery', + useCapture: false, + passive: false, + once: false, + scriptId: '0', + lineNumber: 0, + columnNumber: 0, + }, + ], + }; + } + return originalSend(method, params); + }); } it('lists in-page tools', async t => { From 5ae8622f3c0f442cf21bcaf7ace580b092d43daf Mon Sep 17 00:00:00 2001 From: Wolfgang Beyer Date: Fri, 27 Mar 2026 08:20:18 +0000 Subject: [PATCH 3/5] address comments --- src/McpResponse.ts | 14 +-- src/bin/chrome-devtools-mcp-cli-options.ts | 2 +- src/tools/inPage.ts | 12 +-- tests/McpResponse.test.js.snapshot | 4 +- tests/McpResponse.test.ts | 2 +- tests/cli.test.ts | 2 - tests/index.test.ts | 14 ++- tests/tools/inPage.test.ts | 106 +++++++++++++++------ 8 files changed, 106 insertions(+), 50 deletions(-) diff --git a/src/McpResponse.ts b/src/McpResponse.ts index 5aef87754..110860d31 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -20,7 +20,7 @@ import type { ResourceType, TextContent, } from './third_party/index.js'; -import type {ToolGroup} from './tools/inPage.js'; +import type {ToolGroup, ToolDefinition} from './tools/inPage.js'; import {handleDialog} from './tools/pages.js'; import type { DevToolsData, @@ -41,7 +41,7 @@ interface TraceInsightData { insightName: InsightName; } -async function getToolGroup(page: McpPage): Promise { +async function getToolGroup(page: McpPage): Promise | undefined> { // Check if there is a `devtoolstooldiscovery` event listener const windowHandle = await page.pptrPage.evaluateHandle(() => window); // @ts-expect-error internal API @@ -55,7 +55,7 @@ async function getToolGroup(page: McpPage): Promise { } const toolGroup = await page.pptrPage.evaluate(() => { - return new Promise(resolve => { + return new Promise | undefined>(resolve => { const event = new CustomEvent('devtoolstooldiscovery'); // @ts-expect-error Adding custom property event.respondWith = (toolGroup: ToolGroup) => { @@ -83,7 +83,7 @@ async function getToolGroup(page: McpPage): Promise { resolve(toolGroup); }; window.dispatchEvent(event); - // If the page does not call `event.respondWith`, return instead of timing out + // If the page does not synchronously call `event.respondWith`, return instead of timing out setTimeout(() => { resolve(undefined); }, 0); @@ -417,7 +417,7 @@ export class McpResponse implements Response { extensions = context.listExtensions(); } - let inPageTools: ToolGroup | undefined; + let inPageTools: ToolGroup | undefined; if (this.#listInPageTools) { inPageTools = await getToolGroup(context.getSelectedMcpPage()); } @@ -541,7 +541,7 @@ export class McpResponse implements Response { traceInsight?: TraceInsightData; extensions?: InstalledExtension[]; lighthouseResult?: LighthouseData; - inPageTools?: ToolGroup; + inPageTools?: ToolGroup; }, ): {content: Array; structuredContent: object} { const structuredContent: { @@ -797,7 +797,7 @@ Call ${handleDialog.name} to handle it before continuing.`); if (this.#listInPageTools) { structuredContent.inPageTools = data.inPageTools ?? undefined; response.push('## In-page tools'); - if (!data.inPageTools) { + if (!data.inPageTools || !data.inPageTools.tools) { response.push('No in-page tools available.'); } else { const toolGroup = data.inPageTools; diff --git a/src/bin/chrome-devtools-mcp-cli-options.ts b/src/bin/chrome-devtools-mcp-cli-options.ts index 83c399f3c..80046b115 100644 --- a/src/bin/chrome-devtools-mcp-cli-options.ts +++ b/src/bin/chrome-devtools-mcp-cli-options.ts @@ -218,7 +218,7 @@ export const cliOptions = { }, categoryInPageTools: { type: 'boolean', - default: false, + hidden: true, describe: 'Set to true to enable tools exposed by the inspected page itself', }, diff --git a/src/tools/inPage.ts b/src/tools/inPage.ts index 71e17989d..a47fcce7f 100644 --- a/src/tools/inPage.ts +++ b/src/tools/inPage.ts @@ -13,19 +13,18 @@ export interface ToolDefinition { name: string; description: string; inputSchema: JSONSchema7; - execute: (input: Record) => unknown; } -export interface ToolGroup { +export interface ToolGroup { name: string; description: string; - tools: ToolDefinition[]; + tools: T[]; } declare global { interface Window { __dtmcp?: { - toolGroup?: ToolGroup; + toolGroup?: ToolGroup) => unknown}>; executeTool?: ( toolName: string, args: Record, @@ -37,11 +36,12 @@ declare global { export const listInPageTools = definePageTool({ name: 'list_in_page_tools', description: `Lists all in-page-tools the page exposes for providing runtime information. - In-page-tools are exposed on the page via the 'window.__dtmcp.executeTool(toolName, params)' - function where they can be called by 'evaluate_script'.`, + 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) => { diff --git a/tests/McpResponse.test.js.snapshot b/tests/McpResponse.test.js.snapshot index 2347603a9..fdf8cc5ae 100644 --- a/tests/McpResponse.test.js.snapshot +++ b/tests/McpResponse.test.js.snapshot @@ -1206,14 +1206,14 @@ exports[`extensions > lists extensions 2`] = ` } `; -exports[`in-page tools > lists in-page tools 1`] = ` +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[`in-page tools > lists in-page tools 2`] = ` +exports[`inPage tools > lists in-page tools 2`] = ` { "inPageTools": { "name": "My Tool Group", diff --git a/tests/McpResponse.test.ts b/tests/McpResponse.test.ts index e27d6a1d1..c76423b94 100644 --- a/tests/McpResponse.test.ts +++ b/tests/McpResponse.test.ts @@ -1017,7 +1017,7 @@ describe('lighthouse', () => { }); }); -describe('in-page tools', () => { +describe('inPage tools', () => { function stubToolDiscovery(page: object) { // @ts-expect-error Internal API const client = page._client(); diff --git a/tests/cli.test.ts b/tests/cli.test.ts index 1096b1d44..b18a4532f 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -23,8 +23,6 @@ describe('cli args parsing', () => { performanceCrux: true, 'usage-statistics': true, usageStatistics: true, - 'category-in-page-tools': false, - categoryInPageTools: false, }; it('parses with default args', async () => { diff --git a/tests/index.test.ts b/tests/index.test.ts index c16fec34b..355040ed7 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -116,6 +116,16 @@ describe('e2e', () => { definedNames.sort(); assert.deepStrictEqual(exposedNames, definedNames); }, + ); + }); + + it('has experimental in-Page tools', async () => { + await withClient( + async client => { + const {tools} = await client.listTools(); + const listInPageTools = tools.find(t => t.name === 'list_in_page_tools'); + assert.ok(listInPageTools); + }, ['--category-in-page-tools'], ); }); @@ -124,8 +134,8 @@ describe('e2e', () => { await withClient( async client => { const {tools} = await client.listTools(); - const clickAt = tools.find(t => t.name === 'install_extension'); - assert.ok(clickAt); + const installExtension = tools.find(t => t.name === 'install_extension'); + assert.ok(installExtension); }, ['--category-extensions'], ); diff --git a/tests/tools/inPage.test.ts b/tests/tools/inPage.test.ts index 70718f4dc..af26629a5 100644 --- a/tests/tools/inPage.test.ts +++ b/tests/tools/inPage.test.ts @@ -7,10 +7,7 @@ import assert from 'node:assert'; import {describe, it} from 'node:test'; -import sinon from 'sinon'; - import type {ParsedArguments} from '../../src/bin/chrome-devtools-mcp-cli-options.js'; -import type {ToolGroup} from '../../src/tools/inPage.js'; import {listInPageTools} from '../../src/tools/inPage.js'; import {withMcpContext} from '../utils.js'; @@ -20,40 +17,76 @@ describe('inPage', () => { await withMcpContext( async (response, context) => { const page = await context.newPage(); - const toolGroup: ToolGroup = { - name: 'test-group', - description: 'test description', - tools: [ - { - name: 'test-tool', - description: 'test tool description', - inputSchema: { - type: 'object', - properties: { - arg: {type: 'string'}, + + await page.pptrPage.evaluate(() => { + window.__dtmcp = { + toolGroup: { + name: 'test-group', + description: 'test description', + tools: [ + { + name: 'test-tool', + description: 'test tool description', + inputSchema: { + type: 'object', + properties: { + arg: {type: 'string'}, + }, + }, + execute: () => 'result', }, - }, - execute: () => 'result', + ], }, - ], - }; + }; + window.addEventListener('devtoolstooldiscovery', (e: Event) => { + // @ts-expect-error Event has `respondWith` + e.respondWith(window.__dtmcp?.toolGroup); + }); + }); + await listInPageTools.handler({params: {}, page}, response, context); + + const result = await response.handle('list_in_page_tools', context); + // @ts-expect-error `structuredContent` has `inPageTools` + const actualGroup = result.structuredContent.inPageTools; + assert.strictEqual(actualGroup.name, 'test-group'); + assert.strictEqual(actualGroup.description, 'test description'); + assert.strictEqual(actualGroup.tools.length, 1); + assert.strictEqual(actualGroup.tools[0].name, 'test-tool'); + assert.strictEqual( + actualGroup.tools[0].description, + 'test tool description', + ); + assert.deepEqual(actualGroup.tools[0].inputSchema, { + type: 'object', + properties: { + arg: {type: 'string'}, + }, + }); + }, + undefined, + {categoryInPageTools: true} as ParsedArguments, + ); + }); + + it('handles empty response', async () => { + await withMcpContext( + async (response, context) => { + const page = await context.newPage(); await page.pptrPage.evaluate(() => { - window.addEventListener('devtoolstooldiscovery', () => { - // No-op + window.addEventListener('devtoolstooldiscovery', (e: Event) => { + // @ts-expect-error Event has `respondWith` + e.respondWith({}); }); }); - const evaluateStub = sinon.stub(page.pptrPage, 'evaluate'); - evaluateStub.resolves(toolGroup); - await listInPageTools.handler({params: {}, page}, response, context); const result = await response.handle('list_in_page_tools', context); assert.ok('inPageTools' in result.structuredContent); assert.deepEqual( - (result.structuredContent as {inPageTools: ToolGroup}).inPageTools, - toolGroup, + (result.structuredContent as {inPageTools: undefined}).inPageTools, + {}, ); }, undefined, @@ -61,19 +94,34 @@ describe('inPage', () => { ); }); - it('handles no tools', async () => { + it('handles no response', async () => { await withMcpContext( async (response, context) => { const page = await context.newPage(); await page.pptrPage.evaluate(() => { window.addEventListener('devtoolstooldiscovery', () => { - // No-op + // do nothing }); }); - const evaluateStub = sinon.stub(page.pptrPage, 'evaluate'); - evaluateStub.resolves(undefined); + await listInPageTools.handler({params: {}, page}, response, context); + const result = await response.handle('list_in_page_tools', context); + assert.ok('inPageTools' in result.structuredContent); + assert.strictEqual( + (result.structuredContent as {inPageTools: undefined}).inPageTools, + undefined, + ); + }, + undefined, + {categoryInPageTools: true} as ParsedArguments, + ); + }); + + it('handles no eventListener', async () => { + await withMcpContext( + async (response, context) => { + const page = await context.newPage(); await listInPageTools.handler({params: {}, page}, response, context); const result = await response.handle('list_in_page_tools', context); From a1306eaacd7ee7fcd232ab2cf1584fa98b4c4104 Mon Sep 17 00:00:00 2001 From: Wolfgang Beyer Date: Mon, 30 Mar 2026 08:08:08 +0000 Subject: [PATCH 4/5] format --- src/McpResponse.ts | 4 +- src/tools/inPage.ts | 4 +- tests/index.test.ts | 80 +++++++++++++++++++------------------- tests/tools/inPage.test.ts | 20 +++++----- 4 files changed, 56 insertions(+), 52 deletions(-) diff --git a/src/McpResponse.ts b/src/McpResponse.ts index 046ed16ee..1651c7728 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -41,7 +41,9 @@ interface TraceInsightData { insightName: InsightName; } -async function getToolGroup(page: McpPage): Promise | undefined> { +async function getToolGroup( + page: McpPage, +): Promise | undefined> { // Check if there is a `devtoolstooldiscovery` event listener const windowHandle = await page.pptrPage.evaluateHandle(() => window); // @ts-expect-error internal API diff --git a/src/tools/inPage.ts b/src/tools/inPage.ts index a47fcce7f..2e1deb00b 100644 --- a/src/tools/inPage.ts +++ b/src/tools/inPage.ts @@ -24,7 +24,9 @@ export interface ToolGroup { declare global { interface Window { __dtmcp?: { - toolGroup?: ToolGroup) => unknown}>; + toolGroup?: ToolGroup< + ToolDefinition & {execute: (args: Record) => unknown} + >; executeTool?: ( toolName: string, args: Record, diff --git a/tests/index.test.ts b/tests/index.test.ts index 355040ed7..f08350c08 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -72,58 +72,56 @@ describe('e2e', () => { }); it('has all tools', async () => { - await withClient( - async client => { - const {tools} = await client.listTools(); - const exposedNames = tools.map(t => t.name).sort(); - const files = fs.readdirSync('build/src/tools'); - const definedNames = []; - for (const file of files) { - if ( - file === 'ToolDefinition.js' || - file === 'tools.js' || - file === 'slim' - ) { - continue; - } - const fileTools = await import(`../src/tools/${file}`); - for (const maybeTool of Object.values(fileTools)) { - if (typeof maybeTool === 'function') { - const tool = (maybeTool as (val: boolean) => ToolDefinition)( - false, - ); - if (tool && typeof tool === 'object' && 'name' in tool) { - if (tool.annotations?.conditions) { - continue; - } - definedNames.push(tool.name); - } - continue; - } - if ( - typeof maybeTool === 'object' && - maybeTool !== null && - 'name' in maybeTool - ) { - const tool = maybeTool as ToolDefinition; + await withClient(async client => { + const {tools} = await client.listTools(); + const exposedNames = tools.map(t => t.name).sort(); + const files = fs.readdirSync('build/src/tools'); + const definedNames = []; + for (const file of files) { + if ( + file === 'ToolDefinition.js' || + file === 'tools.js' || + file === 'slim' + ) { + continue; + } + const fileTools = await import(`../src/tools/${file}`); + for (const maybeTool of Object.values(fileTools)) { + if (typeof maybeTool === 'function') { + const tool = (maybeTool as (val: boolean) => ToolDefinition)(false); + if (tool && typeof tool === 'object' && 'name' in tool) { if (tool.annotations?.conditions) { continue; } definedNames.push(tool.name); } + continue; + } + if ( + typeof maybeTool === 'object' && + maybeTool !== null && + 'name' in maybeTool + ) { + const tool = maybeTool as ToolDefinition; + if (tool.annotations?.conditions) { + continue; + } + definedNames.push(tool.name); } } - definedNames.sort(); - assert.deepStrictEqual(exposedNames, definedNames); - }, - ); + } + definedNames.sort(); + assert.deepStrictEqual(exposedNames, definedNames); + }); }); it('has experimental in-Page tools', async () => { await withClient( async client => { const {tools} = await client.listTools(); - const listInPageTools = tools.find(t => t.name === 'list_in_page_tools'); + const listInPageTools = tools.find( + t => t.name === 'list_in_page_tools', + ); assert.ok(listInPageTools); }, ['--category-in-page-tools'], @@ -134,7 +132,9 @@ describe('e2e', () => { await withClient( async client => { const {tools} = await client.listTools(); - const installExtension = tools.find(t => t.name === 'install_extension'); + const installExtension = tools.find( + t => t.name === 'install_extension', + ); assert.ok(installExtension); }, ['--category-extensions'], diff --git a/tests/tools/inPage.test.ts b/tests/tools/inPage.test.ts index af26629a5..79540811f 100644 --- a/tests/tools/inPage.test.ts +++ b/tests/tools/inPage.test.ts @@ -53,16 +53,16 @@ describe('inPage', () => { assert.strictEqual(actualGroup.description, 'test description'); assert.strictEqual(actualGroup.tools.length, 1); assert.strictEqual(actualGroup.tools[0].name, 'test-tool'); - assert.strictEqual( - actualGroup.tools[0].description, - 'test tool description', - ); - assert.deepEqual(actualGroup.tools[0].inputSchema, { - type: 'object', - properties: { - arg: {type: 'string'}, - }, - }); + assert.strictEqual( + actualGroup.tools[0].description, + 'test tool description', + ); + assert.deepEqual(actualGroup.tools[0].inputSchema, { + type: 'object', + properties: { + arg: {type: 'string'}, + }, + }); }, undefined, {categoryInPageTools: true} as ParsedArguments, From a9038361afda16c7d50fe94fca2c3b20f9ef686f Mon Sep 17 00:00:00 2001 From: Wolfgang Beyer Date: Mon, 30 Mar 2026 08:22:14 +0000 Subject: [PATCH 5/5] update docs --- README.md | 7 ------- docs/tool-reference.md | 14 -------------- 2 files changed, 21 deletions(-) diff --git a/README.md b/README.md index c75ec3077..438024c3e 100644 --- a/README.md +++ b/README.md @@ -475,8 +475,6 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles - [`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) -- **In-page tools** (1 tools) - - [`list_in_page_tools`](docs/tool-reference.md#list_in_page_tools) @@ -568,11 +566,6 @@ The Chrome DevTools MCP server supports the following configuration option: - **Type:** boolean - **Default:** `true` -- **`--categoryInPageTools`/ `--category-in-page-tools`** - Set to true to enable tools exposed by the inspected page itself - - **Type:** boolean - - **Default:** `false` - - **`--performanceCrux`/ `--performance-crux`** Set to false to disable sending URLs from performance traces to CrUX API to get field performance data. - **Type:** boolean diff --git a/docs/tool-reference.md b/docs/tool-reference.md index 6697b3dd0..50b4c02c5 100644 --- a/docs/tool-reference.md +++ b/docs/tool-reference.md @@ -37,8 +37,6 @@ - [`list_console_messages`](#list_console_messages) - [`take_screenshot`](#take_screenshot) - [`take_snapshot`](#take_snapshot) -- **[In-page tools](#in-page-tools)** (1 tools) - - [`list_in_page_tools`](#list_in_page_tools) ## Input automation @@ -399,15 +397,3 @@ in the DevTools Elements panel (if any). - **verbose** (boolean) _(optional)_: Whether to include all possible information available in the full a11y tree. Default is false. --- - -## In-page tools - -### `list_in_page_tools` - -**Description:** Lists all in-page-tools the page exposes for providing runtime information. -In-page-tools are exposed on the page via the 'window.\_\_dtmcp.executeTool(toolName, params)' -function where they can be called by '[`evaluate_script`](#evaluate_script)'. - -**Parameters:** None - ----