Web 智能体工具 Playwright MCP
Playwright MCP
一个模型上下文协议 (MCP) 服务器,使用 Playwright 提供浏览器自动化功能。该服务器使 LLM 能够通过结构化的可访问性快照与网页进行交互,无需使用屏幕截图或视觉调整模型。
\
- 快速轻量。使用 Playwright 的无障碍树,而非基于像素的输入。
- LLM 友好。无需视觉模型,纯粹基于结构化数据运行。
- 确定性工具应用程序。避免了基于屏幕截图的方法中常见的歧义。
playwright mcp 的底层 playwright
Playwright 为现代网络应用程序提供可靠的端到端测试。
\
- 任何浏览器 • 任何平台
- 弹性 • 没有不稳定的测试
- 完全隔离 • 快速执行
- 强大的工具生态

Playwright MCP 命令行
# npx -y @playwright/mcp@latest --help
Usage: Playwright MCP [options]
Options:
-V, --version output the version number
--allowed-hosts <hosts...> comma-separated list of hosts this server is allowed to serve from.
Defaults to the host the server is bound to.
--allowed-origins <origins> semicolon-separated list of origins to allow the browser to request.
Default is to allow all.
--blocked-origins <origins> semicolon-separated list of origins to block the browser from requesting.
Blocklist is evaluated before allowlist. If used without the allowlist,
requests not matching the blocklist are still allowed.
--block-service-workers block service workers
--browser <browser> browser or chrome channel to use, possible values: chrome, firefox, webkit,
msedge.
--caps <caps> comma-separated list of additional capabilities to enable, possible values:
vision, pdf.
--cdp-endpoint <endpoint> CDP endpoint to connect to.
--cdp-header <headers...> CDP headers to send with the connect request, multiple can be specified.
--config <path> path to the configuration file.
--device <device> device to emulate, for example: "iPhone 15"
--executable-path <path> path to the browser executable.
--extension Connect to a running browser instance (Edge/Chrome only). Requires the
"Playwright MCP Bridge" browser extension to be installed.
--grant-permissions <permissions...> List of permissions to grant to the browser context, for example
"geolocation", "clipboard-read", "clipboard-write".
--headless run browser in headless mode, headed by default
--host <host> host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all
interfaces.
--ignore-https-errors ignore https errors
--init-script <path...> path to JavaScript file to add as an initialization script. The script will
be evaluated in every page before any of the page's scripts. Can be
specified multiple times.
--isolated keep the browser profile in memory, do not save it to disk.
--image-responses <mode> whether to send image responses to the client. Can be "allow" or "omit",
Defaults to "allow".
--no-sandbox disable the sandbox for all process types that are normally sandboxed.
--output-dir <path> path to the directory for output files.
--port <port> port to listen on for SSE transport.
--proxy-bypass <bypass> comma-separated domains to bypass proxy, for example
".com,chromium.org,.domain.com"
--proxy-server <proxy> specify proxy server, for example "http://myproxy:3128" or
"socks5://myproxy:8080"
--save-session Whether to save the Playwright MCP session into the output directory.
--save-trace Whether to save the Playwright Trace of the session into the output
directory.
--save-video <size> Whether to save the video of the session into the output directory. For
example "--save-video=800x600"
--secrets <path> path to a file containing secrets in the dotenv format
--shared-browser-context reuse the same browser context between all connected HTTP clients.
--storage-state <path> path to the storage state file for isolated sessions.
--timeout-action <timeout> specify action timeout in milliseconds, defaults to 5000ms
--timeout-navigation <timeout> specify navigation timeout in milliseconds, defaults to 60000ms
--user-agent <ua string> specify user agent string
--user-data-dir <path> path to the user data directory. If not specified, a temporary directory
will be created.
--viewport-size <size> specify browser viewport size in pixels, for example "1280x720"
-h, --help display help for command
启动
# stdio
$ npx -y @playwright/mcp@latest
# mcp sse
$ npx -y @playwright/mcp@latest --port 8931
Listening on http://localhost:8931
Put this in your client config:
{
"mcpServers": {
"playwright": {
"url": "http://localhost:8931/mcp"
}
}
}
For legacy SSE transport support, you can use the /sse endpoint instead.
docker 启动
docker run -d -i --rm --init --pull=always \
--entrypoint node \
--name playwright \
-p 8931:8931 \
mcr.microsoft.com/playwright/mcp \
cli.js --headless --browser chromium --no-sandbox --port 8931
mcp 分析

