Playwright MCP Tools

278 阅读3分钟

由于playwright mcp 缺少get_by_role(),get_by_text()等用法,于是让Cursor 生成了一个playwright mcp tools,代码如下:

from mcp.server.fastmcp import FastMCP
from playwright.async_api import async_playwright, TimeoutError, Error
from typing import Optional, Dict
import asyncio

class PlaywrightController:
    def __init__(self):
        self.playwright = None
        self.browser = None
        self.context = None
        self.page = None

    async def _initialize(self):
        """Initialize playwright instance and browser"""
        if self.playwright is None:
            self.playwright = await async_playwright().start()
            self.browser = await self.playwright.chromium.launch(headless=False)
            self.context = await self.browser.new_context()
            self.page = await self.context.new_page()

    async def navigate(self, url: str, wait_until: Optional[str] = None, timeout: Optional[int] = None):
        """Navigate to a URL"""
        await self._initialize()
        try:
            await self.page.goto(url, wait_until=wait_until, timeout=timeout)
            return {"status": "success", "url": url}
        except TimeoutError:
            return {"status": "error", "message": f"Navigation timeout: {url}"}
        except Exception as e:
            return {"status": "error", "message": str(e)}

    async def click(self, selector: str, index: int = 0, strict: bool = False):
        """Click an element"""
        await self._initialize()
        try:
            await self.page.locator(selector).nth(index).click()
            return {"status": "success", "action": "click", "selector": selector}
        except Error as e:
            if "strict mode violation" in str(e):
                try:
                    # Try with exact text match
                    await self.page.get_by_text(selector, exact=True).click()
                    return {"status": "success", "action": "click", "selector": f"text={selector}"}
                except:
                    pass
                try:
                    # Try with role
                    await self.page.get_by_role("button", name=selector).nth(index).click()
                    return {"status": "success", "action": "click", "selector": f"role=button,name={selector}","idnex":index}
                except:
                    pass
            return {"status": "error", "message": str(e)}

    async def fill(self, selector: str, index: int, value: str, strict: bool = False):
        """Fill a form field"""
        await self._initialize()
        try:
            await self.page.locator(selector).nth(index).fill(value)
            return {"status": "success", "action": "fill", "selector": selector, "index":index,"value": value}
        except Error as e:
            if "strict mode violation" in str(e):
                try:
                    # Try with placeholder
                    await self.page.get_by_placeholder(selector).nth(index).fill(value)
                    return {"status": "success", "action": "fill", "selector": f"placeholder={selector}","index":index}
                except:
                    pass
                try:
                    # Try with label
                    await self.page.get_by_label(selector).nth(index).fill(value)
                    return {"status": "success", "action": "fill", "selector": f"label={selector}","index":index}
                except:
                    pass
            return {"status": "error", "message": str(e)}

    async def click_by_text(self, text: str, index: int = 0, exact: bool = False):
        """Click element by text content"""
        await self._initialize()
        try:
            await self.page.get_by_text(text, exact=exact).nth(index).click()
            return {"status": "success", "action": "click_by_text", "text": text,"index":index}
        except Exception as e:
            return {"status": "error", "message": str(e)}

    async def click_by_role(self, role: str, name: str, exact: bool = False, index: int = 0):
        """Click element by role and name"""
        await self._initialize()
        try:
            await self.page.get_by_role(role, name=name, exact=exact).nth(index).click()
            return {"status": "success", "action": "click_by_role", "role": role, "name": name, "index": index}
        except Exception as e:
            return {"status": "error", "message": str(e)}

    async def wait_for_selector(self, selector: str, timeout: Optional[float] = None, strict: bool = False):
        """Wait for an element to be present"""
        await self._initialize()
        try:
            await self.page.wait_for_selector(selector, timeout=timeout)
            return {"status": "success", "action": "wait_for_selector", "selector": selector}
        except Exception as e:
            return {"status": "error", "message": str(e)}

    async def close(self):
        """Close all resources"""
        if self.page:
            await self.page.close()
        if self.context:
            await self.context.close()
        if self.browser:
            await self.browser.close()
        if self.playwright:
            await self.playwright.stop()

# Global controller instance
controller = PlaywrightController()

mcp = FastMCP("Playwright Server")

@mcp.tool()
async def playwright_navigate(url: str, wait_until: Optional[str] = None, timeout: Optional[int] = None) -> Dict:
    """Navigate to a URL
    
    Args:
        url: The URL to navigate to
        wait_until: Optional navigation wait condition ('load'|'domcontentloaded'|'networkidle'|'commit')
        timeout: Maximum navigation time in milliseconds
    """
    return await controller.navigate(url, wait_until=wait_until, timeout=timeout)

@mcp.tool()
async def playwright_click(selector: str, strict: bool = False) -> Dict:
    """Click an element by selector
    
    Args:
        selector: CSS or XPath selector
        strict: Whether to enable strict mode (exact match)
    """
    return await controller.click(selector, strict=strict)

@mcp.tool()
async def playwright_fill(selector: str, index: int,value: str, strict: bool = False) -> Dict:
    """Fill a form field
    
    Args:
        selector: CSS or XPath selector
        value: Text to fill
        strict: Whether to enable strict mode (exact match)
    """
    return await controller.fill(selector, index, value,strict=strict)

@mcp.tool()
async def playwright_click_by_text(text: str, exact: bool = False) -> Dict:
    """Click element by text content
    
    Args:
        text: Text content to match
        exact: Whether to require exact text match
    """
    return await controller.click_by_text(text, exact=exact)

@mcp.tool()
async def playwright_click_by_role(role: str, name: str, exact: bool = False, index: int = 0) -> Dict:
    """Click element by role and name
    
    Args:
        role: ARIA role
        name: Element name or label
        exact: Whether to require exact name match
        index: Index if multiple elements match
    """
    return await controller.click_by_role(role, name, exact=exact, index=index)

@mcp.tool()
async def playwright_wait_for_selector(selector: str, timeout: Optional[float] = None, strict: bool = False) -> Dict:
    """Wait for an element to be present
    
    Args:
        selector: CSS or XPath selector
        timeout: Maximum time to wait in milliseconds
        strict: Whether to enable strict mode (exact match)
    """
    return await controller.wait_for_selector(selector, timeout=timeout, strict=strict)

@mcp.tool()
async def playwright_close() -> Dict:
    """Close all resources"""
    await controller.close()
    return {"status": "success", "action": "close"}

if __name__ == "__main__":
    try:
        mcp.run()
    except KeyboardInterrupt:
        asyncio.run(controller.close())
    finally:
        asyncio.run(controller.close())
  1. 目录结构如下:

image.png

2. 调试mcp tools执行命令: mcp dev playwright_server.py, 然后在UI界面中单步调试;

  1. tools 页面如下:

image.png 4. tools 调试成功后就可以在Claude Desktop 中进行配置了; claude-desktop.json内容如下:

{ "mcpServers": {
       "my_playwright": {
            "command": "uv", 
            "args": [ 
            "--directory", 
            "/Users/xxx/MCPUse/my-playwright-mcp",              
            "run", 
            "playwright_server.py" 
            ] }
    } 
 }

5. 在Claude desktop 配置完成后重启Claude Desktop;

若无法执行脚本,在macos下需要先安装uv, brew install uv;