Skip to main content
Glama

Playwright Server MCP

by ziux
action.py21.2 kB
import asyncio import uuid import base64 import os import json import logging from typing import Any, Optional, Dict, List, Union import mcp.types as types from playwright.async_api import async_playwright, TimeoutError as PlaywrightTimeoutError from playwright_server.tools.base import ToolHandler, Property # 获取日志记录器 logger = logging.getLogger('playwright_server.tools.action') class ActionToolHandler(ToolHandler): name = "playwright_action" description = "根据给定的locator 和action 执行操作" inputSchema = [ Property(name="by", typ="string", description="选择元素方式,遵循playwright的选择器原则,可选项: get_by_text,get_by_placeholder,get_by_label,get_by_role,get_by_alt_text,get_by_title,get_by_test_id,get_by_css,locator", required=True), Property(name="by_value", typ="string", description="选择元素的值", required=True), Property(name="action", typ="string", description="操作类型,可选项: fill,click,dblclick,hover,tap,check,uncheck,select_option,press,type,focus,blur,drag_to,screenshot,get_text,count,is_visible,is_enabled,is_hidden,is_disabled,is_checked,get_attribute,evaluate,wait_for,press_sequentially,clear,scroll_into_view,set_input_files,select_text,dispatch_event,get_inner_text,get_inner_html,get_content,all_inner_texts,all_text_contents", required=True), Property(name="value", typ="string", description="操作的值", required=False), Property(name="force", typ="boolean", description="是否强制执行动作,无视actionability检查", required=False), Property(name="delay", typ="number", description="按键之间的延迟(毫秒)", required=False), Property(name="position", typ="string", description="相对于元素的点击位置,格式为: 'x,y'", required=False), Property(name="timeout", typ="number", description="操作超时时间(毫秒)", required=False), Property(name="selector", typ="string", description="拖拽目标元素的选择器,用于drag_to操作", required=False), Property(name="attribute_name", typ="string", description="要获取的属性名称,用于get_attribute操作", required=False), Property(name="script", typ="string", description="要在元素上执行的JavaScript代码,用于evaluate操作", required=False), Property(name="filename", typ="string", description="截图保存的文件名,用于screenshot操作", required=False), Property(name="no_wait_after", typ="boolean", description="是否不等待动作完成后的导航", required=False), Property(name="strict", typ="boolean", description="是否严格匹配元素(如果找到多个元素是否报错)", required=False), Property(name="state", typ="string", description="等待的元素状态,用于wait_for操作,可选: 'attached', 'detached', 'visible', 'hidden'", required=False), Property(name="modifiers", typ="array", description="修饰键,用于键盘操作,可选: 'Alt', 'Control', 'Meta', 'Shift'", required=False), Property(name="button", typ="string", description="鼠标按钮,用于点击操作,可选: 'left', 'right', 'middle'", required=False), Property(name="options", typ="string", description="选择框的选项,用于select_option操作,JSON格式的字符串,可以包含 value, label, index", required=False), Property(name="files", typ="string", description="要上传的文件路径,用于set_input_files操作,多个文件用逗号分隔", required=False), Property(name="event_name", typ="string", description="要分发的事件名称,用于dispatch_event操作", required=False), Property(name="event_data", typ="string", description="事件数据,JSON格式的字符串,用于dispatch_event操作", required=False), Property(name="exact", typ="boolean", description="是否精确匹配文本", required=False), Property(name="has_text", typ="string", description="元素必须包含的文本", required=False) ] async def handle(self, name: str, arguments: dict | None) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]: logger.info(f"开始执行操作: by={arguments.get('by')}, by_value={arguments.get('by_value')}, action={arguments.get('action')}") try: page = self.get_page() logger.debug(f"当前页面URL: {page.url}") by = arguments.get("by") by_value = arguments.get("by_value") action = arguments.get("action") value = arguments.get("value") force = arguments.get("force", False) delay = arguments.get("delay") position = arguments.get("position") timeout = arguments.get("timeout") selector = arguments.get("selector") attribute_name = arguments.get("attribute_name") script = arguments.get("script") filename = arguments.get("filename", "screenshot.png") no_wait_after = arguments.get("no_wait_after", False) strict = arguments.get("strict", False) state = arguments.get("state") modifiers = arguments.get("modifiers", []) button = arguments.get("button", "left") options_str = arguments.get("options") files_str = arguments.get("files") event_name = arguments.get("event_name") event_data_str = arguments.get("event_data") exact = arguments.get("exact", False) has_text = arguments.get("has_text") logger.debug(f"操作参数: force={force}, delay={delay}, timeout={timeout}, strict={strict}, exact={exact}") # 解析复杂参数 options = {} if options_str: try: logger.debug(f"解析options字符串: {options_str}") options = json.loads(options_str) except json.JSONDecodeError as e: logger.error(f"options参数格式错误: {str(e)}") return [types.TextContent(type="text", text="options参数格式错误,必须是有效的JSON字符串")] files = [] if files_str: logger.debug(f"解析files字符串: {files_str}") files = [f.strip() for f in files_str.split(",")] event_data = {} if event_data_str: try: logger.debug(f"解析event_data字符串: {event_data_str}") event_data = json.loads(event_data_str) except json.JSONDecodeError as e: logger.error(f"event_data参数格式错误: {str(e)}") return [types.TextContent(type="text", text="event_data参数格式错误,必须是有效的JSON字符串")] # 构建选项字典 action_options: Dict[str, Any] = {} if force is not None: action_options["force"] = force if delay is not None: action_options["delay"] = delay if timeout is not None: action_options["timeout"] = timeout if no_wait_after is not None: action_options["no_wait_after"] = no_wait_after if strict is not None: action_options["strict"] = strict if modifiers: action_options["modifiers"] = modifiers if button: action_options["button"] = button if exact is not None: action_options["exact"] = exact logger.debug(f"构建的action_options: {action_options}") # 处理位置参数 if position: try: logger.debug(f"解析位置参数: {position}") x, y = map(float, position.split(',')) action_options["position"] = {"x": x, "y": y} except ValueError as e: logger.error(f"位置格式错误: {str(e)}") return [types.TextContent(type="text", text="位置格式错误,应为'x,y'")] # 根据by参数获取locator locator = None locator_options = {"exact": exact} if exact is not None else {} try: logger.info(f"开始定位元素: by={by}, by_value={by_value}") if by == "get_by_text": locator = page.get_by_text(by_value, **locator_options) elif by == "get_by_placeholder": locator = page.get_by_placeholder(by_value, **locator_options) elif by == "get_by_label": locator = page.get_by_label(by_value, **locator_options) elif by == "get_by_role": locator = page.get_by_role(by_value, name=has_text, **locator_options) elif by == "get_by_alt_text": locator = page.get_by_alt_text(by_value, **locator_options) elif by == "get_by_title": locator = page.get_by_title(by_value, **locator_options) elif by == "get_by_test_id": locator = page.get_by_test_id(by_value) elif by == "get_by_css": locator = page.locator(by_value, has_text=has_text) elif by == "locator": locator = page.locator(by_value, has_text=has_text) else: logger.error(f"不支持的by类型: {by}") return [types.TextContent(type="text", text=f"不支持的by类型: {by}")] logger.debug("元素定位成功") except Exception as e: logger.error(f"定位元素失败: {str(e)}", exc_info=True) return [types.TextContent(type="text", text=f"定位元素失败: {str(e)}")] # 根据action执行不同操作 result = None try: logger.info(f"开始执行操作: action={action}") if action == "fill": logger.debug(f"填充文本: {value}") await locator.fill(value, **action_options) result = "填充成功" elif action == "click": logger.debug("点击元素") await locator.click(**action_options) result = "点击成功" elif action == "dblclick": logger.debug("双击元素") await locator.dblclick(**action_options) result = "双击成功" elif action == "hover": logger.debug("悬停在元素上") await locator.hover(**action_options) result = "悬停成功" elif action == "tap": logger.debug("轻触元素") await locator.tap(**action_options) result = "轻触成功" elif action == "check": logger.debug("选中复选框") await locator.check(**action_options) result = "选中成功" elif action == "uncheck": logger.debug("取消选中复选框") await locator.uncheck(**action_options) result = "取消选中成功" elif action == "select_option": if options: logger.debug(f"选择选项(使用options): {options}") await locator.select_option(**options, **action_options) else: logger.debug(f"选择选项(使用value): {value}") await locator.select_option(value=value, **action_options) result = "选择选项成功" elif action == "press": logger.debug(f"按下按键: {value}") await locator.press(value, **action_options) result = "按键成功" elif action == "type": logger.debug(f"输入文本: {value}") await locator.type(value, **action_options) result = "输入文本成功" elif action == "focus": logger.debug("聚焦元素") await locator.focus(**action_options) result = "聚焦成功" elif action == "blur": logger.debug("失焦元素") await locator.blur(**action_options) result = "失焦成功" elif action == "drag_to": logger.debug(f"拖拽元素到: {selector}") target = page.locator(selector) await locator.drag_to(target, **action_options) result = "拖拽成功" elif action == "screenshot": logger.debug(f"元素截图,保存到: {filename}") screenshot_path = f"{filename}" await locator.screenshot(path=screenshot_path, **action_options) with open(screenshot_path, "rb") as image_file: encoded_string = base64.b64encode(image_file.read()).decode("utf-8") os.remove(screenshot_path) logger.info(f"截图完成,文件大小: {len(encoded_string)} 字节") return [types.ImageContent(type="image", data=encoded_string, mimeType="image/png")] elif action == "get_text": logger.debug("获取元素文本") text = await locator.text_content(**action_options) logger.debug(f"获取到的文本: {text}") result = text elif action == "count": logger.debug("计算匹配元素数量") count = await locator.count() logger.debug(f"匹配的元素数量: {count}") result = str(count) elif action == "is_visible": logger.debug("检查元素是否可见") is_visible = await locator.is_visible(**action_options) logger.debug(f"元素可见性: {is_visible}") result = str(is_visible) elif action == "is_enabled": logger.debug("检查元素是否启用") is_enabled = await locator.is_enabled(**action_options) logger.debug(f"元素启用状态: {is_enabled}") result = str(is_enabled) elif action == "is_hidden": logger.debug("检查元素是否隐藏") is_hidden = await locator.is_hidden(**action_options) logger.debug(f"元素隐藏状态: {is_hidden}") result = str(is_hidden) elif action == "is_disabled": logger.debug("检查元素是否禁用") is_disabled = await locator.is_disabled(**action_options) logger.debug(f"元素禁用状态: {is_disabled}") result = str(is_disabled) elif action == "is_checked": logger.debug("检查元素是否选中") is_checked = await locator.is_checked(**action_options) logger.debug(f"元素选中状态: {is_checked}") result = str(is_checked) elif action == "get_attribute": logger.debug(f"获取元素属性: {attribute_name}") attribute = await locator.get_attribute(attribute_name, **action_options) logger.debug(f"属性值: {attribute}") result = attribute elif action == "evaluate": logger.debug(f"在元素上执行脚本: {script}") evaluated = await locator.evaluate(script, **action_options) logger.debug(f"脚本执行结果: {evaluated}") result = str(evaluated) elif action == "wait_for": if not state: state = "visible" logger.debug(f"等待元素状态: {state}") await locator.wait_for(state=state, **action_options) result = f"等待元素状态 '{state}' 成功" elif action == "press_sequentially": logger.debug(f"序列按键输入: {value}") await locator.press_sequentially(value, **action_options) result = "序列按键输入成功" elif action == "clear": logger.debug("清除元素内容") await locator.clear(**action_options) result = "清除内容成功" elif action == "scroll_into_view": logger.debug("将元素滚动到视图") await locator.scroll_into_view_if_needed(**action_options) result = "滚动到元素成功" elif action == "set_input_files": if files: logger.debug(f"设置文件输入: {files}") await locator.set_input_files(files, **action_options) result = "设置文件输入成功" else: logger.error("没有指定要上传的文件") return [types.TextContent(type="text", text="没有指定要上传的文件")] elif action == "select_text": logger.debug("选择元素中的文本") await locator.select_text(**action_options) result = "选择文本成功" elif action == "dispatch_event": if not event_name: logger.error("没有指定事件名称") return [types.TextContent(type="text", text="没有指定事件名称")] logger.debug(f"分发事件: {event_name}, 数据: {event_data}") await locator.dispatch_event(event_name, event_data, **action_options) result = f"分发事件 '{event_name}' 成功" elif action == "get_inner_text": logger.debug("获取元素内部文本") inner_text = await locator.inner_text(**action_options) logger.debug(f"内部文本: {inner_text}") result = inner_text elif action == "get_inner_html": logger.debug("获取元素内部HTML") inner_html = await locator.inner_html(**action_options) logger.debug(f"内部HTML长度: {len(inner_html)}") result = inner_html elif action == "get_content": logger.debug("获取元素内容") content = await locator.text_content(**action_options) logger.debug(f"内容: {content}") result = content elif action == "all_inner_texts": logger.debug("获取所有内部文本") all_inner_texts = await locator.all_inner_texts() logger.debug(f"内部文本数量: {len(all_inner_texts)}") result = str(all_inner_texts) elif action == "all_text_contents": logger.debug("获取所有文本内容") all_text_contents = await locator.all_text_contents() logger.debug(f"文本内容数量: {len(all_text_contents)}") result = str(all_text_contents) else: logger.error(f"不支持的action类型: {action}") return [types.TextContent(type="text", text=f"不支持的action类型: {action}")] logger.info(f"操作执行成功: {action}") return [types.TextContent(type="text", text=f"{result}")] except PlaywrightTimeoutError as e: logger.error(f"操作超时: {str(e)}", exc_info=True) return [types.TextContent(type="text", text=f"操作超时: {str(e)}")] except Exception as e: logger.error(f"操作执行失败: {str(e)}", exc_info=True) return [types.TextContent(type="text", text=f"操作执行失败: {str(e)}")] except Exception as e: logger.error(f"操作处理器全局异常: {str(e)}", exc_info=True) return [types.TextContent(type="text", text=f"操作处理器发生错误: {str(e)}")]

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ziux/playwright-plus-python-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server