637 lines
24 KiB
JavaScript
637 lines
24 KiB
JavaScript
import { chromium, firefox, webkit, request } from 'playwright';
|
|
import { BROWSER_TOOLS, API_TOOLS } from './tools.js';
|
|
import { ActionRecorder } from './tools/codegen/recorder.js';
|
|
import { spawn } from 'child_process';
|
|
import { startCodegenSession, endCodegenSession, getCodegenSession, clearCodegenSession } from './tools/codegen/index.js';
|
|
import { ScreenshotTool, NavigationTool, CloseBrowserTool, ConsoleLogsTool, ExpectResponseTool, AssertResponseTool, CustomUserAgentTool, ResizeTool } from './tools/browser/index.js';
|
|
import { ClickTool, IframeClickTool, FillTool, SelectTool, HoverTool, EvaluateTool, IframeFillTool, UploadFileTool } from './tools/browser/interaction.js';
|
|
import { VisibleTextTool, VisibleHtmlTool } from './tools/browser/visiblePage.js';
|
|
import { GetRequestTool, PostRequestTool, PutRequestTool, PatchRequestTool, DeleteRequestTool } from './tools/api/requests.js';
|
|
import { GoBackTool, GoForwardTool } from './tools/browser/navigation.js';
|
|
import { DragTool, PressKeyTool } from './tools/browser/interaction.js';
|
|
import { SaveAsPdfTool } from './tools/browser/output.js';
|
|
import { ClickAndSwitchTabTool } from './tools/browser/interaction.js';
|
|
// Global state
|
|
let browser;
|
|
let page;
|
|
let currentBrowserType = 'chromium';
|
|
/**
|
|
* Resets browser and page variables
|
|
* Used when browser is closed
|
|
*/
|
|
export function resetBrowserState() {
|
|
browser = undefined;
|
|
page = undefined;
|
|
currentBrowserType = 'chromium';
|
|
}
|
|
/**
|
|
* Sets the provided page to the global page variable
|
|
* @param newPage The Page object to set as the global page
|
|
*/
|
|
export function setGlobalPage(newPage) {
|
|
page = newPage;
|
|
page.bringToFront(); // Bring the new tab to the front
|
|
}
|
|
// Tool instances
|
|
let screenshotTool;
|
|
let navigationTool;
|
|
let closeBrowserTool;
|
|
let consoleLogsTool;
|
|
let clickTool;
|
|
let iframeClickTool;
|
|
let iframeFillTool;
|
|
let fillTool;
|
|
let selectTool;
|
|
let hoverTool;
|
|
let uploadFileTool;
|
|
let evaluateTool;
|
|
let expectResponseTool;
|
|
let assertResponseTool;
|
|
let customUserAgentTool;
|
|
let visibleTextTool;
|
|
let visibleHtmlTool;
|
|
let resizeTool;
|
|
let getRequestTool;
|
|
let postRequestTool;
|
|
let putRequestTool;
|
|
let patchRequestTool;
|
|
let deleteRequestTool;
|
|
// Add these variables at the top with other tool declarations
|
|
let goBackTool;
|
|
let goForwardTool;
|
|
let dragTool;
|
|
let pressKeyTool;
|
|
let saveAsPdfTool;
|
|
let clickAndSwitchTabTool;
|
|
async function registerConsoleMessage(page) {
|
|
page.on("console", (msg) => {
|
|
if (consoleLogsTool) {
|
|
const type = msg.type();
|
|
const text = msg.text();
|
|
// "Unhandled Rejection In Promise" we injected
|
|
if (text.startsWith("[Playwright]")) {
|
|
const payload = text.replace("[Playwright]", "");
|
|
if (consoleLogsTool) {
|
|
consoleLogsTool.registerConsoleMessage("exception", payload);
|
|
}
|
|
}
|
|
else {
|
|
if (consoleLogsTool) {
|
|
consoleLogsTool.registerConsoleMessage(type, text);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
// Uncaught exception
|
|
page.on("pageerror", (error) => {
|
|
if (consoleLogsTool) {
|
|
const message = error.message;
|
|
const stack = error.stack || "";
|
|
consoleLogsTool.registerConsoleMessage("exception", `${message}\n${stack}`);
|
|
}
|
|
});
|
|
// Unhandled rejection in promise
|
|
await page.addInitScript(() => {
|
|
window.addEventListener("unhandledrejection", (event) => {
|
|
const reason = event.reason;
|
|
const message = typeof reason === "object" && reason !== null
|
|
? reason.message || JSON.stringify(reason)
|
|
: String(reason);
|
|
const stack = reason?.stack || "";
|
|
// Use console.error get "Unhandled Rejection In Promise"
|
|
console.error(`[Playwright][Unhandled Rejection In Promise] ${message}\n${stack}`);
|
|
});
|
|
});
|
|
}
|
|
/**
|
|
* Attempts to install browsers automatically
|
|
*/
|
|
async function installBrowsers(browserType = 'chromium') {
|
|
return new Promise((resolve) => {
|
|
console.error(`[Playwright MCP] Attempting to install ${browserType} browser...`);
|
|
const installProcess = spawn('npx', ['playwright', 'install', browserType], {
|
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
});
|
|
let output = '';
|
|
let errorOutput = '';
|
|
installProcess.stdout?.on('data', (data) => {
|
|
output += data.toString();
|
|
});
|
|
installProcess.stderr?.on('data', (data) => {
|
|
errorOutput += data.toString();
|
|
});
|
|
installProcess.on('close', (code) => {
|
|
if (code === 0) {
|
|
console.error(`[Playwright MCP] Successfully installed ${browserType} browser`);
|
|
resolve({
|
|
success: true,
|
|
message: `Successfully installed ${browserType} browser. Please try your request again.`
|
|
});
|
|
}
|
|
else {
|
|
console.error(`[Playwright MCP] Failed to install browser: ${errorOutput}`);
|
|
resolve({
|
|
success: false,
|
|
message: `Failed to automatically install ${browserType} browser. Please run: npx playwright install ${browserType}`
|
|
});
|
|
}
|
|
});
|
|
installProcess.on('error', (error) => {
|
|
console.error(`[Playwright MCP] Error during browser installation: ${error.message}`);
|
|
resolve({
|
|
success: false,
|
|
message: `Error during installation: ${error.message}. Please run: npx playwright install ${browserType}`
|
|
});
|
|
});
|
|
// Timeout after 2 minutes
|
|
setTimeout(() => {
|
|
installProcess.kill();
|
|
resolve({
|
|
success: false,
|
|
message: `Browser installation timed out. Please run manually: npx playwright install ${browserType}`
|
|
});
|
|
}, 120000);
|
|
});
|
|
}
|
|
/**
|
|
* Ensures a browser is launched and returns the page
|
|
*/
|
|
export async function ensureBrowser(browserSettings) {
|
|
try {
|
|
// Check if browser exists but is disconnected
|
|
if (browser && !browser.isConnected()) {
|
|
try {
|
|
await browser.close().catch(() => { });
|
|
}
|
|
catch (e) {
|
|
// Ignore errors when closing disconnected browser
|
|
}
|
|
// Reset browser and page references
|
|
resetBrowserState();
|
|
}
|
|
// Launch new browser if needed
|
|
if (!browser) {
|
|
const { viewport, userAgent, headless = false, browserType = 'chromium' } = browserSettings ?? {};
|
|
// If browser type is changing, force a new browser instance
|
|
if (browser && currentBrowserType !== browserType) {
|
|
try {
|
|
await browser.close().catch(() => { });
|
|
}
|
|
catch (e) {
|
|
// Ignore errors
|
|
}
|
|
resetBrowserState();
|
|
}
|
|
// Use the appropriate browser engine
|
|
let browserInstance;
|
|
switch (browserType) {
|
|
case 'firefox':
|
|
browserInstance = firefox;
|
|
break;
|
|
case 'webkit':
|
|
browserInstance = webkit;
|
|
break;
|
|
case 'chromium':
|
|
default:
|
|
browserInstance = chromium;
|
|
break;
|
|
}
|
|
const executablePath = process.env.CHROME_EXECUTABLE_PATH;
|
|
try {
|
|
browser = await browserInstance.launch({
|
|
headless,
|
|
executablePath: executablePath
|
|
});
|
|
currentBrowserType = browserType;
|
|
}
|
|
catch (launchError) {
|
|
// Check if error is due to missing browser executable
|
|
if (launchError.message?.includes("Executable doesn't exist") ||
|
|
launchError.message?.includes("Failed to launch") ||
|
|
launchError.message?.includes("browserType.launch")) {
|
|
console.error(`[Playwright MCP] Browser not found, attempting auto-installation...`);
|
|
const installResult = await installBrowsers(browserType);
|
|
if (installResult.success) {
|
|
// Try launching again after installation
|
|
browser = await browserInstance.launch({
|
|
headless,
|
|
executablePath: executablePath
|
|
});
|
|
currentBrowserType = browserType;
|
|
}
|
|
else {
|
|
throw new Error(installResult.message);
|
|
}
|
|
}
|
|
else {
|
|
throw launchError;
|
|
}
|
|
}
|
|
// Add cleanup logic when browser is disconnected
|
|
browser.on('disconnected', () => {
|
|
browser = undefined;
|
|
page = undefined;
|
|
});
|
|
const context = await browser.newContext({
|
|
...userAgent && { userAgent },
|
|
viewport: {
|
|
width: viewport?.width ?? 1280,
|
|
height: viewport?.height ?? 720,
|
|
},
|
|
deviceScaleFactor: 1,
|
|
});
|
|
page = await context.newPage();
|
|
// Register console message handler
|
|
await registerConsoleMessage(page);
|
|
}
|
|
// Verify page is still valid
|
|
if (!page || page.isClosed()) {
|
|
// Create a new page if the current one is invalid
|
|
const context = browser.contexts()[0] || await browser.newContext();
|
|
page = await context.newPage();
|
|
// Re-register console message handler
|
|
await registerConsoleMessage(page);
|
|
}
|
|
return page;
|
|
}
|
|
catch (error) {
|
|
// If something went wrong, clean up completely and retry once
|
|
try {
|
|
if (browser) {
|
|
await browser.close().catch(() => { });
|
|
}
|
|
}
|
|
catch (e) {
|
|
// Ignore errors during cleanup
|
|
}
|
|
resetBrowserState();
|
|
// Check if error is due to missing browser, if so attempt install
|
|
const errorMessage = error.message;
|
|
if (errorMessage?.includes("Executable doesn't exist") ||
|
|
errorMessage?.includes("Failed to launch") ||
|
|
errorMessage?.includes("browserType.launch")) {
|
|
const { browserType = 'chromium' } = browserSettings ?? {};
|
|
console.error(`[Playwright MCP] Browser not found in retry, attempting auto-installation...`);
|
|
const installResult = await installBrowsers(browserType);
|
|
if (!installResult.success) {
|
|
throw new Error(installResult.message);
|
|
}
|
|
// If installation successful, continue to retry
|
|
}
|
|
// Try one more time from scratch
|
|
const { viewport, userAgent, headless = false, browserType = 'chromium' } = browserSettings ?? {};
|
|
// Use the appropriate browser engine
|
|
let browserInstance;
|
|
switch (browserType) {
|
|
case 'firefox':
|
|
browserInstance = firefox;
|
|
break;
|
|
case 'webkit':
|
|
browserInstance = webkit;
|
|
break;
|
|
case 'chromium':
|
|
default:
|
|
browserInstance = chromium;
|
|
break;
|
|
}
|
|
browser = await browserInstance.launch({ headless });
|
|
currentBrowserType = browserType;
|
|
browser.on('disconnected', () => {
|
|
browser = undefined;
|
|
page = undefined;
|
|
});
|
|
const context = await browser.newContext({
|
|
...userAgent && { userAgent },
|
|
viewport: {
|
|
width: viewport?.width ?? 1280,
|
|
height: viewport?.height ?? 720,
|
|
},
|
|
deviceScaleFactor: 1,
|
|
});
|
|
page = await context.newPage();
|
|
await registerConsoleMessage(page);
|
|
return page;
|
|
}
|
|
}
|
|
/**
|
|
* Creates a new API request context
|
|
*/
|
|
async function ensureApiContext(url) {
|
|
return await request.newContext({
|
|
baseURL: url,
|
|
});
|
|
}
|
|
/**
|
|
* Initialize all tool instances
|
|
*/
|
|
function initializeTools(server) {
|
|
// Browser tools
|
|
if (!screenshotTool)
|
|
screenshotTool = new ScreenshotTool(server);
|
|
if (!navigationTool)
|
|
navigationTool = new NavigationTool(server);
|
|
if (!closeBrowserTool)
|
|
closeBrowserTool = new CloseBrowserTool(server);
|
|
if (!consoleLogsTool)
|
|
consoleLogsTool = new ConsoleLogsTool(server);
|
|
if (!clickTool)
|
|
clickTool = new ClickTool(server);
|
|
if (!iframeClickTool)
|
|
iframeClickTool = new IframeClickTool(server);
|
|
if (!iframeFillTool)
|
|
iframeFillTool = new IframeFillTool(server);
|
|
if (!fillTool)
|
|
fillTool = new FillTool(server);
|
|
if (!selectTool)
|
|
selectTool = new SelectTool(server);
|
|
if (!hoverTool)
|
|
hoverTool = new HoverTool(server);
|
|
if (!uploadFileTool)
|
|
uploadFileTool = new UploadFileTool(server);
|
|
if (!evaluateTool)
|
|
evaluateTool = new EvaluateTool(server);
|
|
if (!expectResponseTool)
|
|
expectResponseTool = new ExpectResponseTool(server);
|
|
if (!assertResponseTool)
|
|
assertResponseTool = new AssertResponseTool(server);
|
|
if (!customUserAgentTool)
|
|
customUserAgentTool = new CustomUserAgentTool(server);
|
|
if (!visibleTextTool)
|
|
visibleTextTool = new VisibleTextTool(server);
|
|
if (!visibleHtmlTool)
|
|
visibleHtmlTool = new VisibleHtmlTool(server);
|
|
if (!resizeTool)
|
|
resizeTool = new ResizeTool(server);
|
|
// API tools
|
|
if (!getRequestTool)
|
|
getRequestTool = new GetRequestTool(server);
|
|
if (!postRequestTool)
|
|
postRequestTool = new PostRequestTool(server);
|
|
if (!putRequestTool)
|
|
putRequestTool = new PutRequestTool(server);
|
|
if (!patchRequestTool)
|
|
patchRequestTool = new PatchRequestTool(server);
|
|
if (!deleteRequestTool)
|
|
deleteRequestTool = new DeleteRequestTool(server);
|
|
// Initialize new tools
|
|
if (!goBackTool)
|
|
goBackTool = new GoBackTool(server);
|
|
if (!goForwardTool)
|
|
goForwardTool = new GoForwardTool(server);
|
|
if (!dragTool)
|
|
dragTool = new DragTool(server);
|
|
if (!pressKeyTool)
|
|
pressKeyTool = new PressKeyTool(server);
|
|
if (!saveAsPdfTool)
|
|
saveAsPdfTool = new SaveAsPdfTool(server);
|
|
if (!clickAndSwitchTabTool)
|
|
clickAndSwitchTabTool = new ClickAndSwitchTabTool(server);
|
|
}
|
|
/**
|
|
* Main handler for tool calls
|
|
*/
|
|
export async function handleToolCall(name, args, server) {
|
|
// Initialize tools
|
|
initializeTools(server);
|
|
try {
|
|
// Handle codegen tools
|
|
switch (name) {
|
|
case 'start_codegen_session':
|
|
return await handleCodegenResult(startCodegenSession.handler(args));
|
|
case 'end_codegen_session':
|
|
return await handleCodegenResult(endCodegenSession.handler(args));
|
|
case 'get_codegen_session':
|
|
return await handleCodegenResult(getCodegenSession.handler(args));
|
|
case 'clear_codegen_session':
|
|
return await handleCodegenResult(clearCodegenSession.handler(args));
|
|
}
|
|
// Record tool action if there's an active session
|
|
const recorder = ActionRecorder.getInstance();
|
|
const activeSession = recorder.getActiveSession();
|
|
if (activeSession && name !== 'playwright_close') {
|
|
recorder.recordAction(name, args);
|
|
}
|
|
// Special case for browser close to ensure it always works
|
|
if (name === "playwright_close") {
|
|
if (browser) {
|
|
try {
|
|
if (browser.isConnected()) {
|
|
await browser.close().catch(e => console.error("Error closing browser:", e));
|
|
}
|
|
}
|
|
catch (error) {
|
|
console.error("Error during browser close in handler:", error);
|
|
}
|
|
finally {
|
|
resetBrowserState();
|
|
}
|
|
return {
|
|
content: [{
|
|
type: "text",
|
|
text: "Browser closed successfully",
|
|
}],
|
|
isError: false,
|
|
};
|
|
}
|
|
return {
|
|
content: [{
|
|
type: "text",
|
|
text: "No browser instance to close",
|
|
}],
|
|
isError: false,
|
|
};
|
|
}
|
|
// Check if we have a disconnected browser that needs cleanup
|
|
if (browser && !browser.isConnected() && BROWSER_TOOLS.includes(name)) {
|
|
try {
|
|
await browser.close().catch(() => { }); // Ignore errors
|
|
}
|
|
catch (e) {
|
|
// Ignore any errors during cleanup
|
|
}
|
|
resetBrowserState();
|
|
}
|
|
// Prepare context based on tool requirements
|
|
const context = {
|
|
server
|
|
};
|
|
// Set up browser if needed
|
|
if (BROWSER_TOOLS.includes(name)) {
|
|
const browserSettings = {
|
|
viewport: {
|
|
width: args.width,
|
|
height: args.height
|
|
},
|
|
userAgent: name === "playwright_custom_user_agent" ? args.userAgent : undefined,
|
|
headless: args.headless,
|
|
browserType: args.browserType || 'chromium'
|
|
};
|
|
try {
|
|
context.page = await ensureBrowser(browserSettings);
|
|
context.browser = browser;
|
|
}
|
|
catch (error) {
|
|
return {
|
|
content: [{
|
|
type: "text",
|
|
text: `Failed to initialize browser: ${error.message}. Please try again.`,
|
|
}],
|
|
isError: true,
|
|
};
|
|
}
|
|
}
|
|
// Set up API context if needed
|
|
if (API_TOOLS.includes(name)) {
|
|
try {
|
|
context.apiContext = await ensureApiContext(args.url);
|
|
}
|
|
catch (error) {
|
|
return {
|
|
content: [{
|
|
type: "text",
|
|
text: `Failed to initialize API context: ${error.message}`,
|
|
}],
|
|
isError: true,
|
|
};
|
|
}
|
|
}
|
|
// Route to appropriate tool
|
|
switch (name) {
|
|
// Browser tools
|
|
case "playwright_navigate":
|
|
return await navigationTool.execute(args, context);
|
|
case "playwright_screenshot":
|
|
return await screenshotTool.execute(args, context);
|
|
case "playwright_resize":
|
|
return await resizeTool.execute(args, context);
|
|
case "playwright_close":
|
|
return await closeBrowserTool.execute(args, context);
|
|
case "playwright_console_logs":
|
|
return await consoleLogsTool.execute(args, context);
|
|
case "playwright_click":
|
|
return await clickTool.execute(args, context);
|
|
case "playwright_iframe_click":
|
|
return await iframeClickTool.execute(args, context);
|
|
case "playwright_iframe_fill":
|
|
return await iframeFillTool.execute(args, context);
|
|
case "playwright_fill":
|
|
return await fillTool.execute(args, context);
|
|
case "playwright_select":
|
|
return await selectTool.execute(args, context);
|
|
case "playwright_hover":
|
|
return await hoverTool.execute(args, context);
|
|
case "playwright_upload_file":
|
|
return await uploadFileTool.execute(args, context);
|
|
case "playwright_evaluate":
|
|
return await evaluateTool.execute(args, context);
|
|
case "playwright_expect_response":
|
|
return await expectResponseTool.execute(args, context);
|
|
case "playwright_assert_response":
|
|
return await assertResponseTool.execute(args, context);
|
|
case "playwright_custom_user_agent":
|
|
return await customUserAgentTool.execute(args, context);
|
|
case "playwright_get_visible_text":
|
|
return await visibleTextTool.execute(args, context);
|
|
case "playwright_get_visible_html":
|
|
return await visibleHtmlTool.execute(args, context);
|
|
// API tools
|
|
case "playwright_get":
|
|
return await getRequestTool.execute(args, context);
|
|
case "playwright_post":
|
|
return await postRequestTool.execute(args, context);
|
|
case "playwright_put":
|
|
return await putRequestTool.execute(args, context);
|
|
case "playwright_patch":
|
|
return await patchRequestTool.execute(args, context);
|
|
case "playwright_delete":
|
|
return await deleteRequestTool.execute(args, context);
|
|
// New tools
|
|
case "playwright_go_back":
|
|
return await goBackTool.execute(args, context);
|
|
case "playwright_go_forward":
|
|
return await goForwardTool.execute(args, context);
|
|
case "playwright_drag":
|
|
return await dragTool.execute(args, context);
|
|
case "playwright_press_key":
|
|
return await pressKeyTool.execute(args, context);
|
|
case "playwright_save_as_pdf":
|
|
return await saveAsPdfTool.execute(args, context);
|
|
case "playwright_click_and_switch_tab":
|
|
return await clickAndSwitchTabTool.execute(args, context);
|
|
default:
|
|
return {
|
|
content: [{
|
|
type: "text",
|
|
text: `Unknown tool: ${name}`,
|
|
}],
|
|
isError: true,
|
|
};
|
|
}
|
|
}
|
|
catch (error) {
|
|
// Handle browser-specific errors at the top level
|
|
if (BROWSER_TOOLS.includes(name)) {
|
|
const errorMessage = error.message;
|
|
if (errorMessage.includes("Target page, context or browser has been closed") ||
|
|
errorMessage.includes("Browser has been disconnected") ||
|
|
errorMessage.includes("Target closed") ||
|
|
errorMessage.includes("Protocol error") ||
|
|
errorMessage.includes("Connection closed")) {
|
|
// Reset browser state if it's a connection issue
|
|
resetBrowserState();
|
|
return {
|
|
content: [{
|
|
type: "text",
|
|
text: `Browser connection error: ${errorMessage}. Browser state has been reset, please try again.`,
|
|
}],
|
|
isError: true,
|
|
};
|
|
}
|
|
}
|
|
return {
|
|
content: [{
|
|
type: "text",
|
|
text: error instanceof Error ? error.message : String(error),
|
|
}],
|
|
isError: true,
|
|
};
|
|
}
|
|
}
|
|
/**
|
|
* Helper function to handle codegen tool results
|
|
*/
|
|
async function handleCodegenResult(resultPromise) {
|
|
try {
|
|
const result = await resultPromise;
|
|
return {
|
|
content: [{
|
|
type: "text",
|
|
text: JSON.stringify(result),
|
|
}],
|
|
isError: false,
|
|
};
|
|
}
|
|
catch (error) {
|
|
return {
|
|
content: [{
|
|
type: "text",
|
|
text: error instanceof Error ? error.message : String(error),
|
|
}],
|
|
isError: true,
|
|
};
|
|
}
|
|
}
|
|
/**
|
|
* Get console logs
|
|
*/
|
|
export function getConsoleLogs() {
|
|
return consoleLogsTool?.getConsoleLogs() ?? [];
|
|
}
|
|
/**
|
|
* Get screenshots
|
|
*/
|
|
export function getScreenshots() {
|
|
return screenshotTool?.getScreenshots() ?? new Map();
|
|
}
|
|
export { registerConsoleMessage };
|