# Directory Structure
```
├── .gitignore
├── deno.json
├── deno.lock
├── LICENSE
├── main.ts
└── README.md
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
playwright
playwright-server
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# Deno 2 Playwright Model Context Protocol Server Example
A Model Context Protocol server that provides browser automation capabilities using Playwright. This server enables LLMs to interact with web pages, take screenshots, and execute JavaScript in a real browser environment.
This repo uses Deno 2, which has nice ergonomics, because you can compile a binary and run it without any runtime dependencies.
This code is heavily based on the official Puppeteer MCP server, which you can find here: https://github.com/modelcontextprotocol/servers/tree/main/src/puppeteer
## How to build
Only the mac binary build has been tested, but you should be able to build an executable binary for linux x86_64, linux ARM64, and windows x86_64.
- `deno task build-mac`
- `deno task build-linux-x86_64`
- `deno task build-linux-ARM64`
- `deno task build-windows-x86_64`
## How to run
To invoke the playwright-server binary, you need to update your `~/Library/Application\ Support/Claude/claude_desktop_config.json` to point to the binary.
```json
{
"mcpServers": {
"playwright": {
"command": "/path/to/deno2-playwright-mcp-server/playwright-server"
}
}
}
```
```
--------------------------------------------------------------------------------
/deno.json:
--------------------------------------------------------------------------------
```json
{
"tasks": {
"start": "deno run --watch --allow-net --allow-env --allow-run --allow-sys --allow-read --allow-write main.ts",
"build-mac": "deno compile --target aarch64-apple-darwin --allow-net --allow-env --allow-run --allow-sys --allow-read --allow-write --output playwright-server main.ts",
"build-linux-x86_64": "deno compile --target x86_64-unknown-linux-gnu --allow-net --allow-env --allow-run --allow-sys --allow-read --allow-write --output playwright-server main.ts",
"build-linux-ARM64": "deno compile --target aarch64-unknown-linux-gnu --allow-net --allow-env --allow-run --allow-sys --allow-read --allow-write --output playwright-server main.ts",
"build-windows-x86_64": "deno compile --target x86_64-pc-windows-msvc --allow-net --allow-env --allow-run --allow-sys --allow-read --allow-write --output playwright-server main.ts"
},
"imports": {
"@modelcontextprotocol/sdk": "npm:@modelcontextprotocol/sdk@^1.0.1",
"@std/encoding": "jsr:@std/encoding@^1.0.5",
"playwright": "npm:playwright@^1.49.0"
}
}
```
--------------------------------------------------------------------------------
/main.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env -S deno run --watch --allow-net --allow-env --allow-run --allow-sys --allow-read --allow-write
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListResourcesRequestSchema,
ListToolsRequestSchema,
ReadResourceRequestSchema,
CallToolResult,
TextContent,
ImageContent,
Tool,
ReadResourceRequest,
CallToolRequest,
} from "@modelcontextprotocol/sdk/types.js";
import { chromium, Browser, Page } from "npm:playwright";
import { encodeBase64 } from "@std/encoding";
// Define tools similar to Puppeteer implementation but using Playwright's API
const TOOLS: Tool[] = [
{
name: "playwright_navigate",
description: "Navigate to a URL",
inputSchema: {
type: "object",
properties: {
url: { type: "string" },
},
required: ["url"],
},
},
{
name: "playwright_screenshot",
description: "Take a screenshot of the current page or a specific element",
inputSchema: {
type: "object",
properties: {
name: { type: "string", description: "Name for the screenshot" },
selector: {
type: "string",
description: "CSS selector for element to screenshot",
},
width: {
type: "number",
description: "Width in pixels (default: 800)",
},
height: {
type: "number",
description: "Height in pixels (default: 600)",
},
},
required: ["name"],
},
},
{
name: "playwright_click",
description: "Click an element on the page",
inputSchema: {
type: "object",
properties: {
selector: {
type: "string",
description: "CSS selector for element to click",
},
},
required: ["selector"],
},
},
{
name: "playwright_fill",
description: "Fill out an input field",
inputSchema: {
type: "object",
properties: {
selector: {
type: "string",
description: "CSS selector for input field",
},
value: { type: "string", description: "Value to fill" },
},
required: ["selector", "value"],
},
},
{
name: "playwright_select",
description: "Select an element on the page with Select tag",
inputSchema: {
type: "object",
properties: {
selector: {
type: "string",
description: "CSS selector for element to select",
},
value: { type: "string", description: "Value to select" },
},
required: ["selector", "value"],
},
},
{
name: "playwright_hover",
description: "Hover an element on the page",
inputSchema: {
type: "object",
properties: {
selector: {
type: "string",
description: "CSS selector for element to hover",
},
},
required: ["selector"],
},
},
{
name: "playwright_evaluate",
description: "Execute JavaScript in the browser console",
inputSchema: {
type: "object",
properties: {
script: { type: "string", description: "JavaScript code to execute" },
},
required: ["script"],
},
},
];
// Global state
let browser: Browser | undefined;
let page: Page | undefined;
const consoleLogs: string[] = [];
const screenshots = new Map<string, string>();
async function ensureBrowser() {
if (!browser) {
browser = await chromium.launch({ headless: false });
const context = await browser.newContext();
page = await context.newPage();
page.on("console", (msg) => {
const logEntry = `[${msg.type()}] ${msg.text()}`;
consoleLogs.push(logEntry);
server.notification({
method: "notifications/resources/updated",
params: { uri: "console://logs" },
});
});
}
return page!;
}
async function handleToolCall(
name: string,
args: Record<string, unknown>
): Promise<{ toolResult: CallToolResult }> {
const page = await ensureBrowser();
switch (name) {
case "playwright_navigate":
await page.goto(args.url as string);
return {
toolResult: {
content: [
{
type: "text",
text: `Navigated to ${args.url}`,
},
],
isError: false,
},
};
case "playwright_screenshot": {
const width = (args.width as number) ?? 800;
const height = (args.height as number) ?? 600;
await page.setViewportSize({ width, height });
const screenshot = await (args.selector
? page.locator(args.selector as string).screenshot()
: page.screenshot());
const base64Screenshot = encodeBase64(screenshot);
screenshots.set(args.name as string, base64Screenshot);
server.notification({
method: "notifications/resources/list_changed",
});
return {
toolResult: {
content: [
{
type: "text",
text: `Screenshot '${args.name}' taken at ${width}x${height}`,
} as TextContent,
{
type: "image",
data: base64Screenshot,
mimeType: "image/png",
} as ImageContent,
],
isError: false,
},
};
}
case "playwright_click":
try {
await page.click(args.selector as string);
return {
toolResult: {
content: [
{
type: "text",
text: `Clicked: ${args.selector}`,
},
],
isError: false,
},
};
} catch (err) {
const error = err as Error;
return {
toolResult: {
content: [
{
type: "text",
text: `Failed to click ${args.selector}: ${error.message}`,
},
],
isError: true,
},
};
}
case "playwright_fill":
try {
await page.fill(args.selector as string, args.value as string);
return {
toolResult: {
content: [
{
type: "text",
text: `Filled ${args.selector} with: ${args.value}`,
},
],
isError: false,
},
};
} catch (err) {
const error = err as Error;
return {
toolResult: {
content: [
{
type: "text",
text: `Failed to fill ${args.selector}: ${error.message}`,
},
],
isError: true,
},
};
}
case "playwright_select":
try {
await page.selectOption(args.selector as string, args.value as string);
return {
toolResult: {
content: [
{
type: "text",
text: `Selected ${args.selector} with: ${args.value}`,
},
],
isError: false,
},
};
} catch (err) {
const error = err as Error;
return {
toolResult: {
content: [
{
type: "text",
text: `Failed to select ${args.selector}: ${error.message}`,
},
],
isError: true,
},
};
}
case "playwright_hover":
try {
await page.hover(args.selector as string);
return {
toolResult: {
content: [
{
type: "text",
text: `Hovered ${args.selector}`,
},
],
isError: false,
},
};
} catch (err) {
const error = err as Error;
return {
toolResult: {
content: [
{
type: "text",
text: `Failed to hover ${args.selector}: ${error.message}`,
},
],
isError: true,
},
};
}
case "playwright_evaluate":
try {
const result = await page.evaluate((script: string) => {
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 as string);
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 (err) {
const error = err as Error;
return {
toolResult: {
content: [
{
type: "text",
text: `Script execution failed: ${error.message}`,
},
],
isError: true,
},
};
}
default:
return {
toolResult: {
content: [
{
type: "text",
text: `Unknown tool: ${name}`,
},
],
isError: true,
},
};
}
}
const server = new Server(
{
name: "example-servers/playwright",
version: "0.1.0",
},
{
capabilities: {
resources: {},
tools: {},
},
}
);
// Setup request handlers
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: [
{
uri: "console://logs",
mimeType: "text/plain",
name: "Browser console logs",
},
...Array.from(screenshots.keys()).map((name) => ({
uri: `screenshot://${name}`,
mimeType: "image/png",
name: `Screenshot: ${name}`,
})),
],
}));
server.setRequestHandler(
ReadResourceRequestSchema,
async (request: ReadResourceRequest) => {
const uri = request.params.uri;
if (uri === "console://logs") {
return {
contents: [
{
uri,
mimeType: "text/plain",
text: consoleLogs.join("\n"),
},
],
};
}
if (uri.startsWith("screenshot://")) {
const name = uri.split("://")[1];
const screenshot = screenshots.get(name);
if (screenshot) {
return {
contents: [
{
uri,
mimeType: "image/png",
blob: screenshot,
},
],
};
}
}
throw new Error(`Resource not found: ${uri}`);
}
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: TOOLS,
}));
server.setRequestHandler(CallToolRequestSchema, (request: CallToolRequest) =>
handleToolCall(request.params.name, request.params.arguments ?? {})
);
// Handle cleanup on exit
Deno.addSignalListener("SIGINT", async () => {
if (browser) {
await browser.close();
}
Deno.exit(0);
});
// Run the server
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Playwright MCP server running on stdio");
```