\
npx @modelcontextprotocol/inspector
核心 API
- browser_navigate
- browser_snapshot
- browser_click
- browser_type
- browser_evaluate
- browser_select_option
- browser_wait_for
工具规范
{
"tools": [
{
"name": "browser_close",
"description": "Close the page",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
},
"annotations": {
"title": "Close browser",
"readOnlyHint": false,
"destructiveHint": true,
"openWorldHint": true
}
},
{
"name": "browser_resize",
"description": "Resize the browser window",
"inputSchema": {
"type": "object",
"properties": {
"width": {
"type": "number",
"description": "Width of the browser window"
},
"height": {
"type": "number",
"description": "Height of the browser window"
}
},
"required": ["width", "height"],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
},
"annotations": {
"title": "Resize browser window",
"readOnlyHint": false,
"destructiveHint": true,
"openWorldHint": true
}
},
{
"name": "browser_console_messages",
"description": "Returns all console messages",
"inputSchema": {
"type": "object",
"properties": {
"onlyErrors": {
"type": "boolean",
"description": "Only return error messages"
}
},
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
},
"annotations": {
"title": "Get console messages",
"readOnlyHint": true,
"destructiveHint": false,
"openWorldHint": true
}
},
{
"name": "browser_handle_dialog",
"description": "Handle a dialog",
"inputSchema": {
"type": "object",
"properties": {
"accept": {
"type": "boolean",
"description": "Whether to accept the dialog."
},
"promptText": {
"type": "string",
"description": "The text of the prompt in case of a prompt dialog."
}
},
"required": ["accept"],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
},
"annotations": {
"title": "Handle a dialog",
"readOnlyHint": false,
"destructiveHint": true,
"openWorldHint": true
}
},
{
"name": "browser_evaluate",
"description": "Evaluate JavaScript expression on page or element",
"inputSchema": {
"type": "object",
"properties": {
"function": {
"type": "string",
"description": "() => { /* code */ } or (element) => { /* code */ } when element is provided"
},
"element": {
"type": "string",
"description": "Human-readable element description used to obtain permission to interact with the element"
},
"ref": {
"type": "string",
"description": "Exact target element reference from the page snapshot"
}
},
"required": ["function"],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
},
"annotations": {
"title": "Evaluate JavaScript",
"readOnlyHint": false,
"destructiveHint": true,
"openWorldHint": true
}
},
{
"name": "browser_file_upload",
"description": "Upload one or multiple files",
"inputSchema": {
"type": "object",
"properties": {
"paths": {
"type": "array",
"items": {
"type": "string"
},
"description": "The absolute paths to the files to upload. Can be single file or multiple files. If omitted, file chooser is cancelled."
}
},
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
},
"annotations": {
"title": "Upload files",
"readOnlyHint": false,
"destructiveHint": true,
"openWorldHint": true
}
},
{
"name": "browser_fill_form",
"description": "Fill multiple form fields",
"inputSchema": {
"type": "object",
"properties": {
"fields": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Human-readable field name"
},
"type": {
"type": "string",
"enum": [
"textbox",
"checkbox",
"radio",
"combobox",
"slider"
],
"description": "Type of the field"
},
"ref": {
"type": "string",
"description": "Exact target field reference from the page snapshot"
},
"value": {
"type": "string",
"description": "Value to fill in the field. If the field is a checkbox, the value should be `true` or `false`. If the field is a combobox, the value should be the text of the option."
}
},
"required": ["name", "type", "ref", "value"],
"additionalProperties": false
},
"description": "Fields to fill in"
}
},
"required": ["fields"],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
},
"annotations": {
"title": "Fill form",
"readOnlyHint": false,
"destructiveHint": true,
"openWorldHint": true
}
},
{
"name": "browser_install",
"description": "Install the browser specified in the config. Call this if you get an error about the browser not being installed.",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
},
"annotations": {
"title": "Install the browser specified in the config",
"readOnlyHint": false,
"destructiveHint": true,
"openWorldHint": true
}
},
{
"name": "browser_press_key",
"description": "Press a key on the keyboard",
"inputSchema": {
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "Name of the key to press or a character to generate, such as `ArrowLeft` or `a`"
}
},
"required": ["key"],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
},
"annotations": {
"title": "Press a key",
"readOnlyHint": false,
"destructiveHint": true,
"openWorldHint": true
}
},
{
"name": "browser_type",
"description": "Type text into editable element",
"inputSchema": {
"type": "object",
"properties": {
"element": {
"type": "string",
"description": "Human-readable element description used to obtain permission to interact with the element"
},
"ref": {
"type": "string",
"description": "Exact target element reference from the page snapshot"
},
"text": {
"type": "string",
"description": "Text to type into the element"
},
"submit": {
"type": "boolean",
"description": "Whether to submit entered text (press Enter after)"
},
"slowly": {
"type": "boolean",
"description": "Whether to type one character at a time. Useful for triggering key handlers in the page. By default entire text is filled in at once."
}
},
"required": ["element", "ref", "text"],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
},
"annotations": {
"title": "Type text",
"readOnlyHint": false,
"destructiveHint": true,
"openWorldHint": true
}
},
{
"name": "browser_navigate",
"description": "Navigate to a URL",
"inputSchema": {
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "The URL to navigate to"
}
},
"required": ["url"],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
},
"annotations": {
"title": "Navigate to a URL",
"readOnlyHint": false,
"destructiveHint": true,
"openWorldHint": true
}
},
{
"name": "browser_navigate_back",
"description": "Go back to the previous page",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
},
"annotations": {
"title": "Go back",
"readOnlyHint": false,
"destructiveHint": true,
"openWorldHint": true
}
},
{
"name": "browser_network_requests",
"description": "Returns all network requests since loading the page",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
},
"annotations": {
"title": "List network requests",
"readOnlyHint": true,
"destructiveHint": false,
"openWorldHint": true
}
},
{
"name": "browser_take_screenshot",
"description": "Take a screenshot of the current page. You can't perform actions based on the screenshot, use browser_snapshot for actions.",
"inputSchema": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["png", "jpeg"],
"default": "png",
"description": "Image format for the screenshot. Default is png."
},
"filename": {
"type": "string",
"description": "File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg}` if not specified."
},
"element": {
"type": "string",
"description": "Human-readable element description used to obtain permission to screenshot the element. If not provided, the screenshot will be taken of viewport. If element is provided, ref must be provided too."
},
"ref": {
"type": "string",
"description": "Exact target element reference from the page snapshot. If not provided, the screenshot will be taken of viewport. If ref is provided, element must be provided too."
},
"fullPage": {
"type": "boolean",
"description": "When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Cannot be used with element screenshots."
}
},
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
},
"annotations": {
"title": "Take a screenshot",
"readOnlyHint": true,
"destructiveHint": false,
"openWorldHint": true
}
},
{
"name": "browser_snapshot",
"description": "Capture accessibility snapshot of the current page, this is better than screenshot",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
},
"annotations": {
"title": "Page snapshot",
"readOnlyHint": true,
"destructiveHint": false,
"openWorldHint": true
}
},
{
"name": "browser_click",
"description": "Perform click on a web page",
"inputSchema": {
"type": "object",
"properties": {
"element": {
"type": "string",
"description": "Human-readable element description used to obtain permission to interact with the element"
},
"ref": {
"type": "string",
"description": "Exact target element reference from the page snapshot"
},
"doubleClick": {
"type": "boolean",
"description": "Whether to perform a double click instead of a single click"
},
"button": {
"type": "string",
"enum": ["left", "right", "middle"],
"description": "Button to click, defaults to left"
},
"modifiers": {
"type": "array",
"items": {
"type": "string",
"enum": ["Alt", "Control", "ControlOrMeta", "Meta", "Shift"]
},
"description": "Modifier keys to press"
}
},
"required": ["element", "ref"],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
},
"annotations": {
"title": "Click",
"readOnlyHint": false,
"destructiveHint": true,
"openWorldHint": true
}
},
{
"name": "browser_drag",
"description": "Perform drag and drop between two elements",
"inputSchema": {
"type": "object",
"properties": {
"startElement": {
"type": "string",
"description": "Human-readable source element description used to obtain the permission to interact with the element"
},
"startRef": {
"type": "string",
"description": "Exact source element reference from the page snapshot"
},
"endElement": {
"type": "string",
"description": "Human-readable target element description used to obtain the permission to interact with the element"
},
"endRef": {
"type": "string",
"description": "Exact target element reference from the page snapshot"
}
},
"required": ["startElement", "startRef", "endElement", "endRef"],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
},
"annotations": {
"title": "Drag mouse",
"readOnlyHint": false,
"destructiveHint": true,
"openWorldHint": true
}
},
{
"name": "browser_hover",
"description": "Hover over element on page",
"inputSchema": {
"type": "object",
"properties": {
"element": {
"type": "string",
"description": "Human-readable element description used to obtain permission to interact with the element"
},
"ref": {
"type": "string",
"description": "Exact target element reference from the page snapshot"
}
},
"required": ["element", "ref"],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
},
"annotations": {
"title": "Hover mouse",
"readOnlyHint": false,
"destructiveHint": true,
"openWorldHint": true
}
},
{
"name": "browser_select_option",
"description": "Select an option in a dropdown",
"inputSchema": {
"type": "object",
"properties": {
"element": {
"type": "string",
"description": "Human-readable element description used to obtain permission to interact with the element"
},
"ref": {
"type": "string",
"description": "Exact target element reference from the page snapshot"
},
"values": {
"type": "array",
"items": {
"type": "string"
},
"description": "Array of values to select in the dropdown. This can be a single value or multiple values."
}
},
"required": ["element", "ref", "values"],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
},
"annotations": {
"title": "Select option",
"readOnlyHint": false,
"destructiveHint": true,
"openWorldHint": true
}
},
{
"name": "browser_tabs",
"description": "List, create, close, or select a browser tab.",
"inputSchema": {
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["list", "new", "close", "select"],
"description": "Operation to perform"
},
"index": {
"type": "number",
"description": "Tab index, used for close/select. If omitted for close, current tab is closed."
}
},
"required": ["action"],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
},
"annotations": {
"title": "Manage tabs",
"readOnlyHint": false,
"destructiveHint": true,
"openWorldHint": true
}
},
{
"name": "browser_wait_for",
"description": "Wait for text to appear or disappear or a specified time to pass",
"inputSchema": {
"type": "object",
"properties": {
"time": {
"type": "number",
"description": "The time to wait in seconds"
},
"text": {
"type": "string",
"description": "The text to wait for"
},
"textGone": {
"type": "string",
"description": "The text to wait for to disappear"
}
},
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
},
"annotations": {
"title": "Wait for",
"readOnlyHint": true,
"destructiveHint": false,
"openWorldHint": true
}
}
]
}
智能体调用示例 打开网站
{
"id": "2c648d52-ba94-4414-a4e1-7540142d7fa2",
"object": "chat.completion",
"created": 1760100197,
"model": "deepseek-chat",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "我将帮您完成这些步骤。首先打开bing.com,然后搜索\"霍格沃兹测试开发学社\",最后打开第一条搜索结果。",
"tool_calls": [
{
"index": 0,
"id": "call_00_OpAXlyl3GkVarIpg1LwIGsLN",
"type": "function",
"function": {
"name": "browser_navigate",
"arguments": "{\"url\": \"https://www.bing.com\"}"
}
}
]
},
"logprobs": null,
"finish_reason": "tool_calls"
}
],
"usage": {
"prompt_tokens": 2720,
"completion_tokens": 49,
"total_tokens": 2769,
"prompt_tokens_details": {
"cached_tokens": 2624
},
"prompt_cache_hit_tokens": 2624,
"prompt_cache_miss_tokens": 96
},
"system_fingerprint": "fp_ffc7281d48_prod0820_fp8_kvcache"
}
输入
{
"id": "602b83ce-75fa-4a6a-891e-a58ddfe305cd",
"object": "chat.completion",
"created": 1760100202,
"model": "deepseek-chat",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "现在我在搜索框中输入\"霍格沃兹测试开发学社\"进行搜索。",
"tool_calls": [
{
"index": 0,
"id": "call_00_nKFl4DB5YQOd1ri6LoAAGrLW",
"type": "function",
"function": {
"name": "browser_type",
"arguments": "{\"element\": \"搜索框\", \"ref\": \"e38\", \"text\": \"霍格沃兹测试开发学社\", \"submit\": true}"
}
}
]
},
"logprobs": null,
"finish_reason": "tool_calls"
}
],
"usage": {
"prompt_tokens": 5526,
"completion_tokens": 58,
"total_tokens": 5584,
"prompt_tokens_details": {
"cached_tokens": 2752
},
"prompt_cache_hit_tokens": 2752,
"prompt_cache_miss_tokens": 2774
},
"system_fingerprint": "fp_ffc7281d48_prod0820_fp8_kvcache"
}
点击
{
"id": "f9953ef3-50c9-46fc-88c3-5d2eaf200e45",
"object": "chat.completion",
"created": 1760100207,
"model": "deepseek-chat",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "现在我来点击第一条搜索结果,即\"testing-studio.com\"链接。",
"tool_calls": [
{
"index": 0,
"id": "call_00_9M3a1ibpYGU5qCjvL0ukForP",
"type": "function",
"function": {
"name": "browser_click",
"arguments": "{\"element\": \"第一条搜索结果链接\", \"ref\": \"e75\"}"
}
}
]
},
"logprobs": null,
"finish_reason": "tool_calls"
}
],
"usage": {
"prompt_tokens": 24139,
"completion_tokens": 39,
"total_tokens": 24178,
"prompt_tokens_details": {
"cached_tokens": 2752
},
"prompt_cache_hit_tokens": 2752,
"prompt_cache_miss_tokens": 21387
},
"system_fingerprint": "fp_ffc7281d48_prod0820_fp8_kvcache"
}
智能体集成
官方推荐的智能体集成
- Claude Code
- Claude Desktop
- Codex
- Cursor
- Gemini CLI
- Goose
{}
- LM Studio
- opencode
- Qodo Gen
- VS Code
- Windsurf
Dify 集成 - 暂时还不支持
dify 的 agent 协议目前还不支持 session 管理,每次调用会重新发起请求,导致每次调用工具后都会关闭 session,从而导致在执行一步操作后浏览器就会关闭。等 dify 后续有合适的 agent 策略更新后我们再集成。
\
- 定制 dify 的 agent 策略
- 自己编写 agent 以工具的形式接入 dify
微软 Magentic-UI
Magentic-UI 已经内置了 playwright agent
\
# 安装
pip install magentic-ui -U
# docker 模式
magentic-ui --port 8081
# host 模式
magentic-ui --port 8081 --run-without-docker
LangGraph 智能体集成 playwright mcp
import asyncio
from typing import Optional, Any
from langchain_core.messages import AnyMessage, SystemMessage, HumanMessage, ToolMessage
from langchain_core.tools import Tool
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_mcp_adapters.tools import load_mcp_tools
from langchain_ollama import ChatOllama
from langgraph.constants import START, END
from langgraph.graph import StateGraph
from langgraph.graph.state import _get_node_name
from pydantic import BaseModel
from hogwarts_playwright.log import info
class WebState(BaseModel):
"""
网页任务的基本结构
"""
task: Optional[str] = None
system: Optional[str] = None
messages: list[AnyMessage] = []
def get_last_message(self):
return self.messages[-1]
def model_post_init(self, context: Any, /) -> None:
"""
初始化基本的消息结构
:param context:
:return:
"""
if self.system and not self.messages:
self.messages.append(SystemMessage(self.system))
if self.task and not self.messages:
self.messages.append(HumanMessage(self.task))
info(self)
class LLM:
def __init__(self, tools: list):
"""
调用大模型,本地或者远程
:param tools:
"""
self.llm = ChatOllama(
model='qwen3',
# model='deepseek-r1',
# model='gemma3:latest',
# model='qwen2.5',
reasoning=False,
base_url='http://127.0.0.1:11435'
)
# self.llm = ChatOpenAI(
# model='deepseek-chat',
# base_url='http://127.0.0.1:8001',
# api_key=os.getenv('DEEPSEEK_TOKEN'),
# )
self.llm = self.llm.bind_tools(tools)
def reduce_messages(self, messages: list[AnyMessage]):
"""
精简历史上下文信息,去掉之前的网页结果,降低token数量
:param messages:
:return:
"""
found = False
delete_list = []
for i, message in enumerate(messages):
if isinstance(message, ToolMessage):
delete_list.append(i)
for i in delete_list[0:-1]:
tool_message = messages[i]
if isinstance(tool_message, ToolMessage):
tool_message.content = ''
return messages
async def __call__(self, web: WebState):
"""
发起调用
:param web:
:return:
"""
info(web)
web.messages = self.reduce_messages(messages=web.messages)
r = self.llm.invoke(input=web.messages)
web.messages.append(r)
return {
'messages': web.messages
}
class PlaywrightTools():
def __init__(self, tools: list[Tool]):
self.tools_by_name = {tool.name: tool for tool in tools}
async def __call__(self, web: WebState):
"""
调用playwright mcp工具
:param web:
:return:
"""
info(web)
message = web.get_last_message()
if message.tool_calls:
tool_call = message.tool_calls[0]
info(tool_call)
loop = asyncio.get_event_loop()
if loop:
# 异步调用
tool_result = await self.tools_by_name[tool_call['name']].ainvoke(tool_call['args'])
else:
# 同步调用
tool_result = asyncio.run(
self.tools_by_name[tool_call['name']].ainvoke(tool_call['args'])
)
info(tool_result)
tool_message = ToolMessage(
name=tool_call['name'],
tool_call_id=tool_call['id'],
content=tool_result,
)
# 只保留一个工具调用,因为每次动作都可能会导致页面发生变化,最好不要一次执行多个步骤。
message.tool_calls = [message.tool_calls[0]]
web.messages.append(tool_message)
return {
'messages': web.messages
}
workflow_builder = StateGraph(WebState)
client = MultiServerMCPClient(
{
"playwright-mcp": {
# Ensure you start your weather server on port 8000
"url": "http://localhost:8931/mcp",
"transport": "streamable_http",
},
"playwright-sse": {
# Ensure you start your weather server on port 8000
"url": "http://localhost:8931/sse",
"transport": "sse",
},
"playwright_stdio": {
"command": "npx",
# Replace with absolute path to your math_server.py file
"args": ["-y", "@playwright/mcp@latest", "--port", "8931"],
"transport": "stdio",
}
}
)
class Router():
def __call__(self, web: WebState):
"""
简单的路由规则,本质也是create_react_agent的基本逻辑。因为create_react_agent不支持禁用并行执行,所以需要自己实现。
:param web:
:return:
"""
info(web)
if web.get_last_message().tool_calls:
return 'tools'
else:
return 'end'
def build_workflow(tools):
"""
构建工作流
:param tools:
:return:
"""
playwright = PlaywrightTools(tools)
llm = LLM(tools)
router = Router()
workflow_builder.add_node(llm)
workflow_builder.add_node(playwright)
workflow_builder.add_edge(START, _get_node_name(llm))
workflow_builder.add_edge(_get_node_name(playwright), _get_node_name(llm))
workflow_builder.add_conditional_edges(
_get_node_name(llm),
router,
{
'end': END,
'tools': _get_node_name(playwright)
}
)
workflow = workflow_builder.compile()
info(workflow.get_graph().draw_ascii())
return workflow
async def session(task):
"""
初始化mcp session,浏览器的自动化session。session关闭,浏览器会关闭。
:param task:
:return:
"""
async with client.session('playwright-mcp') as session:
tools = await load_mcp_tools(session)
info(tools)
workflow = build_workflow(tools)
state = WebState(task=task)
await workflow.ainvoke(
input=state
)
def test_workflow():
info("start")
asyncio.run(session(task='打开bing.com 搜索霍格沃兹测试开发学社'))
{.!grow-2}
+-----------+
| __start__ |
+-----------+
*
*
*
+-----+
| LLM |
+-----+.
*** ...
* .
** ..
+-----------------+ +---------+
| PlaywrightTools | | __end__ |
+-----------------+ +---------+
hogwarts-playwright-agent
为了方便大家使用,学社给大家做了一个简单的体验工具
\
# 安装
pip install hogwarts-playwright-agent
# 启动playwright-mcp
npx -y @playwright/mcp@latest --port 8931
# 使用霍格沃兹测试开发学社学员版agent
hogwarts-playwright-agent --llm.model deepseek-chat --llm.base_url https://api.deepseek.com/v1 --llm.key $DEEPSEEK_TOKEN -p '打开百度 搜索霍格沃兹测试开发学社 打开搜索结果中的第一条链接' --mcp.name playwright --mcp.url http://localhost:8931/sse
Web 智能体对比
| 项目 | 智能体 | 内置工具 | MCP 协议 | 三方工具集成 |
|---|---|---|---|---|
| Browser Use | Agent | 是 | 只支持 stdio | 支持扩展 |
| Playwright-MCP | 全部 mcp 传输协议 | |||
| MAgenticUI | Agent | 是 | 支持 mcp 集成 |