playwright-mcp
Version:
Playwright integration for ModelContext
282 lines (245 loc) • 7.21 kB
text/typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { chromium, BrowserContext, Browser, Page } from "playwright";
import { injectToolbox } from "./toolbox.js";
import { secureEvalAsync } from "./eval.js";
import { initState, getState, updateState, type Message } from "./state.js";
import { initRecording } from "./recording";
import { handleBrowserEvent } from "./handle-browser-event.js";
let browser: Browser;
let context: BrowserContext;
let page: Page;
const server = new McpServer({
name: "playwright",
version: "1.0.0",
});
server.prompt(
"server-flow",
"Get prompt on how to use this MCP server",
() => {
return {
messages: [
{
role: "user",
content: {
type: "text",
text: `# DON'T ASSUME ANYTHING. Whatever you write in code, it must be found in the context. Otherwise leave comments.
## Goal
Help me write playwright code with following functionalities:
- [[add semi-high level functionality you want here]]
- [[more]]
- [[more]]
- [[more]]
## Reference
- Use @x, @y files if you want to take reference on how I write POM code
## Steps
- First fetch the context from 'get-context' tool, until it returns no elements remaining
- Based on context and user functionality, write code in POM format, encapsulating high level functionality into reusable functions
- Try executing code using 'execute-code' tool. You could be on any page, so make sure to navigate to the correct page
- Write spec file using those reusable functions, covering multiple scenarios
`
}
}
]
};
}
);
server.tool(
'init-browser',
'Initialize a browser with a URL',
{
url: z.string().url().describe('The URL to navigate to')
},
async ({ url }) => {
if (context) {
await context.close();
}
if (browser) {
await browser.close();
}
browser = await chromium.launch({
headless: false,
});
context = await browser.newContext({
viewport: null,
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
bypassCSP: true,
});
page = await context.newPage();
await page.exposeFunction('triggerMcpStartPicking', (pickingType: 'DOM' | 'Image') => {
page.evaluate((pickingType: 'DOM' | 'Image') => {
window.mcpStartPicking(pickingType);
}, pickingType);
});
await page.exposeFunction('triggerMcpStopPicking', () => {
page.evaluate(() => {
window.mcpStopPicking();
});
});
await page.exposeFunction('onElementPicked', (message: Message) => {
const state = getState();
state.messages.push(message);
state.pickingType = null;
updateState(page, state);
});
await page.exposeFunction('takeScreenshot', async (selector: string) => {
try {
const screenshot = await page.locator(selector).screenshot({
timeout: 5000
});
return screenshot.toString('base64');
} catch (error) {
console.error('Error taking screenshot', error);
return null;
}
});
await page.exposeFunction('executeCode', async (code: string) => {
const result = await secureEvalAsync(page, code);
return result;
});
await initState(page);
await initRecording(page, handleBrowserEvent(page));
await page.addInitScript(injectToolbox);
await page.goto(url);
return {
content: [
{
type: "text",
text: `Browser has been initialized and navigated to ${url}`,
},
],
};
}
)
server.tool(
"get-full-dom",
"Get the full DOM of the current page. (Deprecated, use get-context instead)",
{},
async () => {
const html = await page.content();
return {
content: [
{
type: "text",
text: html,
},
],
};
}
);
server.tool(
'get-screenshot',
'Get a screenshot of the current page',
{},
async () => {
const screenshot = await page.screenshot({
type: "png",
});
return {
content: [
{
type: "image",
data: screenshot.toString('base64'),
mimeType: "image/png",
},
],
};
}
)
server.tool(
'execute-code',
'Execute custom Playwright JS code against the current page',
{
code: z.string().describe(`The Playwright code to execute. Must be an async function declaration that takes a page parameter.
Example:
async function run(page) {
console.log(await page.title());
return await page.title();
}
Returns an object with:
- result: The return value from your function
- logs: Array of console logs from execution
- errors: Array of any errors encountered
Example response:
{"result": "Google", "logs": ["[log] Google"], "errors": []}`)
},
async ({ code }) => {
const result = await secureEvalAsync(page, code);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2) // Pretty print the JSON
}
]
};
}
)
server.tool(
"get-context",
"Get the website context which would be used to write the testcase",
{},
async () => {
const state = getState();
if (state.messages.length === 0) {
return {
content: [
{
type: "text",
text: `No messages available`
}
]
};
}
const content: any = [];
let totalLength = 0;
let messagesProcessed = 0;
while (messagesProcessed < state.messages.length && totalLength < 20000) {
const message = state.messages[messagesProcessed];
let currentContent = message.content
if (message.type === 'DOM') {
currentContent = `DOM: ${message.content}`;
} else if (message.type === 'Text') {
currentContent = `Text: ${message.content}`;
} else if (message.type === 'Interaction') {
const interaction = JSON.parse(message.content);
delete interaction.eventId;
delete interaction.dom;
delete interaction.elementUUID;
if (interaction.selectors) {
interaction.selectors = interaction.selectors.slice(0, 10);
}
currentContent = JSON.stringify(interaction);
} else if (message.type === 'Image') {
currentContent = message.content;
}
totalLength += currentContent.length;
const item: any = {}
const isImage = message.type === 'Image';
if (isImage) {
item.type = "image";
item.data = message.content;
item.mimeType = "image/png";
} else {
item.type = "text";
item.text = currentContent;
}
content.push(item);
messagesProcessed++;
}
// Remove processed messages
state.messages.splice(0, messagesProcessed);
updateState(page, state);
const remainingCount = state.messages.length;
if (remainingCount > 0) {
content.push({
type: "text",
text: `Remaining ${remainingCount} messages, please fetch those in next requests.\n`
});
}
return {
content
};
}
);
export { server }