mac-apps-launcher
#!/usr/bin/env node
import yargs from "yargs/yargs";
import { hideBin } from 'yargs/helpers'
import os from "os";
import path from "path";
import { promises as fs } from "fs";
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,
} from "@modelcontextprotocol/sdk/types.js";
import playwright, { Browser, Page } from "playwright";
// Define the tools once to avoid repetition
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" },
fullPage: { type: "boolean", description: "Take a full page screenshot (default: false)", default: false },
},
required: ["name"],
},
},
{
name: "playwright_click",
description: "Click an element on the page using CSS selector",
inputSchema: {
type: "object",
properties: {
selector: { type: "string", description: "CSS selector for element to click" },
},
required: ["selector"],
},
},
{
name: "playwright_click_text",
description: "Click an element on the page by its text content",
inputSchema: {
type: "object",
properties: {
text: { type: "string", description: "Text content of the element to click" },
},
required: ["text"],
},
},
{
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 using CSS selector",
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_select_text",
description: "Select an element on the page with Select tag by its text content",
inputSchema: {
type: "object",
properties: {
text: { type: "string", description: "Text content of the element to select" },
value: { type: "string", description: "Value to select" },
},
required: ["text", "value"],
},
},
{
name: "playwright_hover",
description: "Hover an element on the page using CSS selector",
inputSchema: {
type: "object",
properties: {
selector: { type: "string", description: "CSS selector for element to hover" },
},
required: ["selector"],
},
},
{
name: "playwright_hover_text",
description: "Hover an element on the page by its text content",
inputSchema: {
type: "object",
properties: {
text: { type: "string", description: "Text content of the element to hover" },
},
required: ["text"],
},
},
{
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 playwright.firefox.launch({ headless: false });
}
if (!page) {
page = await browser.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: any): Promise<CallToolResult> {
const page = await ensureBrowser();
switch (name) {
case "playwright_navigate":
await page.goto(args.url);
return {
content: [{
type: "text",
text: `Navigated to ${args.url}`,
}],
isError: false,
};
case "playwright_screenshot": {
const fullPage = (args.fullPage === 'true');
const screenshot = await (args.selector ?
page.locator(args.selector).screenshot() :
page.screenshot({ fullPage }));
const base64Screenshot = screenshot.toString('base64');
if (!base64Screenshot) {
return {
content: [{
type: "text",
text: args.selector ? `Element not found: ${args.selector}` : "Screenshot failed",
}],
isError: true,
};
}
screenshots.set(args.name, base64Screenshot);
server.notification({
method: "notifications/resources/list_changed",
});
return {
content: [
{
type: "text",
text: `Screenshot '${args.name}' taken`,
} as TextContent,
{
type: "image",
data: base64Screenshot,
mimeType: "image/png",
} as ImageContent,
],
isError: false,
};
}
case "playwright_click":
try {
await page.locator(args.selector).click();
return {
content: [{
type: "text",
text: `Clicked: ${args.selector}`,
}],
isError: false,
};
} catch (error) {
if((error as Error).message.includes("strict mode violation")) {
console.log("Strict mode violation, retrying on first element...");
try {
await page.locator(args.selector).first().click();
return {
content: [{
type: "text",
text: `Clicked: ${args.selector}`,
}],
isError: false,
};
} catch (error) {
return {
content: [{
type: "text",
text: `Failed (twice) to click ${args.selector}: ${(error as Error).message}`,
}],
isError: true,
};
}
}
return {
content: [{
type: "text",
text: `Failed to click ${args.selector}: ${(error as Error).message}`,
}],
isError: true,
};
}
case "playwright_click_text":
try {
await page.getByText(args.text).click();
return {
content: [{
type: "text",
text: `Clicked element with text: ${args.text}`,
}],
isError: false,
};
} catch (error) {
if((error as Error).message.includes("strict mode violation")) {
console.log("Strict mode violation, retrying on first element...");
try {
await page.getByText(args.text).first().click();
return {
content: [{
type: "text",
text: `Clicked element with text: ${args.text}`,
}],
isError: false,
};
} catch (error) {
return {
content: [{
type: "text",
text: `Failed (twice) to click element with text ${args.text}: ${(error as Error).message}`,
}],
isError: true,
};
}
}
return {
content: [{
type: "text",
text: `Failed to click element with text ${args.text}: ${(error as Error).message}`,
}],
isError: true,
};
}
case "playwright_fill":
try {
await page.locator(args.selector).pressSequentially(args.value, { delay: 100 });
return {
content: [{
type: "text",
text: `Filled ${args.selector} with: ${args.value}`,
}],
isError: false,
};
} catch (error) {
if((error as Error).message.includes("strict mode violation")) {
console.log("Strict mode violation, retrying on first element...");
try {
await page.locator(args.selector).first().pressSequentially(args.value, { delay: 100 });
return {
content: [{
type: "text",
text: `Filled ${args.selector} with: ${args.value}`,
}],
isError: false,
};
} catch (error) {
return {
content: [{
type: "text",
text: `Failed (twice) to fill ${args.selector}: ${(error as Error).message}`,
}],
isError: true,
};
}
}
return {
content: [{
type: "text",
text: `Failed to fill ${args.selector}: ${(error as Error).message}`,
}],
isError: true,
};
}
case "playwright_select":
try {
await page.locator(args.selector).selectOption(args.value);
return {
content: [{
type: "text",
text: `Selected ${args.selector} with: ${args.value}`,
}],
isError: false,
};
} catch (error) {
if((error as Error).message.includes("strict mode violation")) {
console.log("Strict mode violation, retrying on first element...");
try {
await page.locator(args.selector).first().selectOption(args.value);
return {
content: [{
type: "text",
text: `Selected ${args.selector} with: ${args.value}`,
}],
isError: false,
};
} catch (error) {
return {
content: [{
type: "text",
text: `Failed (twice) to select ${args.selector}: ${(error as Error).message}`,
}],
isError: true,
};
}
}
return {
content: [{
type: "text",
text: `Failed to select ${args.selector}: ${(error as Error).message}`,
}],
isError: true,
};
}
case "playwright_select_text":
try {
await page.getByText(args.text).selectOption(args.value);
return {
content: [{
type: "text",
text: `Selected element with text ${args.text} with value: ${args.value}`,
}],
isError: false,
};
} catch (error) {
if((error as Error).message.includes("strict mode violation")) {
console.log("Strict mode violation, retrying on first element...");
try {
await page.getByText(args.text).first().selectOption(args.value);
return {
content: [{
type: "text",
text: `Selected element with text ${args.text} with value: ${args.value}`,
}],
isError: false,
};
} catch (error) {
return {
content: [{
type: "text",
text: `Failed (twice) to select element with text ${args.text}: ${(error as Error).message}`,
}],
isError: true,
};
}
}
return {
content: [{
type: "text",
text: `Failed to select element with text ${args.text}: ${(error as Error).message}`,
}],
isError: true,
};
}
case "playwright_hover":
try {
await page.locator(args.selector).hover();
return {
content: [{
type: "text",
text: `Hovered ${args.selector}`,
}],
isError: false,
};
} catch (error) {
if((error as Error).message.includes("strict mode violation")) {
console.log("Strict mode violation, retrying on first element...");
try {
await page.locator(args.selector).first().hover();
return {
content: [{
type: "text",
text: `Hovered ${args.selector}`,
}],
isError: false,
};
} catch (error) {
return {
content: [{
type: "text",
text: `Failed to hover ${args.selector}: ${(error as Error).message}`,
}],
isError: true,
};
}
}
return {
content: [{
type: "text",
text: `Failed to hover ${args.selector}: ${(error as Error).message}`,
}],
isError: true,
};
}
case "playwright_hover_text":
try {
await page.getByText(args.text).hover();
return {
content: [{
type: "text",
text: `Hovered element with text: ${args.text}`,
}],
isError: false,
};
} catch (error) {
if((error as Error).message.includes("strict mode violation")) {
console.log("Strict mode violation, retrying on first element...");
try {
await page.getByText(args.text).first().hover();
return {
content: [{
type: "text",
text: `Hovered element with text: ${args.text}`,
}],
isError: false,
};
} catch (error) {
return {
content: [{
type: "text",
text: `Failed (twice) to hover element with text ${args.text}: ${(error as Error).message}`,
}],
isError: true,
};
}
}
return {
content: [{
type: "text",
text: `Failed to hover element with text ${args.text}: ${(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 {
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 as Error).message}`,
}],
isError: true,
};
}
default:
return {
content: [{
type: "text",
text: `Unknown tool: ${name}`,
}],
isError: true,
};
}
}
const server = new Server(
{
name: "automatalabs/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) => {
const uri = request.params.uri.toString();
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}`);
});
async function runServer() {
const transport = new StdioServerTransport();
await server.connect(transport);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: TOOLS,
}));
server.setRequestHandler(CallToolRequestSchema, async (request) =>
handleToolCall(request.params.name, request.params.arguments ?? {})
);
}
async function checkPlatformAndInstall() {
const platform = os.platform();
if (platform === "win32") {
console.log("Installing MCP Playwright Server for Windows...");
try {
const configFilePath = path.join(os.homedir(), 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json');
let config: any;
try {
// Try to read existing config file
const fileContent = await fs.readFile(configFilePath, 'utf-8');
config = JSON.parse(fileContent);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
// Create new config file with mcpServers object
config = { mcpServers: {} };
await fs.writeFile(configFilePath, JSON.stringify(config, null, 2), 'utf-8');
console.log("Created new Claude config file");
} else {
console.error("Error reading Claude config file:", error);
process.exit(1);
}
}
// Ensure mcpServers exists
if (!config.mcpServers) {
config.mcpServers = {};
}
// Update the playwright configuration
config.mcpServers.playwright = {
command: "npx",
args: ["-y", "@automatalabs/mcp-server-playwright"]
};
// Write the updated config back to file
await fs.writeFile(configFilePath, JSON.stringify(config, null, 2), 'utf-8');
console.log("✓ Successfully updated Claude configuration");
} catch (error) {
console.error("Error during installation:", error);
process.exit(1);
}
} else if (platform === "darwin") {
console.log("Installing MCP Playwright Server for macOS...");
try {
const configFilePath = path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
let config: any;
try {
// Try to read existing config file
const fileContent = await fs.readFile(configFilePath, 'utf-8');
config = JSON.parse(fileContent);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
// Create new config file with mcpServers object
config = { mcpServers: {} };
await fs.writeFile(configFilePath, JSON.stringify(config, null, 2), 'utf-8');
console.log("Created new Claude config file");
} else {
console.error("Error reading Claude config file:", error);
process.exit(1);
}
}
// Ensure mcpServers exists
if (!config.mcpServers) {
config.mcpServers = {};
}
// Update the playwright configuration
config.mcpServers.playwright = {
command: "npx",
args: ["-y", "@automatalabs/mcp-server-playwright"]
};
// Write the updated config back to file
await fs.writeFile(configFilePath, JSON.stringify(config, null, 2), 'utf-8');
console.log("✓ Successfully updated Claude configuration");
} catch (error) {
console.error("Error during installation:", error);
process.exit(1);
}
} else {
console.error("Unsupported platform:", platform);
process.exit(1);
}
}
(async () => {
try {
// Parse args but continue with server if no command specified
await yargs(hideBin(process.argv))
.command('install', 'Install MCP-Server-Playwright dependencies', () => {}, async () => {
await checkPlatformAndInstall();
// Exit after successful installation
process.exit(0);
})
.strict()
.help()
.parse();
// If we get here, no command was specified, so run the server
await runServer().catch(console.error);
} catch (error) {
console.error('Error:', error);
process.exit(1);
}
})();