WebAgent — Browser Interaction via Chrome DevTools¶
Overview¶
WebAgent (parrot.bots.chrome) is a general-purpose web-interaction agent
that drives a real Google Chrome instance through the Chrome DevTools
Protocol (CDP). Instead of Selenium/Playwright, it mounts the
chrome-devtools-mcp
MCP server, which exposes navigation, clicking, form filling, screenshots,
console logs, network monitoring and performance tracing as LLM-callable
tools.
Two ways of working:
- Free-form interaction —
await agent.ask("Open the pricing page and tell me the cheapest plan"). The LLM decides which DevTools tools to call. - QA test execution —
await agent.run_tests([...])runs a list ofQATestCaseobjects (natural-language steps + formal assertions) and returns a structuredQAReportwith a CI exit code and JUnit XML.
If you want deterministic, catalogued navigation on top of your own Chrome profile instead, see WebNavigatorAgent.
Architecture¶
WebAgent (BasicAgent)
│ configure()
▼
add_chrome_devtools_mcp_server(browser_url=..., headless=..., user_data_dir=...)
│
├── ChromeManager (parrot.mcp.chrome) ── launches Chrome with
│ --remote-debugging-port=<port> if nothing answers on it
│
└── MCP stdio client ──► npx -y chrome-devtools-mcp@latest --browser-url=http://127.0.0.1:9222 ...
│
└── CDP ──► Google Chrome
ChromeConfig (Pydantic) captures the subset of chrome-devtools-mcp
flags AI-Parrot needs and renders them with to_mcp_args().
Requirements¶
- Google Chrome (or Chromium) installed and on
PATH(google-chrome,google-chrome-stable,chromium,chromium-browseror the macOS app bundle are auto-detected). - Node.js /
npx—chrome-devtools-mcpis fetched on first run. - An LLM provider key in the environment.
Quick start¶
import asyncio
from parrot.bots.chrome import WebAgent, ChromeConfig
async def main():
agent = WebAgent(
chrome_config=ChromeConfig(headless=False, viewport="1920x1080"),
llm="google:gemini-2.5-flash",
)
await agent.configure() # starts Chrome (if needed) + the MCP server
result = await agent.ask(
"Go to https://quotes.toscrape.com, open the 'love' tag and list the authors"
)
print(result.response)
asyncio.run(main())
ChromeConfig reference¶
| Field | Default | Description |
|---|---|---|
browser_url |
None → http://127.0.0.1:<port> |
Debugging endpoint of an already-running Chrome. |
port |
9222 |
Remote-debugging port used when browser_url is not set. |
headless |
False |
Launch Chrome without a window. Sessions are visible by default. |
user_data_dir |
None |
Chrome user-data directory to reuse (cookies, logins, extensions). |
channel |
None |
stable / beta / dev / canary. |
viewport |
None |
Initial viewport, e.g. "1920x1080". |
executable_path |
None |
Explicit Chrome binary. |
isolated |
False |
Temporary user-data-dir, cleaned up on close. |
no_usage_statistics |
True |
Opt out of chrome-devtools-mcp telemetry. |
auto_connect |
False |
Let chrome-devtools-mcp discover a running local Chrome (Chrome ≥ 144). Skips ChromeManager. |
WebAgent itself also accepts default_timeout_ms (per-test timeout for
run_tests, default 60 s) and screenshot_dir (where failure screenshots
are saved).
Attaching to your own Chrome (remote debugging)¶
By default ChromeManager launches a profile-less Chrome when nothing
answers on the debugging port. To reuse your sessions, extensions and saved
logins, start Chrome yourself with the debugging port open and a dedicated
user-data directory, then point WebAgent at it.
Chrome ≥ 136 requires an explicit --user-data-dir
Chrome refuses --remote-debugging-port on the default profile. Use a
dedicated directory — a copy of your real profile works well (see
copying the profile).
Verify the endpoint, then connect:
agent = WebAgent(
chrome_config=ChromeConfig(browser_url="http://127.0.0.1:9222"),
llm="anthropic:claude-sonnet-5",
)
Any bot can gain the same capability without subclassing WebAgent:
QA test execution¶
run_tests() executes each QATestCase in its own ask() call so that
per-test retries and timeouts apply, and a single LLM/MCP/network error
never aborts the suite.
from parrot.bots.chrome import QATestCase, QAAssertion
cases = [
QATestCase(
name="homepage-smoke",
url="http://localhost:3000",
steps=["Wait for the page to fully load"],
expected="Page loads without errors",
assertions=[
QAAssertion(check="no_console_errors"),
QAAssertion(check="no_network_failures"),
QAAssertion(check="response_status", target="200"),
],
tags=["smoke"],
max_retries=1,
timeout_ms=30_000,
),
QATestCase(
name="login-validation",
url="http://localhost:3000/login",
steps=[
"Leave the email field empty",
"Type '123' in the password field",
"Click the submit/login button",
],
expected="Validation errors appear for required fields",
assertions=[
QAAssertion(check="url_matches", target="/login"),
QAAssertion(check="element_visible", target=".error-message",
wait_timeout_ms=3000),
],
tags=["regression"],
),
]
msg = await agent.run_tests(cases, tags=["smoke"])
report = msg.output # QAReport
print(msg.response) # "1/2 passed"
open("results.xml", "w").write(report.to_junit_xml())
raise SystemExit(report.exit_code)
Assertion types¶
check |
target / value |
Meaning |
|---|---|---|
element_visible / element_not_visible |
CSS selector | Waits up to wait_timeout_ms (default 5000). |
text_contains |
text | Page contains the text. |
url_matches |
pattern | Current URL matches. |
no_console_errors |
— | No JS errors in the console. |
no_network_failures |
— | No 4xx/5xx requests. |
response_status |
status code | Main document status. |
performance |
threshold | Performance metrics against thresholds. |
accessibility_check |
— | ARIA roles, alt text, heading hierarchy. |
screenshot_diff |
— | Placeholder — not implemented yet. |
QAReport¶
summary, url, findings: list[QAFinding], total/passed/failed/errors/skipped,
duration_ms, plus:
exit_code→0whenfailed + errors == 0, else1.to_junit_xml(suite_name)→ JUnit XML for CircleCI, GitHub Actions, GitLab CI and Jenkins.
Each QAFinding carries status (pass|fail|error|skip), detail,
screenshot_path, console_errors, duration_ms and retries.
CLI runner (CI/CD)¶
parrot.bots.chrome_runner runs a JSON test file without writing Python:
python -m parrot.bots.chrome_runner \
--test-file examples/qa-tests-sample.json \
--url http://localhost:3000 \
--headless --tags smoke \
--junit-output qa-results/results.xml \
--screenshot-dir qa-screenshots/ \
--default-timeout 60000 --port 9222 --viewport 1920x1080
The process exits with QAReport.exit_code, so it works directly as a
deploy-or-block gate. See examples/chrome_ci_test.py for a CircleCI
snippet, and examples/qa-tests-sample.json for the file format
(one QATestCase per array element).
Examples¶
| File | What it shows |
|---|---|
examples/chrome_qa_test.py |
Interactive QA run against a site. |
examples/chrome_ci_test.py |
Headless CI pipeline: tags, retries, JUnit, screenshots, exit code. |
examples/chrome_performance_test.py |
Performance assertions. |
examples/qa-tests-sample.json |
Test file for the CLI runner. |
See also¶
- WebNavigatorAgent — catalogued, deterministic
navigation with
WebBrowsingToolkiton a real Chrome profile. - Design note — WebAgent Chrome DevTools MCP
- MCP Sessions