Skip to content

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 集成