import { promises as fs } from "node:fs";
import path from "node:path";
import { getSessionIdForServer } from "../../resourceManager.js";
import { setGlobalPage } from "../../toolHandler.js";
import { getUploadEndpointUrl, parseUploadResourceUri, resolveUploadResource } from "../../uploadManager.js";
import { createErrorResponse, createSuccessResponse, type ToolContext, type ToolResponse } from "../common/types.js";
import { BrowserToolBase } from "./base.js";
/**
* Tool for clicking elements on the page
*/
export class ClickTool extends BrowserToolBase {
/**
* Execute the click tool
*/
async execute(args: any, context: ToolContext): Promise<ToolResponse> {
return this.safeExecute(context, async (page) => {
await page.click(args.selector);
return createSuccessResponse(`Clicked element: ${args.selector}`);
});
}
}
/**
* Tool for clicking a link and switching to the new tab
*/
export class ClickAndSwitchTabTool extends BrowserToolBase {
/**
* Execute the click and switch tab tool
*/
async execute(args: any, context: ToolContext): Promise<ToolResponse> {
return this.safeExecute(context, async (page) => {
// Listen for a new tab to open
const [newPage] = await Promise.all([
//context.browser.waitForEvent('page'), // Wait for a new page (tab) to open
page
.context()
.waitForEvent("page"), // Wait for a new page (tab) to open
page.click(args.selector), // Click the link that opens the new tab
]);
// Wait for the new page to load
await newPage.waitForLoadState("domcontentloaded");
// Switch control to the new tab
setGlobalPage(newPage);
//page= newPage; // Update the current page to the new tab
//context.page = newPage;
//context.page.bringToFront(); // Bring the new tab to the front
return createSuccessResponse(`Clicked link and switched to new tab: ${newPage.url()}`);
//return createSuccessResponse(`Clicked link and switched to new tab: ${context.page.url()}`);
});
}
}
/**
* Tool for clicking elements inside iframes
*/
export class IframeClickTool extends BrowserToolBase {
/**
* Execute the iframe click tool
*/
async execute(args: any, context: ToolContext): Promise<ToolResponse> {
return this.safeExecute(context, async (page) => {
const frame = page.frameLocator(args.iframeSelector);
if (!frame) {
return createErrorResponse(`Iframe not found: ${args.iframeSelector}`);
}
await frame.locator(args.selector).click();
return createSuccessResponse(`Clicked element ${args.selector} inside iframe ${args.iframeSelector}`);
});
}
}
/**
* Tool for filling elements inside iframes
*/
export class IframeFillTool extends BrowserToolBase {
/**
* Execute the iframe fill tool
*/
async execute(args: any, context: ToolContext): Promise<ToolResponse> {
return this.safeExecute(context, async (page) => {
const frame = page.frameLocator(args.iframeSelector);
if (!frame) {
return createErrorResponse(`Iframe not found: ${args.iframeSelector}`);
}
await frame.locator(args.selector).fill(args.value);
return createSuccessResponse(
`Filled element ${args.selector} inside iframe ${args.iframeSelector} with: ${args.value}`,
);
});
}
}
/**
* Tool for filling form fields
*/
export class FillTool extends BrowserToolBase {
/**
* Execute the fill tool
*/
async execute(args: any, context: ToolContext): Promise<ToolResponse> {
return this.safeExecute(context, async (page) => {
await page.waitForSelector(args.selector);
await page.fill(args.selector, args.value);
return createSuccessResponse(`Filled ${args.selector} with: ${args.value}`);
});
}
}
/**
* Tool for selecting options from dropdown menus
*/
export class SelectTool extends BrowserToolBase {
/**
* Execute the select tool
*/
async execute(args: any, context: ToolContext): Promise<ToolResponse> {
return this.safeExecute(context, async (page) => {
await page.waitForSelector(args.selector);
await page.selectOption(args.selector, args.value);
return createSuccessResponse(`Selected ${args.selector} with: ${args.value}`);
});
}
}
/**
* Tool for hovering over elements
*/
export class HoverTool extends BrowserToolBase {
/**
* Execute the hover tool
*/
async execute(args: any, context: ToolContext): Promise<ToolResponse> {
return this.safeExecute(context, async (page) => {
await page.waitForSelector(args.selector);
await page.hover(args.selector);
return createSuccessResponse(`Hovered ${args.selector}`);
});
}
}
/**
* Tool for uploading files
*/
export class UploadFileTool extends BrowserToolBase {
private async prepareUploadFile(
args: any,
server: any,
_context: ToolContext,
): Promise<{ path: string; cleanup: () => Promise<void>; displayName: string } | { error: string }> {
const sessionId = getSessionIdForServer(server);
const uploadResourceUri = args.uploadResourceUri ?? args.fileResourceUri;
const inHttpMode = Boolean(sessionId);
if (uploadResourceUri) {
const parsed = parseUploadResourceUri(uploadResourceUri);
const targetSession = parsed?.sessionId ?? sessionId;
if (!parsed || !targetSession) {
return { error: "Invalid upload resource URI" };
}
const meta = await resolveUploadResource({
resourceUri: uploadResourceUri,
sessionId: targetSession,
});
if (!meta) {
return { error: "Uploaded file could not be resolved for this session" };
}
const cleanup = async () => {};
return { path: meta.filePath, cleanup, displayName: meta.name };
}
if (args.filePath) {
const resolvedPath = path.resolve(args.filePath);
try {
await fs.access(resolvedPath);
return {
path: resolvedPath,
cleanup: async () => {},
displayName: args.filePath,
};
} catch {
if (inHttpMode) {
return {
error: `File not found at path: ${resolvedPath}. In HTTP mode, first call construct_upload_url, upload the file, then call playwright_upload_file with uploadResourceUri.`,
};
}
return { error: `File not found at path: ${resolvedPath}` };
}
}
if (inHttpMode) {
const uploadUrl = getUploadEndpointUrl();
const hint = uploadUrl
? ` Call construct_upload_url to obtain the upload endpoint for this session (e.g., ${uploadUrl}/<sessionId>).`
: "";
return {
error: `No file provided. In HTTP mode, first upload a file and provide uploadResourceUri.${hint}`,
};
}
return {
error: "filePath is required in stdio mode.",
};
}
/**
* Execute the upload file tool
*/
async execute(args: any, context: ToolContext): Promise<ToolResponse> {
return this.safeExecute(context, async (page) => {
if (!args.selector) {
return createErrorResponse("Selector is required to upload a file");
}
const prepared = await this.prepareUploadFile(args, context.server, context);
if ("error" in prepared) {
return createErrorResponse(prepared.error);
}
const { path: uploadPath, cleanup, displayName } = prepared;
try {
await page.waitForSelector(args.selector);
await page.setInputFiles(args.selector, uploadPath);
return createSuccessResponse(`Uploaded file '${displayName}' to '${args.selector}'`);
} finally {
await cleanup().catch(() => {});
}
});
}
}
/**
* Tool for executing JavaScript in the browser
*/
export class EvaluateTool extends BrowserToolBase {
/**
* Execute the evaluate tool
*/
async execute(args: any, context: ToolContext): Promise<ToolResponse> {
return this.safeExecute(context, async (page) => {
const result = await page.evaluate(args.script);
// Convert result to string for display
let resultStr: string;
try {
resultStr = JSON.stringify(result, null, 2);
} catch (_error) {
resultStr = String(result);
}
return createSuccessResponse([`Executed JavaScript:`, `${args.script}`, `Result:`, `${resultStr}`]);
});
}
}
/**
* Tool for dragging elements on the page
*/
export class DragTool extends BrowserToolBase {
/**
* Execute the drag tool
*/
async execute(args: any, context: ToolContext): Promise<ToolResponse> {
return this.safeExecute(context, async (page) => {
const sourceElement = await page.waitForSelector(args.sourceSelector);
const targetElement = await page.waitForSelector(args.targetSelector);
const sourceBound = await sourceElement.boundingBox();
const targetBound = await targetElement.boundingBox();
if (!sourceBound || !targetBound) {
return createErrorResponse("Could not get element positions for drag operation");
}
await page.mouse.move(sourceBound.x + sourceBound.width / 2, sourceBound.y + sourceBound.height / 2);
await page.mouse.down();
await page.mouse.move(targetBound.x + targetBound.width / 2, targetBound.y + targetBound.height / 2);
await page.mouse.up();
return createSuccessResponse(`Dragged element from ${args.sourceSelector} to ${args.targetSelector}`);
});
}
}
/**
* Tool for pressing keyboard keys
*/
export class PressKeyTool extends BrowserToolBase {
/**
* Execute the key press tool
*/
async execute(args: any, context: ToolContext): Promise<ToolResponse> {
return this.safeExecute(context, async (page) => {
if (args.selector) {
await page.waitForSelector(args.selector);
await page.focus(args.selector);
}
await page.keyboard.press(args.key);
return createSuccessResponse(`Pressed key: ${args.key}`);
});
}
}
/**
* Tool for switching browser tabs
*/
// export class SwitchTabTool extends BrowserToolBase {
// /**
// * Switch the tab to the specified index
// */
// async execute(args: any, context: ToolContext): Promise<ToolResponse> {
// return this.safeExecute(context, async (page) => {
// const tabs = await browser.page;
// // Validate the tab index
// const tabIndex = Number(args.index);
// if (isNaN(tabIndex)) {
// return createErrorResponse(`Invalid tab index: ${args.index}. It must be a number.`);
// }
// if (tabIndex >= 0 && tabIndex < tabs.length) {
// await tabs[tabIndex].bringToFront();
// return createSuccessResponse(`Switched to tab with index ${tabIndex}`);
// } else {
// return createErrorResponse(
// `Tab index out of range: ${tabIndex}. Available tabs: 0 to ${tabs.length - 1}.`
// );
// }
// });
// }
// }