UNPKG

puppeteer-core

Version:

A high-level API to control headless Chrome over the DevTools Protocol

442 lines (406 loc) 11.2 kB
/** * @license * Copyright 2026 Google Inc. * SPDX-License-Identifier: Apache-2.0 */ import type {Protocol} from 'devtools-protocol'; import type {CDPSession} from '../api/CDPSession.js'; import type {ElementHandle} from '../api/ElementHandle.js'; import type {Frame} from '../api/Frame.js'; import type {ConsoleMessageLocation} from '../common/ConsoleMessage.js'; import {EventEmitter} from '../common/EventEmitter.js'; import {debugError} from '../common/util.js'; import type {CdpFrame} from './Frame.js'; import type {FrameManager} from './FrameManager.js'; import {FrameManagerEvent} from './FrameManagerEvents.js'; import {MAIN_WORLD} from './IsolatedWorlds.js'; /** * Tool annotations * * @public */ export interface WebMCPAnnotation { /** * A hint indicating that the tool does not modify any state. */ readOnly?: boolean; /** * If the declarative tool was declared with the autosubmit attribute. */ autosubmit?: boolean; } /** * Represents the status of a tool invocation. * * @public */ export type WebMCPInvocationStatus = 'Completed' | 'Canceled' | 'Error'; interface ProtocolWebMCPTool { name: string; description: string; inputSchema?: object; annotations?: WebMCPAnnotation; frameId: string; backendNodeId?: number; stackTrace?: Protocol.Runtime.StackTrace; } interface ProtocolWebMCPToolsAddedEvent { tools: ProtocolWebMCPTool[]; } interface ProtocolWebMCPToolsRemovedEvent { tools: ProtocolWebMCPTool[]; } interface ProtocolWebMCPToolInvokedEvent { toolName: string; frameId: string; invocationId: string; input: string; } interface ProtocolWebMCPToolRespondedEvent { invocationId: string; status: WebMCPInvocationStatus; output?: any; errorText?: string; exception?: Protocol.Runtime.RemoteObject; } /** * Represents a registered WebMCP tool available on the page. * * @public */ export class WebMCPTool extends EventEmitter<{ /** Emitted when invocation starts. */ toolinvoked: WebMCPToolCall; }> { #webmcp: WebMCP; #backendNodeId?: number; #formElement?: ElementHandle<HTMLFormElement>; /** * Tool name. */ name: string; /** * Tool description. */ description: string; /** * Schema for the tool's input parameters. */ inputSchema?: object; /** * Optional annotations for the tool. */ annotations?: WebMCPAnnotation; /** * Frame the tool was defined for. */ frame: Frame; /** * Source location that defined the tool (if available). */ location?: ConsoleMessageLocation; /** * @internal */ rawStackTrace?: Protocol.Runtime.StackTrace; /** * @internal */ constructor(webmcp: WebMCP, tool: ProtocolWebMCPTool, frame: Frame) { super(); this.#webmcp = webmcp; this.name = tool.name; this.description = tool.description; this.inputSchema = tool.inputSchema; this.annotations = tool.annotations; this.frame = frame; this.#backendNodeId = tool.backendNodeId; if (tool.stackTrace?.callFrames.length) { this.location = { url: tool.stackTrace.callFrames[0]!.url, lineNumber: tool.stackTrace.callFrames[0]!.lineNumber, columnNumber: tool.stackTrace.callFrames[0]!.columnNumber, }; } this.rawStackTrace = tool.stackTrace; } /** * The corresponding ElementHandle when tool was registered via a form. */ get formElement(): Promise<ElementHandle<HTMLFormElement> | undefined> { return (async () => { if (this.#formElement && !this.#formElement.disposed) { return this.#formElement; } if (!this.#backendNodeId) { return undefined; } this.#formElement = (await (this.frame as CdpFrame).worlds[ MAIN_WORLD ].adoptBackendNode( this.#backendNodeId, )) as ElementHandle<HTMLFormElement>; return this.#formElement; })(); } /** * Executes tool with input parameters, matching tool's `inputSchema`. */ async execute(input: object = {}): Promise<WebMCPToolCallResult> { const {invocationId} = await this.#webmcp.invokeTool(this, input); return await new Promise<WebMCPToolCallResult>(resolve => { const handler = (event: WebMCPToolCallResult) => { if (event.id === invocationId) { this.#webmcp.off('toolresponded', handler); resolve(event); } }; this.#webmcp.on('toolresponded', handler); }); } } /** * @public */ export interface WebMCPToolsAddedEvent { /** * Array of tools that were added. */ tools: WebMCPTool[]; } /** * @public */ export interface WebMCPToolsRemovedEvent { /** * Array of tools that were removed. */ tools: WebMCPTool[]; } /** * @public */ export class WebMCPToolCall { /** * Tool invocation identifier. */ id: string; /** * Tool that was called. */ tool: WebMCPTool; /** * The input parameters used for the call. */ input: object; /** * @internal */ constructor(invocationId: string, tool: WebMCPTool, input: string) { this.id = invocationId; this.tool = tool; try { this.input = JSON.parse(input); } catch (error) { this.input = {}; debugError(error); } } } /** * @public */ export interface WebMCPToolCallResult { /** * Tool invocation identifier. */ id: string; /** * The corresponding tool call if available. */ call?: WebMCPToolCall; /** * Status of the invocation. */ status: WebMCPInvocationStatus; /** * Output or error delivered as delivered to the agent. Missing if `status` is anything * other than Completed. */ output?: any; /** * Error text. */ errorText?: string; /** * The exception object, if the javascript tool threw an error. */ exception?: Protocol.Runtime.RemoteObject; } /** * The experimental WebMCP class provides an API for the WebMCP API. * * See the * {@link https://pptr.dev/guides/webmcp|WebMCP guide} * for more details. * * @example * * ```ts * await page.goto('https://www.example.com'); * const tools = page.webmcp.tools(); * for (const tool of tools) { * console.log(`Tool found: ${tool.name} - ${tool.description}`); * } * ``` * * @experimental * @public */ export class WebMCP extends EventEmitter<{ /** Emitted when tools are added. */ toolsadded: WebMCPToolsAddedEvent; /** Emitted when tools are removed. */ toolsremoved: WebMCPToolsRemovedEvent; /** Emitted when a tool invocation starts. */ toolinvoked: WebMCPToolCall; /** Emitted when a tool invocation completes or fails. */ toolresponded: WebMCPToolCallResult; }> { #client: CDPSession; #frameManager: FrameManager; #tools = new Map<string, Map<string, WebMCPTool>>(); #pendingCalls = new Map<string, WebMCPToolCall>(); #onToolsAdded = (event: ProtocolWebMCPToolsAddedEvent) => { const tools: WebMCPTool[] = []; for (const tool of event.tools) { const frame = this.#frameManager.frame(tool.frameId); if (!frame) { continue; } const frameTools = this.#tools.get(tool.frameId) ?? new Map(); if (!this.#tools.has(tool.frameId)) { this.#tools.set(tool.frameId, frameTools); } const addedTool = new WebMCPTool(this, tool, frame); frameTools.set(tool.name, addedTool); tools.push(addedTool); } this.emit('toolsadded', {tools}); }; #onToolsRemoved = (event: ProtocolWebMCPToolsRemovedEvent) => { const tools: WebMCPTool[] = []; event.tools.forEach(tool => { const removedTool = this.#tools.get(tool.frameId)?.get(tool.name); if (removedTool) { tools.push(removedTool); } this.#tools.get(tool.frameId)?.delete(tool.name); }); this.emit('toolsremoved', {tools}); }; #onToolInvoked = (event: ProtocolWebMCPToolInvokedEvent) => { const tool = this.#tools.get(event.frameId)?.get(event.toolName); if (!tool) { return; } const call = new WebMCPToolCall(event.invocationId, tool, event.input); this.#pendingCalls.set(call.id, call); tool.emit('toolinvoked', call); this.emit('toolinvoked', call); }; #onToolResponded = (event: ProtocolWebMCPToolRespondedEvent) => { const call = this.#pendingCalls.get(event.invocationId); if (call) { this.#pendingCalls.delete(event.invocationId); } const response: WebMCPToolCallResult = { id: event.invocationId, call: call, status: event.status, output: event.output, errorText: event.errorText, exception: event.exception, }; this.emit('toolresponded', response); }; #onFrameNavigated = (frame: Frame) => { this.#pendingCalls.clear(); const frameTools = this.#tools.get(frame._id); if (!frameTools) { return; } const tools = Array.from(frameTools.values()); this.#tools.delete(frame._id); if (tools.length) { this.emit('toolsremoved', {tools}); } }; /** * @internal */ constructor(client: CDPSession, frameManager: FrameManager) { super(); this.#client = client; this.#frameManager = frameManager; this.#frameManager.on( FrameManagerEvent.FrameNavigated, this.#onFrameNavigated, ); this.#bindListeners(); } /** * @internal */ async initialize(): Promise<void> { // @ts-expect-error WebMCP is not yet in the Protocol types. return await this.#client.send('WebMCP.enable').catch(debugError); } /** * @internal */ async invokeTool( tool: WebMCPTool, input: object, ): Promise<{invocationId: string}> { // @ts-expect-error WebMCP is not yet in the Protocol types. return await this.#client.send('WebMCP.invokeTool', { frameId: tool.frame._id, toolName: tool.name, input, }); } /** * Gets all WebMCP tools defined by the page. */ tools(): WebMCPTool[] { return Array.from(this.#tools.values()).flatMap(toolMap => { return Array.from(toolMap.values()); }); } #bindListeners(): void { // @ts-expect-error WebMCP is not yet in the Protocol types. this.#client.on('WebMCP.toolsAdded', this.#onToolsAdded); // @ts-expect-error WebMCP is not yet in the Protocol types. this.#client.on('WebMCP.toolsRemoved', this.#onToolsRemoved); // @ts-expect-error WebMCP is not yet in the Protocol types. this.#client.on('WebMCP.toolInvoked', this.#onToolInvoked); // @ts-expect-error WebMCP is not yet in the Protocol types. this.#client.on('WebMCP.toolResponded', this.#onToolResponded); } /** * @internal */ updateClient(client: CDPSession): void { // @ts-expect-error WebMCP is not yet in the Protocol types. this.#client.off('WebMCP.toolsAdded', this.#onToolsAdded); // @ts-expect-error WebMCP is not yet in the Protocol types. this.#client.off('WebMCP.toolsRemoved', this.#onToolsRemoved); // @ts-expect-error WebMCP is not yet in the Protocol types. this.#client.off('WebMCP.toolInvoked', this.#onToolInvoked); // @ts-expect-error WebMCP is not yet in the Protocol types. this.#client.off('WebMCP.toolResponded', this.#onToolResponded); this.#client = client; this.#bindListeners(); } }