diff --git a/package.json b/package.json index 10e165f87..b0f02da38 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,6 @@ "dependencies": { "core-js": "3.46.0", "debug": "4.4.3", - "puppeteer-core": "^24.24.1", "yargs": "18.0.0" }, "devDependencies": { diff --git a/rollup.config.mjs b/rollup.config.mjs index b03ee2762..9ca5ef30b 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -29,11 +29,31 @@ import license from 'rollup-plugin-license'; const isProduction = process.env.NODE_ENV === 'production'; -/** @type {import('rollup').RollupOptions} */ -const sdk = { - input: './build/src/third_party/modelcontextprotocol-sdk/index.js', +const allowedLicenses = [ + 'MIT', + 'Apache 2.0', + 'Apache-2.0', + 'BSD-3-Clause', + 'BSD-2-Clause', + 'ISC', + '0BSD', +]; + +/** + * @param {string} wrapperIndexPath + * @param {import('rollup').OutputOptions} [extraOutputOptions={}] + * @param {string[]} [external=[]] + * @returns {import('rollup').RollupOptions} + */ +const bundleDependency = ( + wrapperIndexPath, + extraOutputOptions = {}, + external = [], +) => ({ + input: wrapperIndexPath, output: { - file: './build/src/third_party/modelcontextprotocol-sdk/index.js', + ...extraOutputOptions, + file: wrapperIndexPath, sourcemap: !isProduction, format: 'esm', }, @@ -48,18 +68,14 @@ const sdk = { thirdParty: { allow: { test: dependency => { - let allowed_licenses = ['MIT', 'Apache 2.0', 'BSD-2-Clause', 'ISC']; - return allowed_licenses.includes(dependency.license); + return allowedLicenses.includes(dependency.license); }, failOnUnlicensed: true, failOnViolation: true, }, output: { file: path.join( - 'build', - 'src', - 'third_party', - 'modelcontextprotocol-sdk', + path.dirname(wrapperIndexPath), 'THIRD_PARTY_NOTICES', ), template(dependencies) { @@ -90,6 +106,16 @@ const sdk = { json(), nodeResolve(), ], -}; + external, +}); -export default [sdk]; +export default [ + bundleDependency('./build/src/third_party/modelcontextprotocol-sdk/index.js'), + bundleDependency( + './build/src/third_party/puppeteer-core/index.js', + { + inlineDynamicImports: true, + }, + ['./bidi.js', '../bidi/bidi.js'], + ), +]; diff --git a/src/DevToolsConnectionAdapter.ts b/src/DevToolsConnectionAdapter.ts index 814dc7f60..ec07d1a72 100644 --- a/src/DevToolsConnectionAdapter.ts +++ b/src/DevToolsConnectionAdapter.ts @@ -4,10 +4,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {type ConnectionTransport} from 'puppeteer-core'; - import {Connection} from '../node_modules/chrome-devtools-frontend/front_end/core/protocol_client/InspectorBackend.js'; +import {type ConnectionTransport} from './third_party/puppeteer-core/index.js'; + /** * Allows a puppeteer {@link ConnectionTransport} to act like a DevTools {@link Connection}. */ diff --git a/src/McpContext.ts b/src/McpContext.ts index 29170568d..4f7641d50 100644 --- a/src/McpContext.ts +++ b/src/McpContext.ts @@ -8,6 +8,10 @@ import os from 'node:os'; import path from 'node:path'; import type {Debugger} from 'debug'; + +import type {ListenerMap} from './PageCollector.js'; +import {NetworkCollector, PageCollector} from './PageCollector.js'; +import {Locator} from './third_party/puppeteer-core/index.js'; import type { Browser, ConsoleMessage, @@ -17,10 +21,7 @@ import type { Page, SerializedAXNode, PredefinedNetworkConditions, -} from 'puppeteer-core'; - -import type {ListenerMap} from './PageCollector.js'; -import {NetworkCollector, PageCollector} from './PageCollector.js'; +} from './third_party/puppeteer-core/index.js'; import {listPages} from './tools/pages.js'; import {takeSnapshot} from './tools/snapshot.js'; import {CLOSE_PAGE_ERROR} from './tools/ToolDefinition.js'; @@ -91,9 +92,16 @@ export class McpContext implements Context { #nextSnapshotId = 1; #traceResults: TraceResult[] = []; - private constructor(browser: Browser, logger: Debugger) { + #locatorClass: typeof Locator; + + private constructor( + browser: Browser, + logger: Debugger, + locatorClass: typeof Locator, + ) { this.browser = browser; this.logger = logger; + this.#locatorClass = locatorClass; this.#networkCollector = new NetworkCollector(this.browser); @@ -122,8 +130,13 @@ export class McpContext implements Context { await this.#consoleCollector.init(); } - static async from(browser: Browser, logger: Debugger) { - const context = new McpContext(browser, logger); + static async from( + browser: Browser, + logger: Debugger, + /* Let tests use unbundled Locator class to avoid overly strict checks within puppeteer that fail when mixing bundled and unbundled class instances */ + locatorClass: typeof Locator = Locator, + ) { + const context = new McpContext(browser, logger, locatorClass); await context.#init(); return context; } @@ -428,4 +441,28 @@ export class McpContext implements Context { getNetworkRequestStableId(request: HTTPRequest): number { return this.#networkCollector.getIdForResource(request); } + + waitForTextOnPage({ + text, + timeout, + }: { + text: string; + timeout?: number | undefined; + }): Promise { + const page = this.getSelectedPage(); + const frames = page.frames(); + + const locator = this.#locatorClass.race( + frames.flatMap(frame => [ + frame.locator(`aria/${text}`), + frame.locator(`text/${text}`), + ]), + ); + + if (timeout) { + locator.setTimeout(timeout); + } + + return locator.wait(); + } } diff --git a/src/McpResponse.ts b/src/McpResponse.ts index 8a1b6dc94..31bbdcc5a 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -3,8 +3,6 @@ * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ -import type {ConsoleMessage, ResourceType} from 'puppeteer-core'; - import type {ConsoleMessageData} from './formatters/consoleFormatter.js'; import { formatConsoleEventShort, @@ -23,6 +21,10 @@ import type { ImageContent, TextContent, } from './third_party/modelcontextprotocol-sdk/index.js'; +import type { + ConsoleMessage, + ResourceType, +} from './third_party/puppeteer-core/index.js'; import {handleDialog} from './tools/pages.js'; import type {ImageContentData, Response} from './tools/ToolDefinition.js'; import {paginate} from './utils/pagination.js'; diff --git a/src/PageCollector.ts b/src/PageCollector.ts index b6b3d6798..44d1363f8 100644 --- a/src/PageCollector.ts +++ b/src/PageCollector.ts @@ -11,7 +11,7 @@ import { type HTTPRequest, type Page, type PageEvents, -} from 'puppeteer-core'; +} from './third_party/puppeteer-core/index.js'; export type ListenerMap = { [K in keyof EventMap]?: (event: EventMap[K]) => void; diff --git a/src/WaitForHelper.ts b/src/WaitForHelper.ts index 62cc83f03..a18f6448a 100644 --- a/src/WaitForHelper.ts +++ b/src/WaitForHelper.ts @@ -3,10 +3,10 @@ * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ -import type {Page, Protocol} from 'puppeteer-core'; import type {CdpPage} from 'puppeteer-core/internal/cdp/Page.js'; import {logger} from './logger.js'; +import type {Page, Protocol} from './third_party/puppeteer-core/index.js'; export class WaitForHelper { #abortController = new AbortController(); diff --git a/src/browser.ts b/src/browser.ts index d76d5a9f5..47afe5a93 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -13,8 +13,8 @@ import type { ChromeReleaseChannel, LaunchOptions, Target, -} from 'puppeteer-core'; -import puppeteer from 'puppeteer-core'; +} from './third_party/puppeteer-core/index.js'; +import {puppeteer} from './third_party/puppeteer-core/index.js'; let browser: Browser | undefined; diff --git a/src/formatters/networkFormatter.ts b/src/formatters/networkFormatter.ts index 572797339..b3466e8c1 100644 --- a/src/formatters/networkFormatter.ts +++ b/src/formatters/networkFormatter.ts @@ -6,7 +6,10 @@ import {isUtf8} from 'node:buffer'; -import type {HTTPRequest, HTTPResponse} from 'puppeteer-core'; +import type { + HTTPRequest, + HTTPResponse, +} from '../third_party/puppeteer-core/index.js'; const BODY_CONTEXT_SIZE_LIMIT = 10000; diff --git a/src/third_party/puppeteer-core/index.ts b/src/third_party/puppeteer-core/index.ts new file mode 100644 index 000000000..92ae88ae7 --- /dev/null +++ b/src/third_party/puppeteer-core/index.ts @@ -0,0 +1,9 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +export {Locator, PredefinedNetworkConditions} from 'puppeteer-core'; +export {default as puppeteer} from 'puppeteer-core'; +export type * from 'puppeteer-core'; diff --git a/src/tools/ToolDefinition.ts b/src/tools/ToolDefinition.ts index e90264a7e..19415dd27 100644 --- a/src/tools/ToolDefinition.ts +++ b/src/tools/ToolDefinition.ts @@ -4,10 +4,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type {Dialog, ElementHandle, Page} from 'puppeteer-core'; - import type {TextSnapshotNode} from '../McpContext.js'; import {zod} from '../third_party/modelcontextprotocol-sdk/index.js'; +import type { + Dialog, + ElementHandle, + Page, +} from '../third_party/puppeteer-core/index.js'; import type {TraceResult} from '../trace-processing/parse.js'; import type {PaginationOptions} from '../utils/types.js'; @@ -93,6 +96,10 @@ export type Context = Readonly<{ filename: string, ): Promise<{filename: string}>; waitForEventsAfterAction(action: () => Promise): Promise; + waitForTextOnPage(params: { + text: string; + timeout?: number | undefined; + }): Promise; }>; export function defineTool( diff --git a/src/tools/console.ts b/src/tools/console.ts index 3f8967eb9..8be953c89 100644 --- a/src/tools/console.ts +++ b/src/tools/console.ts @@ -4,9 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type {ConsoleMessageType} from 'puppeteer-core'; - import {zod} from '../third_party/modelcontextprotocol-sdk/index.js'; +import type {ConsoleMessageType} from '../third_party/puppeteer-core/index.js'; import {ToolCategories} from './categories.js'; import {defineTool} from './ToolDefinition.js'; diff --git a/src/tools/emulation.ts b/src/tools/emulation.ts index d65ac5e89..9287c00d6 100644 --- a/src/tools/emulation.ts +++ b/src/tools/emulation.ts @@ -4,9 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {PredefinedNetworkConditions} from 'puppeteer-core'; - import {zod} from '../third_party/modelcontextprotocol-sdk/index.js'; +import {PredefinedNetworkConditions} from '../third_party/puppeteer-core/index.js'; import {ToolCategories} from './categories.js'; import {defineTool} from './ToolDefinition.js'; diff --git a/src/tools/input.ts b/src/tools/input.ts index ffb1cbb7c..973eb829f 100644 --- a/src/tools/input.ts +++ b/src/tools/input.ts @@ -4,10 +4,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type {ElementHandle} from 'puppeteer-core'; - import type {McpContext, TextSnapshotNode} from '../McpContext.js'; import {zod} from '../third_party/modelcontextprotocol-sdk/index.js'; +import type {ElementHandle} from '../third_party/puppeteer-core/index.js'; import {ToolCategories} from './categories.js'; import {defineTool} from './ToolDefinition.js'; diff --git a/src/tools/network.ts b/src/tools/network.ts index 0dbc7f7e7..d47fa9960 100644 --- a/src/tools/network.ts +++ b/src/tools/network.ts @@ -4,9 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type {ResourceType} from 'puppeteer-core'; - import {zod} from '../third_party/modelcontextprotocol-sdk/index.js'; +import type {ResourceType} from '../third_party/puppeteer-core/index.js'; import {ToolCategories} from './categories.js'; import {defineTool} from './ToolDefinition.js'; diff --git a/src/tools/performance.ts b/src/tools/performance.ts index 6c172fbb9..a76f30d5a 100644 --- a/src/tools/performance.ts +++ b/src/tools/performance.ts @@ -4,10 +4,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type {Page} from 'puppeteer-core'; - import {logger} from '../logger.js'; import {zod} from '../third_party/modelcontextprotocol-sdk/index.js'; +import type {Page} from '../third_party/puppeteer-core/index.js'; import type {InsightName} from '../trace-processing/parse.js'; import { getInsightOutput, diff --git a/src/tools/screenshot.ts b/src/tools/screenshot.ts index 991fbd2b9..e053186f5 100644 --- a/src/tools/screenshot.ts +++ b/src/tools/screenshot.ts @@ -4,9 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type {ElementHandle, Page} from 'puppeteer-core'; - import {zod} from '../third_party/modelcontextprotocol-sdk/index.js'; +import type {ElementHandle, Page} from '../third_party/puppeteer-core/index.js'; import {ToolCategories} from './categories.js'; import {defineTool} from './ToolDefinition.js'; diff --git a/src/tools/script.ts b/src/tools/script.ts index 128363bdc..6f08af08a 100644 --- a/src/tools/script.ts +++ b/src/tools/script.ts @@ -3,9 +3,13 @@ * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ -import type {Frame, JSHandle, Page} from 'puppeteer-core'; import {zod} from '../third_party/modelcontextprotocol-sdk/index.js'; +import type { + Frame, + JSHandle, + Page, +} from '../third_party/puppeteer-core/index.js'; import {ToolCategories} from './categories.js'; import {defineTool} from './ToolDefinition.js'; diff --git a/src/tools/snapshot.ts b/src/tools/snapshot.ts index fb774f78f..0cf548b41 100644 --- a/src/tools/snapshot.ts +++ b/src/tools/snapshot.ts @@ -4,8 +4,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {Locator} from 'puppeteer-core'; - import {zod} from '../third_party/modelcontextprotocol-sdk/index.js'; import {ToolCategories} from './categories.js'; @@ -44,21 +42,7 @@ export const waitFor = defineTool({ ...timeoutSchema, }, handler: async (request, response, context) => { - const page = context.getSelectedPage(); - const frames = page.frames(); - - const locator = Locator.race( - frames.flatMap(frame => [ - frame.locator(`aria/${request.params.text}`), - frame.locator(`text/${request.params.text}`), - ]), - ); - - if (request.params.timeout) { - locator.setTimeout(request.params.timeout); - } - - await locator.wait(); + await context.waitForTextOnPage(request.params); response.appendResponseLine( `Element with text "${request.params.text}" found.`, diff --git a/tests/utils.ts b/tests/utils.ts index 6c8744fb0..7adaaf533 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -5,7 +5,7 @@ */ import logger from 'debug'; import type {Browser} from 'puppeteer'; -import puppeteer from 'puppeteer'; +import puppeteer, {Locator} from 'puppeteer'; import type {Frame, HTTPRequest, HTTPResponse} from 'puppeteer-core'; import {McpContext} from '../src/McpContext.js'; @@ -36,7 +36,7 @@ export async function withBrowser( }), ); const response = new McpResponse(); - const context = await McpContext.from(browser, logger('test')); + const context = await McpContext.from(browser, logger('test'), Locator); await cb(response, context); }