patchright-cli 0.8.0__tar.gz → 0.9.1__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.
Files changed (62) hide show
  1. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/.claude-plugin/marketplace.json +1 -1
  2. patchright_cli-0.9.1/.github/dependabot.yml +10 -0
  3. patchright_cli-0.9.1/.github/workflows/ci.yml +96 -0
  4. patchright_cli-0.9.1/.github/workflows/publish-docker.yml +65 -0
  5. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/.github/workflows/publish.yml +8 -8
  6. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/CLAUDE.md +2 -2
  7. patchright_cli-0.9.1/Dockerfile +37 -0
  8. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/PKG-INFO +135 -6
  9. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/README.md +131 -3
  10. patchright_cli-0.9.1/compose.yml +34 -0
  11. patchright_cli-0.9.1/docs/daemon-protocol.md +102 -0
  12. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/pyproject.toml +6 -4
  13. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/skills/patchright-cli/SKILL.md +34 -2
  14. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/src/patchright_cli/__init__.py +1 -1
  15. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/src/patchright_cli/cli.py +62 -5
  16. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/src/patchright_cli/daemon.py +628 -32
  17. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/src/patchright_cli/mcp_server.py +205 -32
  18. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/src/patchright_cli/ref_registry.py +35 -12
  19. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/tests/test_cli_config.py +18 -0
  20. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/tests/test_daemon_handlers.py +4 -0
  21. patchright_cli-0.9.1/tests/test_daemon_info.py +53 -0
  22. patchright_cli-0.9.1/tests/test_daemon_read.py +118 -0
  23. patchright_cli-0.9.1/tests/test_daemon_token.py +302 -0
  24. patchright_cli-0.9.1/tests/test_docker.py +112 -0
  25. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/tests/test_e2e.py +2 -2
  26. patchright_cli-0.9.1/tests/test_emulation.py +203 -0
  27. patchright_cli-0.9.1/tests/test_mcp.py +445 -0
  28. patchright_cli-0.9.1/tests/test_navigation_policy.py +361 -0
  29. patchright_cli-0.9.1/tests/test_network_guard.py +192 -0
  30. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/tests/test_profiles.py +13 -0
  31. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/tests/test_ref_registry.py +87 -1
  32. patchright_cli-0.9.1/tests/test_supply_chain.py +65 -0
  33. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/uv.lock +6 -4
  34. patchright_cli-0.8.0/.github/workflows/ci.yml +0 -34
  35. patchright_cli-0.8.0/tests/test_mcp.py +0 -172
  36. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/.claude-plugin/skills/browser-automation.json +0 -0
  37. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/.gitignore +0 -0
  38. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/.pre-commit-config.yaml +0 -0
  39. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/CONTRIBUTING.md +0 -0
  40. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/GLOSSARY.md +0 -0
  41. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/LICENSE +0 -0
  42. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/Makefile +0 -0
  43. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/docs/installation.md +0 -0
  44. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/docs/mcp-architecture.svg +0 -0
  45. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/patchright-cli +0 -0
  46. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/patchright-cli.cmd +0 -0
  47. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/skills/patchright-cli/references/request-mocking.md +0 -0
  48. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/skills/patchright-cli/references/running-code.md +0 -0
  49. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/skills/patchright-cli/references/session-management.md +0 -0
  50. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/skills/patchright-cli/references/snapshot-refs.md +0 -0
  51. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/skills/patchright-cli/references/storage-state.md +0 -0
  52. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/skills/patchright-cli/references/tracing.md +0 -0
  53. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/skills/patchright-cli/references/video-recording.md +0 -0
  54. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/src/patchright_cli/__main__.py +0 -0
  55. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/src/patchright_cli/dashboard.py +0 -0
  56. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/src/patchright_cli/snapshot.py +0 -0
  57. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/tests/__init__.py +0 -0
  58. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/tests/fixture.html +0 -0
  59. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/tests/test_annotate.py +0 -0
  60. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/tests/test_attach.py +0 -0
  61. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/tests/test_packaging.py +0 -0
  62. {patchright_cli-0.8.0 → patchright_cli-0.9.1}/tests/test_tabs.py +0 -0
@@ -5,7 +5,7 @@
5
5
  },
6
6
  "metadata": {
7
7
  "description": "Anti-detect browser automation CLI & Skills for AI agents, powered by Patchright",
8
- "version": "0.8.0"
8
+ "version": "0.9.1"
9
9
  },
