Skip to main content
Glama
interaction.ts10.9 kB
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}.` // ); // } // }); // } // }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/aakashH242/mcp-playwright'

If you have feedback or need assistance with the MCP directory API, please join our Discord server