patchright-cli 0.8.0__tar.gz → 0.9.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/.claude-plugin/marketplace.json +1 -1
- patchright_cli-0.9.0/.github/dependabot.yml +10 -0
- patchright_cli-0.9.0/.github/workflows/ci.yml +86 -0
- patchright_cli-0.9.0/.github/workflows/publish-docker.yml +65 -0
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/CLAUDE.md +2 -2
- patchright_cli-0.9.0/Dockerfile +37 -0
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/PKG-INFO +103 -6
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/README.md +99 -3
- patchright_cli-0.9.0/compose.yml +34 -0
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/pyproject.toml +6 -4
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/src/patchright_cli/__init__.py +1 -1
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/src/patchright_cli/cli.py +18 -4
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/src/patchright_cli/daemon.py +519 -29
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/src/patchright_cli/mcp_server.py +127 -32
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/tests/test_cli_config.py +18 -0
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/tests/test_daemon_handlers.py +4 -0
- patchright_cli-0.9.0/tests/test_daemon_token.py +280 -0
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/tests/test_e2e.py +2 -2
- patchright_cli-0.9.0/tests/test_mcp.py +445 -0
- patchright_cli-0.9.0/tests/test_navigation_policy.py +361 -0
- patchright_cli-0.9.0/tests/test_network_guard.py +192 -0
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/tests/test_profiles.py +13 -0
- patchright_cli-0.9.0/tests/test_supply_chain.py +65 -0
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/uv.lock +6 -4
- patchright_cli-0.8.0/.github/workflows/ci.yml +0 -34
- patchright_cli-0.8.0/tests/test_mcp.py +0 -172
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/.claude-plugin/skills/browser-automation.json +0 -0
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/.github/workflows/publish.yml +0 -0
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/.gitignore +0 -0
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/.pre-commit-config.yaml +0 -0
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/CONTRIBUTING.md +0 -0
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/GLOSSARY.md +0 -0
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/LICENSE +0 -0
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/Makefile +0 -0
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/docs/installation.md +0 -0
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/docs/mcp-architecture.svg +0 -0
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/patchright-cli +0 -0
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/patchright-cli.cmd +0 -0
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/skills/patchright-cli/SKILL.md +0 -0
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/skills/patchright-cli/references/request-mocking.md +0 -0
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/skills/patchright-cli/references/running-code.md +0 -0
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/skills/patchright-cli/references/session-management.md +0 -0
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/skills/patchright-cli/references/snapshot-refs.md +0 -0
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/skills/patchright-cli/references/storage-state.md +0 -0
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/skills/patchright-cli/references/tracing.md +0 -0
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/skills/patchright-cli/references/video-recording.md +0 -0
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/src/patchright_cli/__main__.py +0 -0
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/src/patchright_cli/dashboard.py +0 -0
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/src/patchright_cli/ref_registry.py +0 -0
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/src/patchright_cli/snapshot.py +0 -0
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/tests/__init__.py +0 -0
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/tests/fixture.html +0 -0
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/tests/test_annotate.py +0 -0
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/tests/test_attach.py +0 -0
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/tests/test_packaging.py +0 -0
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/tests/test_ref_registry.py +0 -0
- {patchright_cli-0.8.0 → patchright_cli-0.9.0}/tests/test_tabs.py +0 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
lint:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- uses: astral-sh/setup-uv@v6
|
|
15
|
+
with:
|
|
16
|
+
version: ">=0.7,<0.8"
|
|
17
|
+
python-version: "3.12"
|
|
18
|
+
- run: uv venv && uv pip install -e ".[dev]"
|
|
19
|
+
- run: uv run pre-commit run --all-files
|
|
20
|
+
|
|
21
|
+
test:
|
|
22
|
+
strategy:
|
|
23
|
+
matrix:
|
|
24
|
+
os: [ubuntu-latest, windows-latest]
|
|
25
|
+
python-version: ["3.10", "3.12"]
|
|
26
|
+
include:
|
|
27
|
+
- os: macos-latest
|
|
28
|
+
python-version: "3.12"
|
|
29
|
+
runs-on: ${{ matrix.os }}
|
|
30
|
+
steps:
|
|
31
|
+
- uses: actions/checkout@v4
|
|
32
|
+
- uses: astral-sh/setup-uv@v6
|
|
33
|
+
with:
|
|
34
|
+
version: ">=0.7,<0.8"
|
|
35
|
+
python-version: ${{ matrix.python-version }}
|
|
36
|
+
- run: uv venv && uv pip install -e ".[dev]"
|
|
37
|
+
- run: uv run pytest tests/ -v --ignore=tests/test_e2e.py
|
|
38
|
+
|
|
39
|
+
docker:
|
|
40
|
+
runs-on: ubuntu-latest
|
|
41
|
+
steps:
|
|
42
|
+
- uses: actions/checkout@v4
|
|
43
|
+
- run: docker build -t patchright-cli:ci .
|
|
44
|
+
- run: docker run --rm patchright-cli:ci --help
|
|
45
|
+
- name: Browser session inside the container
|
|
46
|
+
run: |
|
|
47
|
+
docker run -d --name cli --entrypoint sleep patchright-cli:ci infinity
|
|
48
|
+
docker exec cli uv run patchright-cli open https://example.com --headless
|
|
49
|
+
docker exec cli uv run patchright-cli eval document.title | tee title.txt
|
|
50
|
+
grep -q "Example Domain" title.txt
|
|
51
|
+
docker exec cli uv run patchright-cli close
|
|
52
|
+
- name: MCP over HTTP with a token
|
|
53
|
+
run: |
|
|
54
|
+
docker run -d --name mcp -p 9090:9090 -e PATCHRIGHT_CLI_MODE=mcp-http -e PATCHRIGHT_CLI_MCP_TOKEN=ci-token patchright-cli:ci
|
|
55
|
+
for _ in $(seq 30); do curl -s -o /dev/null http://localhost:9090/mcp && break; sleep 1; done
|
|
56
|
+
init='{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"ci","version":"0"}}}'
|
|
57
|
+
headers=(-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream")
|
|
58
|
+
test "$(curl -s -o /dev/null -w '%{http_code}' "${headers[@]}" -d "$init" http://localhost:9090/mcp)" = 401
|
|
59
|
+
test "$(curl -s -o /dev/null -w '%{http_code}' "${headers[@]}" -H "Authorization: Bearer ci-token" -d "$init" http://localhost:9090/mcp)" = 200
|
|
60
|
+
# Host/Origin checks are off by default, so any Host is accepted
|
|
61
|
+
test "$(curl -s -o /dev/null -w '%{http_code}' "${headers[@]}" -H "Authorization: Bearer ci-token" -H "Host: other.example:9090" -d "$init" http://localhost:9090/mcp)" = 200
|
|
62
|
+
- name: MCP over HTTP without a token, Host/Origin checks on
|
|
63
|
+
run: |
|
|
64
|
+
docker run -d --name mcp-open -p 9091:9090 -e PATCHRIGHT_CLI_MODE=mcp-http -e FASTMCP_HTTP_HOST_ORIGIN_PROTECTION=true -e FASTMCP_HTTP_ALLOWED_HOSTS='["patchright-mcp"]' patchright-cli:ci
|
|
65
|
+
for _ in $(seq 30); do curl -s -o /dev/null http://localhost:9091/mcp && break; sleep 1; done
|
|
66
|
+
init='{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"ci","version":"0"}}}'
|
|
67
|
+
headers=(-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream")
|
|
68
|
+
test "$(curl -s -o /dev/null -w '%{http_code}' "${headers[@]}" -d "$init" http://localhost:9091/mcp)" = 200
|
|
69
|
+
test "$(curl -s -o /dev/null -w '%{http_code}' "${headers[@]}" -H "Host: evil.example:9091" -d "$init" http://localhost:9091/mcp)" = 421
|
|
70
|
+
test "$(curl -s -o /dev/null -w '%{http_code}' "${headers[@]}" -H "Origin: http://evil.example" -d "$init" http://localhost:9091/mcp)" = 403
|
|
71
|
+
test "$(curl -s -o /dev/null -w '%{http_code}' "${headers[@]}" -H "Host: patchright-mcp:9090" -d "$init" http://localhost:9091/mcp)" = 200
|
|
72
|
+
- name: MCP over HTTP refuses to start with neither a token nor a Host/Origin choice
|
|
73
|
+
run: |
|
|
74
|
+
status=0
|
|
75
|
+
timeout 120 docker run --name mcp-unprotected -e PATCHRIGHT_CLI_MODE=mcp-http patchright-cli:ci > unprotected.log 2>&1 || status=$?
|
|
76
|
+
cat unprotected.log
|
|
77
|
+
test "$status" -ne 0
|
|
78
|
+
grep -q "refusing to start \`mcp --http\`" unprotected.log
|
|
79
|
+
- name: compose.yml works without a token
|
|
80
|
+
run: docker compose config -q
|
|
81
|
+
- if: failure()
|
|
82
|
+
run: |
|
|
83
|
+
docker logs cli || true
|
|
84
|
+
docker logs mcp || true
|
|
85
|
+
docker logs mcp-open || true
|
|
86
|
+
docker logs mcp-unprotected || true
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
name: docker-publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
inputs:
|
|
6
|
+
tag:
|
|
7
|
+
description: "Docker image tag to publish"
|
|
8
|
+
required: true
|
|
9
|
+
type: string
|
|
10
|
+
publish_docker:
|
|
11
|
+
description: "Publish docker image"
|
|
12
|
+
required: true
|
|
13
|
+
type: boolean
|
|
14
|
+
default: false
|
|
15
|
+
|
|
16
|
+
permissions:
|
|
17
|
+
contents: read
|
|
18
|
+
packages: write
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
docker:
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
name: docker-publish
|
|
24
|
+
|
|
25
|
+
steps:
|
|
26
|
+
- name: Checkout repository
|
|
27
|
+
uses: actions/checkout@v6
|
|
28
|
+
|
|
29
|
+
- name: Set up Docker Buildx
|
|
30
|
+
uses: docker/setup-buildx-action@v4
|
|
31
|
+
|
|
32
|
+
- name: Set Docker image tags
|
|
33
|
+
id: tags
|
|
34
|
+
shell: bash
|
|
35
|
+
env:
|
|
36
|
+
VERSION_TAG: ${{ inputs.tag }}
|
|
37
|
+
run: |
|
|
38
|
+
REPOSITORY="$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')"
|
|
39
|
+
IMAGE_NAME="ghcr.io/${REPOSITORY}"
|
|
40
|
+
|
|
41
|
+
{
|
|
42
|
+
echo "tags<<EOF"
|
|
43
|
+
echo "${IMAGE_NAME}:${VERSION_TAG}"
|
|
44
|
+
echo "${IMAGE_NAME}:latest"
|
|
45
|
+
echo "EOF"
|
|
46
|
+
} >> "$GITHUB_OUTPUT"
|
|
47
|
+
|
|
48
|
+
- name: Authenticate with GHCR
|
|
49
|
+
if: ${{ inputs.publish_docker }}
|
|
50
|
+
uses: docker/login-action@v4
|
|
51
|
+
with:
|
|
52
|
+
registry: ghcr.io
|
|
53
|
+
username: ${{ github.actor }}
|
|
54
|
+
password: ${{ secrets.GITHUB_TOKEN }}
|
|
55
|
+
|
|
56
|
+
- name: Build Docker image
|
|
57
|
+
uses: docker/build-push-action@v7
|
|
58
|
+
with:
|
|
59
|
+
context: .
|
|
60
|
+
file: ./Dockerfile
|
|
61
|
+
push: ${{ inputs.publish_docker }}
|
|
62
|
+
tags: ${{ steps.tags.outputs.tags }}
|
|
63
|
+
# Attestations need the docker-container driver that setup-buildx-action creates above.
|
|
64
|
+
provenance: mode=max
|
|
65
|
+
sbom: true
|
|
@@ -58,8 +58,8 @@ CLI (cli.py) --TCP:9321--> Daemon (daemon.py) --Patchright--> Chrome
|
|
|
58
58
|
- Always uses persistent browser context (`launch_persistent_context`) with real Chrome (`channel="chrome"`), not Chromium
|
|
59
59
|
- Headed by default (headless is more detectable)
|
|
60
60
|
- Default daemon port: 9321, profiles stored at `~/.patchright-cli/profiles/<profile-name>`; a session's profile defaults to its session name (`--profile=<name>` overrides, a value with `/` or `\` is a directory). One session per profile at a time
|
|
61
|
-
- CLI-daemon protocol: length-prefixed (4-byte big-endian `!I`) JSON over TCP
|
|
62
|
-
- CLI parses argv manually in `main()` — click is only used for `click.echo()`, not subcommands. New commands
|
|
61
|
+
- CLI-daemon protocol: length-prefixed (4-byte big-endian `!I`) JSON over TCP; every message carries a `token` field read from `~/.patchright-cli/daemon-<port>.token` (written 0600 by the daemon before it listens), and the daemon rejects messages without the right one
|
|
62
|
+
- CLI parses argv manually in `main()` — click is only used for `click.echo()`, not subcommands. New commands need a `COMMANDS_HELP` entry and a `@register("<name>")` handler in daemon.py; only session and lifecycle commands (open, attach, close, kill-all, ...) live in `handle_command()` itself
|
|
63
63
|
- Version must be updated in `pyproject.toml`, `src/patchright_cli/__init__.py` and `.claude-plugin/marketplace.json` (then run `uv lock` to sync the lock file; `tests/test_packaging.py` checks they match)
|
|
64
64
|
- `python -m patchright_cli` works via `__main__.py`
|
|
65
65
|
- Uses hatchling as build backend, ruff for linting (line-length=120, target py310)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Base images are pinned by multi-arch index digest; Dependabot (.github/dependabot.yml) bumps them.
|
|
2
|
+
# uv is a named stage because Dependabot only reads image references on FROM lines.
|
|
3
|
+
FROM ghcr.io/astral-sh/uv:latest@sha256:32a7bb39b496babbd997b0a39a4d66c126bdaf5661b302bf6f06abda8deca6ff AS uv
|
|
4
|
+
|
|
5
|
+
FROM python:3.12-slim@sha256:5b8b1421ccb6a525860a77863c049eab5847076cda34e030c75a80c58faf3f3c
|
|
6
|
+
|
|
7
|
+
ENV PYTHONUNBUFFERED=1 \
|
|
8
|
+
UV_LINK_MODE=copy \
|
|
9
|
+
UV_NO_SYNC=1 \
|
|
10
|
+
PATCHRIGHT_CLI_MCP_HEADLESS=1 \
|
|
11
|
+
PATCHRIGHT_CLI_MCP_ROOT=/patchright
|
|
12
|
+
|
|
13
|
+
WORKDIR /app
|
|
14
|
+
|
|
15
|
+
RUN apt-get update \
|
|
16
|
+
&& apt-get install -y --no-install-recommends ffmpeg \
|
|
17
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
18
|
+
|
|
19
|
+
COPY --from=uv /uv /uvx /bin/
|
|
20
|
+
|
|
21
|
+
COPY pyproject.toml uv.lock ./
|
|
22
|
+
RUN uv sync --frozen --no-install-project --extra mcp --no-dev
|
|
23
|
+
|
|
24
|
+
RUN uv run --no-sync python -m patchright install --with-deps chrome \
|
|
25
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
26
|
+
|
|
27
|
+
COPY README.md LICENSE ./
|
|
28
|
+
COPY src ./src
|
|
29
|
+
COPY skills ./skills
|
|
30
|
+
RUN uv sync --frozen --extra mcp --no-dev
|
|
31
|
+
|
|
32
|
+
RUN mkdir -p /patchright/output /root/.patchright-cli
|
|
33
|
+
|
|
34
|
+
EXPOSE 9090
|
|
35
|
+
|
|
36
|
+
ENTRYPOINT ["sh", "-c", "case \"$PATCHRIGHT_CLI_MODE\" in mcp-http) exec uv run patchright-cli mcp --http --host=0.0.0.0 --port=\"${PATCHRIGHT_CLI_MCP_PORT:-9090}\" ;; mcp-stdio) exec uv run patchright-cli mcp ;; \"\"|standalone) exec uv run patchright-cli \"$@\" ;; *) echo \"Unknown PATCHRIGHT_CLI_MODE: $PATCHRIGHT_CLI_MODE\" >&2; exit 2 ;; esac", "--"]
|
|
37
|
+
CMD ["--help"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: patchright-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: Anti-detect browser automation CLI using Patchright (undetected Playwright fork)
|
|
5
5
|
Project-URL: Homepage, https://github.com/AhaiMk01/patchright-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/AhaiMk01/patchright-cli
|
|
@@ -24,14 +24,15 @@ Requires-Python: >=3.10
|
|
|
24
24
|
Requires-Dist: aiohttp>=3.9
|
|
25
25
|
Requires-Dist: click
|
|
26
26
|
Requires-Dist: patchright
|
|
27
|
+
Requires-Dist: pywin32>=306; sys_platform == 'win32'
|
|
27
28
|
Requires-Dist: pyyaml
|
|
28
29
|
Provides-Extra: dev
|
|
29
|
-
Requires-Dist: fastmcp<5,>=3.
|
|
30
|
+
Requires-Dist: fastmcp<5,>=3.4.3; extra == 'dev'
|
|
30
31
|
Requires-Dist: pre-commit; extra == 'dev'
|
|
31
32
|
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
32
33
|
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
33
34
|
Provides-Extra: mcp
|
|
34
|
-
Requires-Dist: fastmcp<5,>=3.
|
|
35
|
+
Requires-Dist: fastmcp<5,>=3.4.3; extra == 'mcp'
|
|
35
36
|
Description-Content-Type: text/markdown
|
|
36
37
|
|
|
37
38
|
# patchright-cli
|
|
@@ -115,6 +116,78 @@ python -m patchright install chromium
|
|
|
115
116
|
|
|
116
117
|
</details>
|
|
117
118
|
|
|
119
|
+
### Docker
|
|
120
|
+
|
|
121
|
+
A prebuilt image is available from GHCR:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
docker pull ghcr.io/ahaimk01/patchright-cli:latest
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
By default the container runs `patchright-cli` directly, so it can be used for normal CLI commands:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
docker run --rm ghcr.io/ahaimk01/patchright-cli:latest --help
|
|
131
|
+
docker run --rm ghcr.io/ahaimk01/patchright-cli:latest profile-list
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Browser sessions live inside the container, so for a sequence of standalone CLI commands keep the same container running:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
docker run --rm -it \
|
|
138
|
+
--name patchright-cli \
|
|
139
|
+
-v patchright-profiles:/root/.patchright-cli \
|
|
140
|
+
--entrypoint /bin/sh \
|
|
141
|
+
ghcr.io/ahaimk01/patchright-cli:latest
|
|
142
|
+
|
|
143
|
+
uv run patchright-cli open https://example.com --headless
|
|
144
|
+
uv run patchright-cli snapshot
|
|
145
|
+
uv run patchright-cli close
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
For MCP over stdio, set `PATCHRIGHT_CLI_MODE=mcp-stdio` and keep stdin attached. The volumes keep logins and output after the container exits:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
docker run --rm -i \
|
|
152
|
+
-e PATCHRIGHT_CLI_MODE=mcp-stdio \
|
|
153
|
+
-v ./patchright:/patchright \
|
|
154
|
+
-v patchright-profiles:/root/.patchright-cli \
|
|
155
|
+
ghcr.io/ahaimk01/patchright-cli:latest
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
For a long-running MCP server over Streamable HTTP, set `PATCHRIGHT_CLI_MODE=mcp-http`:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
docker run -d \
|
|
162
|
+
--name patchright-mcp \
|
|
163
|
+
-p 127.0.0.1:9090:9090 \
|
|
164
|
+
-e PATCHRIGHT_CLI_MODE=mcp-http \
|
|
165
|
+
-e PATCHRIGHT_CLI_MCP_TOKEN=replace-with-a-long-random-token \
|
|
166
|
+
-v ./patchright:/patchright \
|
|
167
|
+
-v patchright-profiles:/root/.patchright-cli \
|
|
168
|
+
ghcr.io/ahaimk01/patchright-cli:latest
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The HTTP MCP endpoint is then available at `http://localhost:9090/mcp`. The MCP server defaults to headless mode in the image and uses `/patchright` as its filesystem root. CLI commands inside the container still need `--headless`, since there is no display.
|
|
172
|
+
|
|
173
|
+
An example `compose.yml` is included in the repository:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
PATCHRIGHT_CLI_MCP_TOKEN=replace-with-a-long-random-token docker compose up -d
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Both examples publish the port on `127.0.0.1` only, but that alone doesn't stop a web page in your browser from reaching the server through DNS rebinding, so `mcp-http` refuses to start unless you pick one of three defences (see [MCP Server](#mcp-server) for details): set `PATCHRIGHT_CLI_MCP_TOKEN`; set `-e FASTMCP_HTTP_HOST_ORIGIN_PROTECTION=true` so requests must be addressed to `localhost`, `127.0.0.1` or `::1`, listing any other hostname or IP (a LAN address, a Compose service name) as a JSON array, for example `-e FASTMCP_HTTP_ALLOWED_HOSTS='["patchright-mcp", "192.168.1.20"]'`; or set `-e FASTMCP_HTTP_HOST_ORIGIN_PROTECTION=false` on purpose. A plain `docker run -e PATCHRIGHT_CLI_MODE=mcp-http` with neither variable exits with an error. The example `compose.yml` sets `FASTMCP_HTTP_HOST_ORIGIN_PROTECTION: "false"` so reverse-proxy and MCP hub setups keep working, which makes the token optional there: leave it out if your MCP client can't send an `Authorization` header. A reverse proxy with auth or an IP allowlist can stand in for the token. If you publish the port beyond loopback, protect it one of these ways: the endpoint gives full control of the browser and its saved logins.
|
|
180
|
+
|
|
181
|
+
### Chrome sandbox
|
|
182
|
+
|
|
183
|
+
Patchright starts Chrome with `--no-sandbox`, so by default no Chrome process is sandboxed (`chrome://sandbox` lists every one as "Not Sandboxed", on Windows too). Set `PATCHRIGHT_CLI_CHROME_SANDBOX=1` to launch Chrome with its sandbox instead. It is off by default.
|
|
184
|
+
|
|
185
|
+
The variable is read by the daemon, not by each CLI command. The CLI passes its environment on when it starts the daemon, so set it before the first `open`; a daemon that is already running keeps the setting it started with until you restart it (`patchright-cli kill-all`).
|
|
186
|
+
|
|
187
|
+
In Docker the image runs as root, and Chrome will not start as root with its sandbox on, so the switch also needs a non-root user (`docker run --user ...` with a writable `HOME`). Even then, Docker's default seccomp profile blocks the namespaces Chrome's Linux sandbox needs: measured on a GitHub Actions runner, as uid 1000 with the switch on, Chrome exits at launch ("Failed to move to new namespace ... Operation not permitted"). Leave the switch off in Docker; the image runs Chrome without its sandbox.
|
|
188
|
+
|
|
189
|
+
How the sandbox affects bot detection has not been measured.
|
|
190
|
+
|
|
118
191
|
---
|
|
119
192
|
|
|
120
193
|
## Quick Start
|
|
@@ -154,6 +227,15 @@ graph LR
|
|
|
154
227
|
| **CLI** (`cli.py`) | Thin client — connects, sends command, prints result, disconnects. Browser stays open. |
|
|
155
228
|
| **Snapshot** (`snapshot.py`) | Playwright `aria_snapshot()` accessibility tree, annotated with sequential `[ref=eN]` handles for element targeting. |
|
|
156
229
|
|
|
230
|
+
### Daemon token
|
|
231
|
+
|
|
232
|
+
The daemon only listens on `127.0.0.1`, but any local process can reach that port, so every message must carry a secret token. On startup the daemon writes a fresh random token to `~/.patchright-cli/daemon-<port>.token` (one file per port, so daemons on different ports coexist), before it starts listening, and deletes the file on a clean shutdown. The CLI (and the MCP server, which runs the CLI) reads the file for its `--port` and sends the token with each command; a message with a missing or wrong token is rejected and nothing runs.
|
|
233
|
+
|
|
234
|
+
- On Linux/macOS the file is created with mode `0600`. On Windows, POSIX modes do not apply, so the file is created with its own protected ACL: one full-access entry for your user account and nothing inherited from `~/.patchright-cli`, whatever that directory grants. If the daemon cannot create the file that way it does not start.
|
|
235
|
+
- A daemon holds `~/.patchright-cli/daemon-<port>.lock` while it runs, so only one daemon per port ever writes that port's token; a second daemon started on the same port exits without touching it. The lock file stays after shutdown and is harmless (the OS releases the lock when the process exits).
|
|
236
|
+
- Run the CLI as the same user, with the same home directory, as the daemon. In Docker the MCP server and daemon share a user and `HOME`, so this holds.
|
|
237
|
+
- An older CLI talking to a newer daemon gets an error saying the daemon requires an auth token: upgrade, or stop the daemon (`patchright-cli kill-all` from the newer install, or end the process). A newer CLI talking to an older daemon finds no token file and sends none, which the older daemon accepts.
|
|
238
|
+
|
|
157
239
|
---
|
|
158
240
|
|
|
159
241
|
<details>
|
|
@@ -385,15 +467,27 @@ pip install 'patchright-cli[mcp]'
|
|
|
385
467
|
# stdio (default)
|
|
386
468
|
patchright-cli mcp
|
|
387
469
|
|
|
388
|
-
# Streamable HTTP
|
|
389
|
-
patchright-cli mcp --http --host=127.0.0.1 --port=8000
|
|
470
|
+
# Streamable HTTP (needs a token or an explicit Host/Origin choice, see below)
|
|
471
|
+
PATCHRIGHT_CLI_MCP_TOKEN=replace-with-a-long-random-token patchright-cli mcp --http --host=127.0.0.1 --port=8000
|
|
390
472
|
```
|
|
391
473
|
|
|
392
474
|

|
|
393
475
|
|
|
394
476
|
The HTTP endpoint is `/mcp`. Set `PATCHRIGHT_CLI_MCP_TOKEN` to require `Authorization: Bearer <token>` on every request, and always set it before binding to `0.0.0.0`.
|
|
395
477
|
|
|
396
|
-
|
|
478
|
+
FastMCP's Host and Origin checks are off by default, so binding to `127.0.0.1` alone does not stop a web page from reaching a server without a token through DNS rebinding. `patchright-cli mcp --http` therefore refuses to start, whatever the bind host, unless `PATCHRIGHT_CLI_MCP_TOKEN` is set or `FASTMCP_HTTP_HOST_ORIGIN_PROTECTION` is set to a value FastMCP accepts (an empty value counts as unset). Pick one:
|
|
479
|
+
|
|
480
|
+
- Set `PATCHRIGHT_CLI_MCP_TOKEN`.
|
|
481
|
+
- Set `FASTMCP_HTTP_HOST_ORIGIN_PROTECTION=true` to accept only requests addressed to `localhost`, `127.0.0.1` or `::1` (421 otherwise) and reject foreign `Origin` headers (403). To allow another hostname or IP, list it as a JSON array, for example `FASTMCP_HTTP_ALLOWED_HOSTS='["mcp.lan"]'`.
|
|
482
|
+
- Set `FASTMCP_HTTP_HOST_ORIGIN_PROTECTION=false` on purpose, for example behind a reverse proxy with auth or an IP allowlist.
|
|
483
|
+
|
|
484
|
+
A reverse proxy with auth or an IP allowlist can stand in for the token. The stdio transport needs none of this.
|
|
485
|
+
|
|
486
|
+
One token covers one trust domain. Any client holding it can open, read or close any session by name, and `patchright_kill_all` closes them all. Clients that must not reach each other's browsers need separate servers, each with its own token. Two servers on one host share the daemon unless their `PATCHRIGHT_CLI_MCP_DAEMON_PORT` values differ, and they always share saved profiles in `~/.patchright-cli/profiles`. For full separation, run each server in its own container or as a different OS user.
|
|
487
|
+
|
|
488
|
+
File paths passed to MCP tools (uploads, state and cookie files, screenshot/PDF/snapshot output, profile directories) must be absolute and beneath the MCP root, so a client cannot reach arbitrary host files. Outputs without a path land in `<root>/output/`, which `browser_delete_output` can clean up. `patchright_eval` and `patchright_run_code` still run arbitrary JavaScript in the page, so protect HTTP deployments regardless. The MCP server never loads a `.patchright-cli/config.json`, since clients can write files beneath the root; tool arguments carry every option.
|
|
489
|
+
|
|
490
|
+
URLs passed to `patchright_open`, `patchright_goto` and `patchright_tab_new` must be `http`, `https` or `about:blank`, so the browser cannot read local files through `file://` (allowed again with `PATCHRIGHT_CLI_MCP_UNRESTRICTED_FILESYSTEM=1`). The server sends `--web-only` with every call, so the daemon holds the navigations it starts itself to the same rule (a link `patchright_click` follows, `patchright_go_back`, `patchright_go_forward`), also in a session you opened with the plain CLI, whose own commands stay unrestricted. A client can still read whatever such a session already shows, a `file://` page included, so to keep your own CLI sessions out of reach give the server its own daemon with `PATCHRIGHT_CLI_MCP_DAEMON_PORT`. Sessions the server opens keep the option, and the daemon sends a frame in them that still lands on another scheme back to `about:blank`. The browser also stays off `169.254.0.0/16` and `fe80::/10`, where cloud metadata services hand out credentials: `patchright_open`, `patchright_goto`, `patchright_tab_new` and `patchright_attach` refuse those hosts (the proxy too), and a frame that still lands there through a redirect or a script is sent back to `about:blank`. Set `PATCHRIGHT_CLI_MCP_BLOCK_NETWORKS=private` to refuse every address that is not globally routable as well (loopback, RFC 1918, CGNAT, unique local), or `none` to turn the check off. It never intercepts requests, so the HTTP cache stays on, and in `attach --context=host` sessions it only checks URLs before navigating, leaving the Host's other pages alone. That also means it is not a hard boundary: images, `fetch()` and WebSockets are not checked (CORS still hides their responses from the page), and a DNS server that answers differently each time can get past it. For that, restrict the container's or host's outbound traffic.
|
|
397
491
|
|
|
398
492
|
| Variable | Default | Effect |
|
|
399
493
|
|---|---|---|
|
|
@@ -402,6 +496,9 @@ File paths passed to MCP tools (uploads, state and cookie files, screenshot/PDF/
|
|
|
402
496
|
| `PATCHRIGHT_CLI_MCP_UNRESTRICTED_FILESYSTEM` | unset | `1` drops the path check and enables `patchright_install_skills` |
|
|
403
497
|
| `PATCHRIGHT_CLI_MCP_HEADLESS` | unset (headed) | `1` makes `patchright_open` default to headless, for hosts without a display |
|
|
404
498
|
| `PATCHRIGHT_CLI_MCP_DAEMON_PORT` | `9321` | Daemon port the tools talk to |
|
|
499
|
+
| `PATCHRIGHT_CLI_MCP_BLOCK_NETWORKS` | `link-local` | Address ranges the browser may not reach: `link-local`, `private` or `none` |
|
|
500
|
+
| `FASTMCP_HTTP_HOST_ORIGIN_PROTECTION` | unset (off) | `true` rejects requests whose Host is not loopback or allowed, and foreign Origins; without a token, HTTP mode needs it set to `true` or `false` |
|
|
501
|
+
| `FASTMCP_HTTP_ALLOWED_HOSTS` | unset | JSON array of extra hostnames the Host check accepts |
|
|
405
502
|
|
|
406
503
|
### Skill docs
|
|
407
504
|
|
|
@@ -79,6 +79,78 @@ python -m patchright install chromium
|
|
|
79
79
|
|
|
80
80
|
</details>
|
|
81
81
|
|
|
82
|
+
### Docker
|
|
83
|
+
|
|
84
|
+
A prebuilt image is available from GHCR:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
docker pull ghcr.io/ahaimk01/patchright-cli:latest
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
By default the container runs `patchright-cli` directly, so it can be used for normal CLI commands:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
docker run --rm ghcr.io/ahaimk01/patchright-cli:latest --help
|
|
94
|
+
docker run --rm ghcr.io/ahaimk01/patchright-cli:latest profile-list
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Browser sessions live inside the container, so for a sequence of standalone CLI commands keep the same container running:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
docker run --rm -it \
|
|
101
|
+
--name patchright-cli \
|
|
102
|
+
-v patchright-profiles:/root/.patchright-cli \
|
|
103
|
+
--entrypoint /bin/sh \
|
|
104
|
+
ghcr.io/ahaimk01/patchright-cli:latest
|
|
105
|
+
|
|
106
|
+
uv run patchright-cli open https://example.com --headless
|
|
107
|
+
uv run patchright-cli snapshot
|
|
108
|
+
uv run patchright-cli close
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
For MCP over stdio, set `PATCHRIGHT_CLI_MODE=mcp-stdio` and keep stdin attached. The volumes keep logins and output after the container exits:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
docker run --rm -i \
|
|
115
|
+
-e PATCHRIGHT_CLI_MODE=mcp-stdio \
|
|
116
|
+
-v ./patchright:/patchright \
|
|
117
|
+
-v patchright-profiles:/root/.patchright-cli \
|
|
118
|
+
ghcr.io/ahaimk01/patchright-cli:latest
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
For a long-running MCP server over Streamable HTTP, set `PATCHRIGHT_CLI_MODE=mcp-http`:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
docker run -d \
|
|
125
|
+
--name patchright-mcp \
|
|
126
|
+
-p 127.0.0.1:9090:9090 \
|
|
127
|
+
-e PATCHRIGHT_CLI_MODE=mcp-http \
|
|
128
|
+
-e PATCHRIGHT_CLI_MCP_TOKEN=replace-with-a-long-random-token \
|
|
129
|
+
-v ./patchright:/patchright \
|
|
130
|
+
-v patchright-profiles:/root/.patchright-cli \
|
|
131
|
+
ghcr.io/ahaimk01/patchright-cli:latest
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The HTTP MCP endpoint is then available at `http://localhost:9090/mcp`. The MCP server defaults to headless mode in the image and uses `/patchright` as its filesystem root. CLI commands inside the container still need `--headless`, since there is no display.
|
|
135
|
+
|
|
136
|
+
An example `compose.yml` is included in the repository:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
PATCHRIGHT_CLI_MCP_TOKEN=replace-with-a-long-random-token docker compose up -d
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Both examples publish the port on `127.0.0.1` only, but that alone doesn't stop a web page in your browser from reaching the server through DNS rebinding, so `mcp-http` refuses to start unless you pick one of three defences (see [MCP Server](#mcp-server) for details): set `PATCHRIGHT_CLI_MCP_TOKEN`; set `-e FASTMCP_HTTP_HOST_ORIGIN_PROTECTION=true` so requests must be addressed to `localhost`, `127.0.0.1` or `::1`, listing any other hostname or IP (a LAN address, a Compose service name) as a JSON array, for example `-e FASTMCP_HTTP_ALLOWED_HOSTS='["patchright-mcp", "192.168.1.20"]'`; or set `-e FASTMCP_HTTP_HOST_ORIGIN_PROTECTION=false` on purpose. A plain `docker run -e PATCHRIGHT_CLI_MODE=mcp-http` with neither variable exits with an error. The example `compose.yml` sets `FASTMCP_HTTP_HOST_ORIGIN_PROTECTION: "false"` so reverse-proxy and MCP hub setups keep working, which makes the token optional there: leave it out if your MCP client can't send an `Authorization` header. A reverse proxy with auth or an IP allowlist can stand in for the token. If you publish the port beyond loopback, protect it one of these ways: the endpoint gives full control of the browser and its saved logins.
|
|
143
|
+
|
|
144
|
+
### Chrome sandbox
|
|
145
|
+
|
|
146
|
+
Patchright starts Chrome with `--no-sandbox`, so by default no Chrome process is sandboxed (`chrome://sandbox` lists every one as "Not Sandboxed", on Windows too). Set `PATCHRIGHT_CLI_CHROME_SANDBOX=1` to launch Chrome with its sandbox instead. It is off by default.
|
|
147
|
+
|
|
148
|
+
The variable is read by the daemon, not by each CLI command. The CLI passes its environment on when it starts the daemon, so set it before the first `open`; a daemon that is already running keeps the setting it started with until you restart it (`patchright-cli kill-all`).
|
|
149
|
+
|
|
150
|
+
In Docker the image runs as root, and Chrome will not start as root with its sandbox on, so the switch also needs a non-root user (`docker run --user ...` with a writable `HOME`). Even then, Docker's default seccomp profile blocks the namespaces Chrome's Linux sandbox needs: measured on a GitHub Actions runner, as uid 1000 with the switch on, Chrome exits at launch ("Failed to move to new namespace ... Operation not permitted"). Leave the switch off in Docker; the image runs Chrome without its sandbox.
|
|
151
|
+
|
|
152
|
+
How the sandbox affects bot detection has not been measured.
|
|
153
|
+
|
|
82
154
|
---
|
|
83
155
|
|
|
84
156
|
## Quick Start
|
|
@@ -118,6 +190,15 @@ graph LR
|
|
|
118
190
|
| **CLI** (`cli.py`) | Thin client — connects, sends command, prints result, disconnects. Browser stays open. |
|
|
119
191
|
| **Snapshot** (`snapshot.py`) | Playwright `aria_snapshot()` accessibility tree, annotated with sequential `[ref=eN]` handles for element targeting. |
|
|
120
192
|
|
|
193
|
+
### Daemon token
|
|
194
|
+
|
|
195
|
+
The daemon only listens on `127.0.0.1`, but any local process can reach that port, so every message must carry a secret token. On startup the daemon writes a fresh random token to `~/.patchright-cli/daemon-<port>.token` (one file per port, so daemons on different ports coexist), before it starts listening, and deletes the file on a clean shutdown. The CLI (and the MCP server, which runs the CLI) reads the file for its `--port` and sends the token with each command; a message with a missing or wrong token is rejected and nothing runs.
|
|
196
|
+
|
|
197
|
+
- On Linux/macOS the file is created with mode `0600`. On Windows, POSIX modes do not apply, so the file is created with its own protected ACL: one full-access entry for your user account and nothing inherited from `~/.patchright-cli`, whatever that directory grants. If the daemon cannot create the file that way it does not start.
|
|
198
|
+
- A daemon holds `~/.patchright-cli/daemon-<port>.lock` while it runs, so only one daemon per port ever writes that port's token; a second daemon started on the same port exits without touching it. The lock file stays after shutdown and is harmless (the OS releases the lock when the process exits).
|
|
199
|
+
- Run the CLI as the same user, with the same home directory, as the daemon. In Docker the MCP server and daemon share a user and `HOME`, so this holds.
|
|
200
|
+
- An older CLI talking to a newer daemon gets an error saying the daemon requires an auth token: upgrade, or stop the daemon (`patchright-cli kill-all` from the newer install, or end the process). A newer CLI talking to an older daemon finds no token file and sends none, which the older daemon accepts.
|
|
201
|
+
|
|
121
202
|
---
|
|
122
203
|
|
|
123
204
|
<details>
|
|
@@ -349,15 +430,27 @@ pip install 'patchright-cli[mcp]'
|
|
|
349
430
|
# stdio (default)
|
|
350
431
|
patchright-cli mcp
|
|
351
432
|
|
|
352
|
-
# Streamable HTTP
|
|
353
|
-
patchright-cli mcp --http --host=127.0.0.1 --port=8000
|
|
433
|
+
# Streamable HTTP (needs a token or an explicit Host/Origin choice, see below)
|
|
434
|
+
PATCHRIGHT_CLI_MCP_TOKEN=replace-with-a-long-random-token patchright-cli mcp --http --host=127.0.0.1 --port=8000
|
|
354
435
|
```
|
|
355
436
|
|
|
356
437
|

|
|
357
438
|
|
|
358
439
|
The HTTP endpoint is `/mcp`. Set `PATCHRIGHT_CLI_MCP_TOKEN` to require `Authorization: Bearer <token>` on every request, and always set it before binding to `0.0.0.0`.
|
|
359
440
|
|
|
360
|
-
|
|
441
|
+
FastMCP's Host and Origin checks are off by default, so binding to `127.0.0.1` alone does not stop a web page from reaching a server without a token through DNS rebinding. `patchright-cli mcp --http` therefore refuses to start, whatever the bind host, unless `PATCHRIGHT_CLI_MCP_TOKEN` is set or `FASTMCP_HTTP_HOST_ORIGIN_PROTECTION` is set to a value FastMCP accepts (an empty value counts as unset). Pick one:
|
|
442
|
+
|
|
443
|
+
- Set `PATCHRIGHT_CLI_MCP_TOKEN`.
|
|
444
|
+
- Set `FASTMCP_HTTP_HOST_ORIGIN_PROTECTION=true` to accept only requests addressed to `localhost`, `127.0.0.1` or `::1` (421 otherwise) and reject foreign `Origin` headers (403). To allow another hostname or IP, list it as a JSON array, for example `FASTMCP_HTTP_ALLOWED_HOSTS='["mcp.lan"]'`.
|
|
445
|
+
- Set `FASTMCP_HTTP_HOST_ORIGIN_PROTECTION=false` on purpose, for example behind a reverse proxy with auth or an IP allowlist.
|
|
446
|
+
|
|
447
|
+
A reverse proxy with auth or an IP allowlist can stand in for the token. The stdio transport needs none of this.
|
|
448
|
+
|
|
449
|
+
One token covers one trust domain. Any client holding it can open, read or close any session by name, and `patchright_kill_all` closes them all. Clients that must not reach each other's browsers need separate servers, each with its own token. Two servers on one host share the daemon unless their `PATCHRIGHT_CLI_MCP_DAEMON_PORT` values differ, and they always share saved profiles in `~/.patchright-cli/profiles`. For full separation, run each server in its own container or as a different OS user.
|
|
450
|
+
|
|
451
|
+
File paths passed to MCP tools (uploads, state and cookie files, screenshot/PDF/snapshot output, profile directories) must be absolute and beneath the MCP root, so a client cannot reach arbitrary host files. Outputs without a path land in `<root>/output/`, which `browser_delete_output` can clean up. `patchright_eval` and `patchright_run_code` still run arbitrary JavaScript in the page, so protect HTTP deployments regardless. The MCP server never loads a `.patchright-cli/config.json`, since clients can write files beneath the root; tool arguments carry every option.
|
|
452
|
+
|
|
453
|
+
URLs passed to `patchright_open`, `patchright_goto` and `patchright_tab_new` must be `http`, `https` or `about:blank`, so the browser cannot read local files through `file://` (allowed again with `PATCHRIGHT_CLI_MCP_UNRESTRICTED_FILESYSTEM=1`). The server sends `--web-only` with every call, so the daemon holds the navigations it starts itself to the same rule (a link `patchright_click` follows, `patchright_go_back`, `patchright_go_forward`), also in a session you opened with the plain CLI, whose own commands stay unrestricted. A client can still read whatever such a session already shows, a `file://` page included, so to keep your own CLI sessions out of reach give the server its own daemon with `PATCHRIGHT_CLI_MCP_DAEMON_PORT`. Sessions the server opens keep the option, and the daemon sends a frame in them that still lands on another scheme back to `about:blank`. The browser also stays off `169.254.0.0/16` and `fe80::/10`, where cloud metadata services hand out credentials: `patchright_open`, `patchright_goto`, `patchright_tab_new` and `patchright_attach` refuse those hosts (the proxy too), and a frame that still lands there through a redirect or a script is sent back to `about:blank`. Set `PATCHRIGHT_CLI_MCP_BLOCK_NETWORKS=private` to refuse every address that is not globally routable as well (loopback, RFC 1918, CGNAT, unique local), or `none` to turn the check off. It never intercepts requests, so the HTTP cache stays on, and in `attach --context=host` sessions it only checks URLs before navigating, leaving the Host's other pages alone. That also means it is not a hard boundary: images, `fetch()` and WebSockets are not checked (CORS still hides their responses from the page), and a DNS server that answers differently each time can get past it. For that, restrict the container's or host's outbound traffic.
|
|
361
454
|
|
|
362
455
|
| Variable | Default | Effect |
|
|
363
456
|
|---|---|---|
|
|
@@ -366,6 +459,9 @@ File paths passed to MCP tools (uploads, state and cookie files, screenshot/PDF/
|
|
|
366
459
|
| `PATCHRIGHT_CLI_MCP_UNRESTRICTED_FILESYSTEM` | unset | `1` drops the path check and enables `patchright_install_skills` |
|
|
367
460
|
| `PATCHRIGHT_CLI_MCP_HEADLESS` | unset (headed) | `1` makes `patchright_open` default to headless, for hosts without a display |
|
|
368
461
|
| `PATCHRIGHT_CLI_MCP_DAEMON_PORT` | `9321` | Daemon port the tools talk to |
|
|
462
|
+
| `PATCHRIGHT_CLI_MCP_BLOCK_NETWORKS` | `link-local` | Address ranges the browser may not reach: `link-local`, `private` or `none` |
|
|
463
|
+
| `FASTMCP_HTTP_HOST_ORIGIN_PROTECTION` | unset (off) | `true` rejects requests whose Host is not loopback or allowed, and foreign Origins; without a token, HTTP mode needs it set to `true` or `false` |
|
|
464
|
+
| `FASTMCP_HTTP_ALLOWED_HOSTS` | unset | JSON array of extra hostnames the Host check accepts |
|
|
369
465
|
|
|
370
466
|
### Skill docs
|
|
371
467
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
services:
|
|
2
|
+
patchright-mcp:
|
|
3
|
+
image: ghcr.io/ahaimk01/patchright-cli:latest
|
|
4
|
+
container_name: patchright-mcp
|
|
5
|
+
restart: unless-stopped
|
|
6
|
+
ports:
|
|
7
|
+
- "127.0.0.1:9090:9090"
|
|
8
|
+
environment:
|
|
9
|
+
PATCHRIGHT_CLI_MODE: mcp-http
|
|
10
|
+
PATCHRIGHT_CLI_MCP_HEADLESS: "1"
|
|
11
|
+
PATCHRIGHT_CLI_MCP_ROOT: /patchright
|
|
12
|
+
# HTTP mode refuses to start unless you pick a defence against DNS rebinding:
|
|
13
|
+
# (a) set PATCHRIGHT_CLI_MCP_TOKEN (clients send an Authorization header);
|
|
14
|
+
# (b) set FASTMCP_HTTP_HOST_ORIGIN_PROTECTION to "true", so only requests
|
|
15
|
+
# addressed to localhost, 127.0.0.1 or ::1 are accepted, and list any other
|
|
16
|
+
# name or IP (such as this service's name) in FASTMCP_HTTP_ALLOWED_HOSTS; or
|
|
17
|
+
# (c) leave it "false" on purpose. This file does (c) so reverse-proxy and MCP
|
|
18
|
+
# hub setups keep working: a reverse proxy with auth or an IP allowlist can
|
|
19
|
+
# stand in for the token. Leave the token unset if your client can't send one.
|
|
20
|
+
PATCHRIGHT_CLI_MCP_TOKEN: ${PATCHRIGHT_CLI_MCP_TOKEN:-}
|
|
21
|
+
FASTMCP_HTTP_HOST_ORIGIN_PROTECTION: "false"
|
|
22
|
+
# FASTMCP_HTTP_ALLOWED_HOSTS: '["patchright-mcp", "192.168.1.20"]'
|
|
23
|
+
# Set to 1 only if you intentionally want MCP tools to access paths
|
|
24
|
+
# outside PATCHRIGHT_CLI_MCP_ROOT.
|
|
25
|
+
# PATCHRIGHT_CLI_MCP_UNRESTRICTED_FILESYSTEM: "1"
|
|
26
|
+
# PATCHRIGHT_CLI_MCP_PORT: "9090"
|
|
27
|
+
volumes:
|
|
28
|
+
# Files made available to MCP tools, plus generated output under ./patchright/output.
|
|
29
|
+
- ./patchright:/patchright
|
|
30
|
+
# Persist named browser profiles and login state across container updates.
|
|
31
|
+
- patchright-profiles:/root/.patchright-cli
|
|
32
|
+
|
|
33
|
+
volumes:
|
|
34
|
+
patchright-profiles:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "patchright-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.9.0"
|
|
4
4
|
description = "Anti-detect browser automation CLI using Patchright (undetected Playwright fork)"
|
|
5
5
|
license = "Apache-2.0"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -27,6 +27,8 @@ dependencies = [
|
|
|
27
27
|
"pyyaml",
|
|
28
28
|
"click",
|
|
29
29
|
"aiohttp>=3.9",
|
|
30
|
+
# Windows ACL for the daemon token file
|
|
31
|
+
"pywin32>=306; sys_platform == 'win32'",
|
|
30
32
|
]
|
|
31
33
|
|
|
32
34
|
[project.urls]
|
|
@@ -55,20 +57,20 @@ markers = [
|
|
|
55
57
|
|
|
56
58
|
[project.optional-dependencies]
|
|
57
59
|
mcp = [
|
|
58
|
-
"fastmcp>=3.
|
|
60
|
+
"fastmcp>=3.4.3,<5",
|
|
59
61
|
]
|
|
60
62
|
dev = [
|
|
61
63
|
"pytest>=8.0",
|
|
62
64
|
"pytest-asyncio>=0.23",
|
|
63
65
|
"pre-commit",
|
|
64
|
-
"fastmcp>=3.
|
|
66
|
+
"fastmcp>=3.4.3,<5",
|
|
65
67
|
]
|
|
66
68
|
|
|
67
69
|
[dependency-groups]
|
|
68
70
|
dev = [
|
|
69
71
|
"pytest>=8.0",
|
|
70
72
|
"pytest-asyncio>=0.23",
|
|
71
|
-
"fastmcp>=3.
|
|
73
|
+
"fastmcp>=3.4.3,<5",
|
|
72
74
|
]
|
|
73
75
|
|
|
74
76
|
[tool.ruff]
|