@eimsto/playwright-mcp-server
Version:
Model Context Protocol servers for Playwright
437 lines (436 loc) • 16.3 kB
JavaScript
import { chromium, request } from "playwright";
import { BROWSER_TOOLS, API_TOOLS } from "./tools.js";
import fs from 'node:fs';
import * as os from 'os';
import * as path from 'path';
// Global state
let browser;
let page;
const consoleLogs = [];
const screenshots = new Map();
const defaultDownloadsPath = path.join(os.homedir(), 'Downloads');
async function ensureBrowser(viewport) {
if (!browser) {
browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
viewport: {
width: viewport?.width ?? 1920,
height: viewport?.height ?? 1080,
},
deviceScaleFactor: 1,
});
page = await context.newPage();
page.on("console", (msg) => {
const logEntry = `[${msg.type()}] ${msg.text()}`;
consoleLogs.push(logEntry);
});
}
return page;
}
async function ensureApiContext(url) {
return await request.newContext({
baseURL: url,
});
}
export async function handleToolCall(name, args, server) {
// Check if the tool requires browser interaction
const requiresBrowser = BROWSER_TOOLS.includes(name);
// Check if the tool requires api interaction
const requiresApi = API_TOOLS.includes(name);
let page;
let apiContext;
// Only launch browser if the tool requires browser interaction
if (requiresBrowser) {
page = await ensureBrowser({
width: args.width,
height: args.height
});
}
// Set up API context for API-related operations
if (requiresApi) {
apiContext = await ensureApiContext(args.url);
}
switch (name) {
case "playwright_navigate":
try {
await page.goto(args.url, {
timeout: args.timeout || 30000,
waitUntil: args.waitUntil || "load"
});
return {
content: [{
type: "text",
text: `Navigated to ${args.url}`,
}],
isError: false,
};
}
catch (error) {
return {
content: [{
type: "text",
text: `Navigation failed: ${error.message}`,
}],
isError: true,
};
}
case "playwright_screenshot": {
try {
const screenshotOptions = {
type: args.type || "png",
fullPage: !!args.fullPage
};
if (args.selector) {
const element = await page.$(args.selector);
if (!element) {
return {
content: [{
type: "text",
text: `Element not found: ${args.selector}`,
}],
isError: true
};
}
screenshotOptions.element = element;
}
if (args.mask) {
screenshotOptions.mask = await Promise.all(args.mask.map(async (selector) => await page.$(selector)));
}
const screenshot = await page.screenshot(screenshotOptions);
const base64Screenshot = screenshot.toString('base64');
const responseContent = [];
// Handle PNG file saving
if (args.savePng !== false) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `${args.name}-${timestamp}.png`;
const downloadsDir = args.downloadsDir || defaultDownloadsPath;
// Create downloads directory if it doesn't exist
if (!fs.existsSync(downloadsDir)) {
fs.mkdirSync(downloadsDir, { recursive: true });
}
const filePath = path.join(downloadsDir, filename);
await fs.promises.writeFile(filePath, screenshot);
responseContent.push({
type: "text",
text: `Screenshot saved to: ${filePath}`,
});
}
// Handle base64 storage
if (args.storeBase64 !== false) {
screenshots.set(args.name, base64Screenshot);
server.notification({
method: "notifications/resources/list_changed",
});
responseContent.push({
type: "image",
data: base64Screenshot,
mimeType: "image/png",
});
}
return {
content: responseContent,
isError: false,
};
}
catch (error) {
return {
content: [{
type: "text",
text: `Screenshot failed: ${error.message}`,
}],
isError: true,
};
}
}
case "playwright_click":
try {
await page.click(args.selector);
return {
content: [{
type: "text",
text: `Clicked: ${args.selector}`,
}],
isError: false,
};
}
catch (error) {
return {
content: [{
type: "text",
text: `Failed to click ${args.selector}: ${error.message}`,
}],
isError: true,
};
}
case "playwright_fill":
try {
await page.waitForSelector(args.selector);
await page.fill(args.selector, args.value);
return {
content: [{
type: "text",
text: `Filled ${args.selector} with: ${args.value}`,
}],
isError: false,
};
}
catch (error) {
return {
content: [{
type: "text",
text: `Failed to type ${args.selector}: ${error.message}`,
}],
isError: true,
};
}
case "playwright_select":
try {
await page.waitForSelector(args.selector);
await page.selectOption(args.selector, args.value);
return {
content: [{
type: "text",
text: `Selected ${args.selector} with: ${args.value}`,
}],
isError: false,
};
}
catch (error) {
return {
content: [{
type: "text",
text: `Failed to select ${args.selector}: ${error.message}`,
}],
isError: true,
};
}
case "playwright_hover":
try {
await page.waitForSelector(args.selector);
await page.hover(args.selector);
return {
content: [{
type: "text",
text: `Hovered ${args.selector}`,
}],
isError: false,
};
}
catch (error) {
return {
content: [{
type: "text",
text: `Failed to hover ${args.selector}: ${error.message}`,
}],
isError: true,
};
}
case "playwright_evaluate":
try {
const result = await page.evaluate((script) => {
const logs = [];
const originalConsole = { ...console };
['log', 'info', 'warn', 'error'].forEach(method => {
console[method] = (...args) => {
logs.push(`[${method}] ${args.join(' ')}`);
originalConsole[method](...args);
};
});
try {
const result = eval(script);
Object.assign(console, originalConsole);
return { result, logs };
}
catch (error) {
Object.assign(console, originalConsole);
throw error;
}
}, args.script);
return {
content: [
{
type: "text",
text: `Execution result:\n${JSON.stringify(result.result, null, 2)}\n\nConsole output:\n${result.logs.join('\n')}`,
},
],
isError: false,
};
}
catch (error) {
return {
content: [{
type: "text",
text: `Script execution failed: ${error.message}`,
}],
isError: true,
};
}
case "playwright_get":
try {
var response = await apiContext.get(args.url);
return {
content: [{
type: "text",
text: `Performed GET Operation ${args.url}`,
},
{
type: "text",
text: `Response: ${JSON.stringify(await response.json(), null, 2)}`,
},
{
type: "text",
text: `Response code ${response.status()}`
}
],
isError: false,
};
}
catch (error) {
return {
content: [{
type: "text",
text: `Failed to perform GET operation on ${args.url}: ${error.message}`,
}],
isError: true,
};
}
case "playwright_post":
try {
var data = {
data: args.value,
headers: {
'Content-Type': 'application/json'
}
};
var response = await apiContext.post(args.url, data);
return {
content: [{
type: "text",
text: `Performed POST Operation ${args.url} with data ${JSON.stringify(args.value, null, 2)}`,
},
{
type: "text",
text: `Response: ${JSON.stringify(await response.json(), null, 2)}`,
},
{
type: "text",
text: `Response code ${response.status()}`
}],
isError: false,
};
}
catch (error) {
return {
content: [{
type: "text",
text: `Failed to perform POST operation on ${args.url}: ${error.message}`,
}],
isError: true,
};
}
case "playwright_put":
try {
var data = {
data: args.value,
headers: {
'Content-Type': 'application/json'
}
};
var response = await apiContext.put(args.url, data);
return {
content: [{
type: "text",
text: `Performed PUT Operation ${args.url} with data ${JSON.stringify(args.value, null, 2)}`,
}, {
type: "text",
text: `Response: ${JSON.stringify(await response.json(), null, 2)}`,
},
{
type: "text",
text: `Response code ${response.status()}`
}],
isError: false,
};
}
catch (error) {
return {
content: [{
type: "text",
text: `Failed to perform PUT operation on ${args.url}: ${error.message}`,
}],
isError: true,
};
}
case "playwright_delete":
try {
var response = await apiContext.delete(args.url);
return {
content: [{
type: "text",
text: `Performed delete Operation ${args.url}`,
},
{
type: "text",
text: `Response code ${response.status()}`
}],
isError: false,
};
}
catch (error) {
return {
content: [{
type: "text",
text: `Failed to perform delete operation on ${args.url}: ${error.message}`,
}],
isError: true,
};
}
case "playwright_patch":
try {
var data = {
data: args.value,
headers: {
'Content-Type': 'application/json'
}
};
var response = await apiContext.patch(args.url, data);
return {
content: [{
type: "text",
text: `Performed PATCH Operation ${args.url} with data ${JSON.stringify(args.value, null, 2)}`,
}, {
type: "text",
text: `Response: ${JSON.stringify(await response.json(), null, 2)}`,
}, {
type: "text",
text: `Response code ${response.status()}`
}],
isError: false,
};
}
catch (error) {
return {
content: [{
type: "text",
text: `Failed to perform PATCH operation on ${args.url}: ${error.message}`,
}],
isError: true,
};
}
default:
return {
content: [{
type: "text",
text: `Unknown tool: ${name}`,
}],
isError: true,
};
}
}
// Expose utility functions for resource management
export function getConsoleLogs() {
return consoleLogs;
}
export function getScreenshots() {
return screenshots;
}