10
10
  "plugins": [
11
11
  {
@@ -0,0 +1,10 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: docker
4
+ directory: /
5
+ schedule:
6
+ interval: weekly
7
+ - package-ecosystem: github-actions
8
+ directory: /
9
+ schedule:
10
+ interval: weekly
@@ -0,0 +1,96 @@
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@v7
14
+ - uses: astral-sh/setup-uv@v7
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@v7
32
+ - uses: astral-sh/setup-uv@v7
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@v7
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
+ - uses: astral-sh/setup-uv@v7
82
+ with:
83
+ version: ">=0.7,<0.8"
84
+ python-version: "3.12"
85
+ - name: Image test suite
86
+ run: |
87
+ uv venv && uv pip install -e ".[dev]"
88
+ uv run pytest tests/test_docker.py -v
89
+ env:
90
+ PATCHRIGHT_CLI_DOCKER_IMAGE: patchright-cli:ci
91
+ - if: failure()
92
+ run: |
93
+ docker logs cli || true
94
+ docker logs mcp || true
95
+ docker logs mcp-open || true
96
+ 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@v7
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
@@ -15,10 +15,10 @@ jobs:
15
15
  build:
16
16
  runs-on: ubuntu-latest
17
17
  steps:
18
- - uses: actions/checkout@v4
18
+ - uses: actions/checkout@v7
19
19
 
20
20
  - name: Install uv
21
- uses: astral-sh/setup-uv@v5
21
+ uses: astral-sh/setup-uv@v7
22
22
 
23
23
  - name: Set up Python
24
24
  run: uv python install 3.12
@@ -27,7 +27,7 @@ jobs:
27
27
  run: uv build
28
28
 
29
29
  - name: Upload artifacts
30
- uses: actions/upload-artifact@v4
30
+ uses: actions/upload-artifact@v7
31
31
  with:
32
32
  name: dist
33
33
  path: dist/
@@ -40,13 +40,13 @@ jobs:
40
40
  id-token: write
41
41
  steps:
42
42
  - name: Download artifacts
43
- uses: actions/download-artifact@v4
43
+ uses: actions/download-artifact@v8
44
44
  with:
45
45
  name: dist
46
46
  path: dist/
47
47
 
48
48
  - name: Install uv
49
- uses: astral-sh/setup-uv@v5
49
+ uses: astral-sh/setup-uv@v7
50
50
 
51
51
  - name: Publish to PyPI
52
52
  run: uv publish
@@ -59,16 +59,16 @@ jobs:
59
59
  permissions:
60
60
  contents: write
61
61
  steps:
62
- - uses: actions/checkout@v4
62
+ - uses: actions/checkout@v7
63
63
 
64
64
  - name: Download artifacts
65
- uses: actions/download-artifact@v4
65
+ uses: actions/download-artifact@v8
66
66
  with:
67
67
  name: dist
68
68
  path: dist/
69
69
 
70
70
  - name: Create GitHub Release
71
- uses: softprops/action-gh-release@v2
71
+ uses: softprops/action-gh-release@v3
72
72
  with:
73
73
  files: dist/*
74
74
  generate_release_notes: 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 must be added to both `COMMANDS_HELP` dict and the `handle_command()` if/elif chain in daemon.py
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:af22a52f5eafef3d78e551599d405c02df61919d18028f0cea8eda93bc9986d0 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.8.0
3
+ Version: 0.9.1
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.0; extra == 'dev'
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.0; extra == 'mcp'
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
@@ -138,6 +211,16 @@ patchright-cli screenshot
138
211
  patchright-cli close
139
212
  ```
140
213
 
214
+ Quote URLs that contain `&`; unquoted, bash, PowerShell and cmd.exe all split the command there. On Windows two cases need more than quotes: in cmd.exe without quotes, escape it as `^&` (inside quotes the `^` stays in the URL, so use one or the other), and when PowerShell runs a `.cmd`/`.bat` wrapper such as the repo's `patchright-cli.cmd`, PowerShell drops the quotes and cmd.exe splits at `&`, so pass the rest verbatim with `--%`:
215
+
216
+ ```
217
+ patchright-cli goto "https://example.com/?a=1&b=2" # any shell, patchright-cli.exe
218
+ patchright-cli goto https://example.com/?a=1^&b=2 # cmd.exe, unquoted
219
+ .\patchright-cli.cmd --% goto "https://example.com/?a=1&b=2" # PowerShell, .cmd wrapper
220
+ ```
221
+
222
+ Don't use `--%` with the installed `patchright-cli.exe`: in PowerShell 7 the quotes after it reach the CLI and end up in the URL.
223
+
141
224
  ---
142
225
 
143
226
  ## Architecture
@@ -154,6 +237,21 @@ graph LR
154
237
  | **CLI** (`cli.py`) | Thin client — connects, sends command, prints result, disconnects. Browser stays open. |
155
238
  | **Snapshot** (`snapshot.py`) | Playwright `aria_snapshot()` accessibility tree, annotated with sequential `[ref=eN]` handles for element targeting. |
156
239
 
240
+ ### Daemon token
241
+
242
+ 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.
243
+
244
+ - 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.
245
+ - 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).
246
+ - 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.
247
+ - 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.
248
+
249
+ ### Daemon client protocol
250
+
251
+ Third-party clients can use the daemon TCP interface directly. The
252
+ [client protocol reference](docs/daemon-protocol.md) defines discovery,
253
+ authentication, message framing, replies, and compatibility rules.
254
+
157
255
  ---
158
256
 
159
257
  <details>
@@ -240,6 +338,22 @@ patchright-cli upload ./file.pdf e5 # Upload to specific input
240
338
  patchright-cli resize 1920 1080 # Resize viewport
241
339
  ```
242
340
 
341
+ ### Emulation
342
+ ```bash
343
+ patchright-cli set-color-scheme dark # dark | light | no-preference
344
+ patchright-cli clear-color-scheme
345
+ patchright-cli set-reduced-motion reduce # reduce | no-preference
346
+ patchright-cli clear-reduced-motion
347
+ patchright-cli set-forced-colors active # active | none
348
+ patchright-cli clear-forced-colors
349
+ patchright-cli set-contrast more # more | no-preference
350
+ patchright-cli clear-contrast
351
+ patchright-cli set-media print # screen | print
352
+ patchright-cli clear-media
353
+ ```
354
+
355
+ Each setting holds for the whole session, including tabs and popups opened later; `clear-*` drops just that one, back to the browser default. A popup gets the setting just after it opens, so a script that reads media queries the instant the popup is created may still see the default.
356
+
243
357
  ### Tabs
244
358
  ```bash
245
359
  patchright-cli tab-list
@@ -385,15 +499,27 @@ pip install 'patchright-cli[mcp]'
385
499
  # stdio (default)
386
500
  patchright-cli mcp
387
501
 
388
- # Streamable HTTP
389
- patchright-cli mcp --http --host=127.0.0.1 --port=8000
502
+ # Streamable HTTP (needs a token or an explicit Host/Origin choice, see below)
503
+ PATCHRIGHT_CLI_MCP_TOKEN=replace-with-a-long-random-token patchright-cli mcp --http --host=127.0.0.1 --port=8000
390
504
  ```
391
505
 
392
506
  ![How a remote MCP call reaches Chrome](docs/mcp-architecture.svg)
393
507
 
394
508
  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
509
 
396
- 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.
510
+ 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:
511
+
512
+ - Set `PATCHRIGHT_CLI_MCP_TOKEN`.
513
+ - 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"]'`.
514
+ - Set `FASTMCP_HTTP_HOST_ORIGIN_PROTECTION=false` on purpose, for example behind a reverse proxy with auth or an IP allowlist.
515
+
516
+ A reverse proxy with auth or an IP allowlist can stand in for the token. The stdio transport needs none of this.
517
+
518
+ 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.
519
+
520
+ 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.
521
+
522
+ 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
523
 
398
524
  | Variable | Default | Effect |
399
525
  |---|---|---|
@@ -402,6 +528,9 @@ File paths passed to MCP tools (uploads, state and cookie files, screenshot/PDF/
402
528
  | `PATCHRIGHT_CLI_MCP_UNRESTRICTED_FILESYSTEM` | unset | `1` drops the path check and enables `patchright_install_skills` |
403
529
  | `PATCHRIGHT_CLI_MCP_HEADLESS` | unset (headed) | `1` makes `patchright_open` default to headless, for hosts without a display |
404
530
  | `PATCHRIGHT_CLI_MCP_DAEMON_PORT` | `9321` | Daemon port the tools talk to |
531
+ | `PATCHRIGHT_CLI_MCP_BLOCK_NETWORKS` | `link-local` | Address ranges the browser may not reach: `link-local`, `private` or `none` |
532
+ | `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` |
533
+ | `FASTMCP_HTTP_ALLOWED_HOSTS` | unset | JSON array of extra hostnames the Host check accepts |
405
534
 
406
535
  ### Skill docs
407
536