Playwright MCP Server
by lebrodus
- src
import { chromium, Browser, Page, request, APIRequest, APIRequestContext } from "playwright";
import { CallToolResult, TextContent, ImageContent } from "@modelcontextprotocol/sdk/types.js";
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: Browser | undefined;
let page: Page | undefined;
const consoleLogs: string[] = [];
const screenshots = new Map<string, string>();
const defaultDownloadsPath = path.join(os.homedir(), 'Downloads');
async function ensureBrowser() {
if (!browser) {
browser = await chromium.launch({ headless: false });
const context = await browser.newContext({
viewport: { width: 1920, height: 1080 },
deviceScaleFactor: 1,
});
page = await context.newPage();
page.on("console", (msg) => {
const logEntry = `[${msg.type()}] ${msg.text()}`;
consoleLogs.push(logEntry);
// Note: server.notification is assumed to be passed in from the main server
});
}
return page!;
}
async function ensureApiContext(url: string) {
return await request.newContext({
baseURL: url,
});
}
export async function handleToolCall(
name: string,
args: any,
server: any
): Promise<{ toolResult: CallToolResult }> {
// 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: Page | undefined;
let apiContext: APIRequestContext;
// Only launch browser if the tool requires browser interaction
if (requiresBrowser) {
page = await ensureBrowser();
}
// 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 {
toolResult: {
content: [{
type: "text",
text: `Navigated to ${args.url} with ${args.waitUntil || "load"} wait`,
}],
isError: false,
},
};
} catch (error) {
return {
toolResult: {
content: [{
type: "text",
text: `Navigation failed: ${(error as Error).message}`,
}],
isError: true,
},
};
}
case "playwright_screenshot": {
try {
const screenshotOptions: any = {
type: args.type || "png",
fullPage: !!args.fullPage
};
if (args.selector) {
const element = await page!.$(args.selector);
if (!element) {
return {
toolResult: {
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: string) => await page!.$(selector))
);
}
const screenshot = await page!.screenshot(screenshotOptions);
const base64Screenshot = screenshot.toString('base64');
const responseContent: (TextContent | ImageContent)[] = [];
// 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}`,
} as TextContent);
}
// 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",
} as ImageContent);
}
return {
toolResult: {
content: responseContent,
isError: false,
},
};
} catch (error) {
return {
toolResult: {
content: [{
type: "text",
text: `Screenshot failed: ${(error as Error).message}`,
}],
isError: true,
},
};
}
}
case "playwright_click":
try {
await page!.click(args.selector);
return {
toolResult: {
content: [{
type: "text",
text: `Clicked: ${args.selector}`,
}],
isError: false,
},
};
} catch (error) {
return {
toolResult: {
content: [{
type: "text",
text: `Failed to click ${args.selector}: ${(error as Error).message}`,
}],
isError: true,
},
};
}
case "playwright_fill":
try {
await page!.waitForSelector(args.selector);
await page!.fill(args.selector, args.value);
return {
toolResult: {
content: [{
type: "text",
text: `Filled ${args.selector} with: ${args.value}`,
}],
isError: false,
},
};
} catch (error) {
return {
toolResult: {
content: [{
type: "text",
text: `Failed to type ${args.selector}: ${(error as Error).message}`,
}],
isError: true,
},
};
}
case "playwright_select":
try {
await page!.waitForSelector(args.selector);
await page!.selectOption(args.selector, args.value);
return {
toolResult: {
content: [{
type: "text",
text: `Selected ${args.selector} with: ${args.value}`,
}],
isError: false,
},
};
} catch (error) {
return {
toolResult: {
content: [{
type: "text",
text: `Failed to select ${args.selector}: ${(error as Error).message}`,
}],
isError: true,
},
};
}
case "playwright_hover":
try {
await page!.waitForSelector(args.selector);
await page!.hover(args.selector);
return {
toolResult: {
content: [{
type: "text",
text: `Hovered ${args.selector}`,
}],
isError: false,
},
};
} catch (error) {
return {
toolResult: {
content: [{
type: "text",
text: `Failed to hover ${args.selector}: ${(error as Error).message}`,
}],
isError: true,
},
};
}
case "playwright_evaluate":
try {
const result = await page!.evaluate((script) => {
const logs: string[] = [];
const originalConsole = { ...console };
['log', 'info', 'warn', 'error'].forEach(method => {
(console as any)[method] = (...args: any[]) => {
logs.push(`[${method}] ${args.join(' ')}`);
(originalConsole as any)[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 {
toolResult: {
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 {
toolResult: {
content: [{
type: "text",
text: `Script execution failed: ${(error as Error).message}`,
}],
isError: true,
},
};
}
case "playwright_get":
try {
var response = await apiContext!.get(args.url);
return {
toolResult: {
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 {
toolResult: {
content: [{
type: "text",
text: `Failed to perform GET operation on ${args.url}: ${(error as 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 {
toolResult: {
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 {
toolResult: {
content: [{
type: "text",
text: `Failed to perform POST operation on ${args.url}: ${(error as 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 {
toolResult: {
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 {
toolResult: {
content: [{
type: "text",
text: `Failed to perform PUT operation on ${args.url}: ${(error as Error).message}`,
}],
isError: true,
},
};
}
case "playwright_delete":
try {
var response = await apiContext!.delete(args.url);
return {
toolResult: {
content: [{
type: "text",
text: `Performed delete Operation ${args.url}`,
},
{
type: "text",
text: `Response code ${response.status()}`
}],
isError: false,
},
};
} catch (error) {
return {
toolResult: {
content: [{
type: "text",
text: `Failed to perform delete operation on ${args.url}: ${(error as 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 {
toolResult: {
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 {
toolResult: {
content: [{
type: "text",
text: `Failed to perform PATCH operation on ${args.url}: ${(error as Error).message}`,
}],
isError: true,
},
};
}
default:
return {
toolResult: {
content: [{
type: "text",
text: `Unknown tool: ${name}`,
}],
isError: true,
},
};
}
}
// Expose utility functions for resource management
export function getConsoleLogs(): string[] {
return consoleLogs;
}
export function getScreenshots(): Map<string, string> {
return screenshots;
}