UNPKG

puppeteer-core

Version:

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

275 lines 7.98 kB
/** * @license * Copyright 2026 Google Inc. * SPDX-License-Identifier: Apache-2.0 */ import { EventEmitter } from '../common/EventEmitter.js'; import { debugError } from '../common/util.js'; import { FrameManagerEvent } from './FrameManagerEvents.js'; import { MAIN_WORLD } from './IsolatedWorlds.js'; /** * Represents a registered WebMCP tool available on the page. * * @public */ export class WebMCPTool extends EventEmitter { #webmcp; #backendNodeId; #formElement; /** * Tool name. */ name; /** * Tool description. */ description; /** * Schema for the tool's input parameters. */ inputSchema; /** * Optional annotations for the tool. */ annotations; /** * Frame the tool was defined for. */ frame; /** * Source location that defined the tool (if available). */ location; /** * @internal */ rawStackTrace; /** * @internal */ constructor(webmcp, tool, 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() { return (async () => { if (this.#formElement && !this.#formElement.disposed) { return this.#formElement; } if (!this.#backendNodeId) { return undefined; } this.#formElement = (await this.frame.worlds[MAIN_WORLD].adoptBackendNode(this.#backendNodeId)); return this.#formElement; })(); } /** * Executes tool with input parameters, matching tool's `inputSchema`. */ async execute(input = {}) { const { invocationId } = await this.#webmcp.invokeTool(this, input); return await new Promise(resolve => { const handler = (event) => { if (event.id === invocationId) { this.#webmcp.off('toolresponded', handler); resolve(event); } }; this.#webmcp.on('toolresponded', handler); }); } } /** * @public */ export class WebMCPToolCall { /** * Tool invocation identifier. */ id; /** * Tool that was called. */ tool; /** * The input parameters used for the call. */ input; /** * @internal */ constructor(invocationId, tool, input) { this.id = invocationId; this.tool = tool; try { this.input = JSON.parse(input); } catch (error) { this.input = {}; debugError(error); } } } /** * 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 { #client; #frameManager; #tools = new Map(); #pendingCalls = new Map(); #onToolsAdded = (event) => { const tools = []; 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) => { const tools = []; 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) => { 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) => { const call = this.#pendingCalls.get(event.invocationId); if (call) { this.#pendingCalls.delete(event.invocationId); } const response = { id: event.invocationId, call: call, status: event.status, output: event.output, errorText: event.errorText, exception: event.exception, }; this.emit('toolresponded', response); }; #onFrameNavigated = (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, frameManager) { super(); this.#client = client; this.#frameManager = frameManager; this.#frameManager.on(FrameManagerEvent.FrameNavigated, this.#onFrameNavigated); this.#bindListeners(); } /** * @internal */ async initialize() { return await this.#client.send('WebMCP.enable').catch(debugError); } /** * @internal */ async invokeTool(tool, input) { // @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() { return Array.from(this.#tools.values()).flatMap(toolMap => { return Array.from(toolMap.values()); }); } #bindListeners() { this.#client.on('WebMCP.toolsAdded', this.#onToolsAdded); this.#client.on('WebMCP.toolsRemoved', this.#onToolsRemoved); this.#client.on('WebMCP.toolInvoked', this.#onToolInvoked); // @ts-expect-error M148 has non-final status type, update expected in M149 this.#client.on('WebMCP.toolResponded', this.#onToolResponded); } /** * @internal */ updateClient(client) { this.#client.off('WebMCP.toolsAdded', this.#onToolsAdded); this.#client.off('WebMCP.toolsRemoved', this.#onToolsRemoved); this.#client.off('WebMCP.toolInvoked', this.#onToolInvoked); // @ts-expect-error M148 has non-final status type, update expected in M149 this.#client.off('WebMCP.toolResponded', this.#onToolResponded); this.#client = client; this.#bindListeners(); } } //# sourceMappingURL=WebMCP.js.map