@executeautomation/playwright-mcp-server
Version:
Model Context Protocol servers for Playwright
495 lines (494 loc) • 18.6 kB
JavaScript
import { chromium, firefox, webkit, request } from 'playwright';
import { BROWSER_TOOLS, API_TOOLS } from './tools.js';
import { ActionRecorder } from './tools/codegen/recorder.js';
import { startCodegenSession, endCodegenSession, getCodegenSession, clearCodegenSession } from './tools/codegen/index.js';
import { ScreenshotTool, NavigationTool, CloseBrowserTool, ConsoleLogsTool, ExpectResponseTool, AssertResponseTool, CustomUserAgentTool } from './tools/browser/index.js';
import { ClickTool, IframeClickTool, FillTool, SelectTool, HoverTool, EvaluateTool } 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';
// 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';
}
// Tool instances
let screenshotTool;
let navigationTool;
let closeBrowserTool;
let consoleLogsTool;
let clickTool;
let iframeClickTool;
let fillTool;
let selectTool;
let hoverTool;
let evaluateTool;
let expectResponseTool;
let assertResponseTool;
let customUserAgentTool;
let visibleTextTool;
let visibleHtmlTool;
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;
/**
* Ensures a browser is launched and returns the page
*/
async function ensureBrowser(browserSettings) {
try {
// Check if browser exists but is disconnected
if (browser && !browser.isConnected()) {
console.error("Browser exists but is disconnected. Cleaning up...");
try {
await browser.close().catch(err => console.error("Error closing disconnected browser:", err));
}
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(err => console.error("Error closing browser on type change:", err));
}
catch (e) {
// Ignore errors
}
resetBrowserState();
}
console.error(`Launching new ${browserType} browser instance...`);
// 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;
// Add cleanup logic when browser is disconnected
browser.on('disconnected', () => {
console.error("Browser disconnected event triggered");
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
page.on("console", (msg) => {
if (consoleLogsTool) {
consoleLogsTool.registerConsoleMessage(msg.type(), msg.text());
}
});
}
// Verify page is still valid
if (!page || page.isClosed()) {
console.error("Page is closed or invalid. Creating new page...");
// 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
page.on("console", (msg) => {
if (consoleLogsTool) {
consoleLogsTool.registerConsoleMessage(msg.type(), msg.text());
}
});
}
return page;
}
catch (error) {
console.error("Error ensuring browser:", 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();
// 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', () => {
console.error("Browser disconnected event triggered (retry)");
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();
page.on("console", (msg) => {
if (consoleLogsTool) {
consoleLogsTool.registerConsoleMessage(msg.type(), msg.text());
}
});
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 (!fillTool)
fillTool = new FillTool(server);
if (!selectTool)
selectTool = new SelectTool(server);
if (!hoverTool)
hoverTool = new HoverTool(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);
// 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);
}
/**
* 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)) {
console.error("Detected disconnected browser before tool execution, cleaning up...");
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) {
console.error("Failed to ensure browser:", 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_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_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_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);
default:
return {
content: [{
type: "text",
text: `Unknown tool: ${name}`,
}],
isError: true,
};
}
}
catch (error) {
console.error(`Error handling tool ${name}:`, 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();
}