browserr-mcp 0.7.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.
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/target/
|
|
2
|
+
engine/target/
|
|
3
|
+
chrome/target/
|
|
4
|
+
Cargo.lock
|
|
5
|
+
*.tmp
|
|
6
|
+
dist/
|
|
7
|
+
|
|
8
|
+
engine*.log
|
|
9
|
+
chrome-boot-test.log
|
|
10
|
+
|
|
11
|
+
*.zip
|
|
12
|
+
*.tar.gz
|
|
13
|
+
|
|
14
|
+
chrome-driver-test.log
|
|
15
|
+
|
|
16
|
+
tools/__pycache__/
|
|
17
|
+
__pycache__/
|
|
18
|
+
mcp-crash.log
|
|
19
|
+
mcp-wrap.log
|
|
20
|
+
tools/mcp-shots/
|
|
21
|
+
tools/mcp-evidence/
|
|
22
|
+
|
|
23
|
+
# icon build artifacts: sources live in scripts/, outputs are regenerable
|
|
24
|
+
scripts/node_modules/
|
|
25
|
+
scripts/package-lock.json
|
|
26
|
+
scripts/icon-*.png
|
|
27
|
+
scripts/extracted-exe-icon.png
|
|
28
|
+
scripts/ci-*.log
|
|
29
|
+
|
|
30
|
+
# terminal test captures and caches
|
|
31
|
+
tests/__pycache__/
|
|
32
|
+
term/__pycache__/
|
|
33
|
+
term/*.log
|
|
34
|
+
term/native-frame.png
|
|
35
|
+
|
|
36
|
+
# one-off smoke probes and binary captures (reusable harnesses are committed)
|
|
37
|
+
.smoke/gh-logs/
|
|
38
|
+
.smoke/*.bin
|
|
39
|
+
.smoke/sixel_probe.sh
|
|
40
|
+
.smoke/tmux_query_test.py
|
|
41
|
+
.smoke/tmux_decision_probe.py
|
|
42
|
+
.smoke/tmux_env_probe.py
|
|
43
|
+
.smoke/tmux_img_probe.sh
|
|
44
|
+
browserr-term-engine.log
|
|
45
|
+
browserr-term-geometry.log
|
|
46
|
+
tools/*.html
|
|
47
|
+
tools/*.bin
|
|
48
|
+
tools/*-shot.png
|
|
49
|
+
tools/filepicker-picked.txt
|
|
50
|
+
tools/mcp-picker.txt
|
|
51
|
+
tools/mcp-engine.log
|
|
52
|
+
tools/net-test.html
|
|
53
|
+
tools/net-api.json
|
|
54
|
+
tools/net-xhr.txt
|
|
55
|
+
tools/frames-test.html
|
|
56
|
+
tools/frames-child.html
|
|
57
|
+
tools/intercept-test.html
|
|
58
|
+
tools/form-test.html
|
|
59
|
+
tools/form-flow.html
|
|
60
|
+
tools/annotate-test.html
|
|
61
|
+
tools/annotate-shot.png
|
|
62
|
+
tools/annotate-preview.png
|
|
63
|
+
tools/handoff-test.html
|
|
64
|
+
tools/handoff-clean.html
|
|
65
|
+
tools/pay-test.html
|
|
66
|
+
tools/sw-render.html
|
|
67
|
+
tools/compat-probe.html
|
|
68
|
+
tools/compat-probe2.html
|
|
69
|
+
tools/probe_mod.js
|
|
70
|
+
tools/probe_run.py
|
|
71
|
+
tools/probe2_run.py
|
|
72
|
+
|
|
73
|
+
tools/trust-evidence/
|
|
74
|
+
tools/mcp-profiles/
|
|
75
|
+
tools/mcp-evidence/
|
|
76
|
+
tools/mcp-shots/
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: browserr-mcp
|
|
3
|
+
Version: 0.7.1
|
|
4
|
+
Summary: MCP server for Browserr, the Servo-based agent browser. One command; the platform engine binary is downloaded and digest-verified on first run.
|
|
5
|
+
Project-URL: Homepage, https://browserr.dev/
|
|
6
|
+
Project-URL: Repository, https://github.com/Omodaka9375/browserr
|
|
7
|
+
Project-URL: Changelog, https://github.com/Omodaka9375/browserr/blob/main/CHANGELOG.md
|
|
8
|
+
Project-URL: Issues, https://github.com/Omodaka9375/browserr/issues
|
|
9
|
+
Author: Omodaka9375
|
|
10
|
+
License-Expression: Apache-2.0
|
|
11
|
+
Keywords: agents,ai,browser,browser-automation,headless,mcp,model-context-protocol,servo
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: MacOS
|
|
15
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Rust
|
|
19
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
|
|
20
|
+
Classifier: Topic :: Software Development :: Testing
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# browserr-mcp
|
|
25
|
+
|
|
26
|
+
[MCP](https://modelcontextprotocol.io) server for
|
|
27
|
+
[Browserr](https://browserr.dev) — the Servo-based browser engine built
|
|
28
|
+
for AI agents. One command; the platform engine binary is downloaded and
|
|
29
|
+
sha256-verified on first run.
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"mcpServers": {
|
|
34
|
+
"browserr": {
|
|
35
|
+
"command": "uvx",
|
|
36
|
+
"args": ["browserr-mcp"]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
or, installed directly:
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"mcpServers": {
|
|
47
|
+
"browserr": { "command": "browserr-mcp" }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## What you get
|
|
53
|
+
|
|
54
|
+
42 tools covering navigation, token-cheap page reads, structured page
|
|
55
|
+
inventories with stable element refs (`e7`), type-aware form filling,
|
|
56
|
+
annotated screenshots, fail-closed request interception, HTTP-auth and
|
|
57
|
+
file-picker events, evidence capture, and human handoff (pause the
|
|
58
|
+
page, let a person handle the CAPTCHA, resume).
|
|
59
|
+
|
|
60
|
+
The engine is a single Rust binary (Servo 0.8): CPU rendering by
|
|
61
|
+
default, EasyList adblock built in, no Chromium, no CDP, no driver
|
|
62
|
+
downloads. It runs on Windows, Linux (x64/ARM64) and macOS.
|
|
63
|
+
|
|
64
|
+
Browserr fails closed by design: uncertain requests are blocked, not
|
|
65
|
+
guessed; credentials are never logged; every run leaves timestamped
|
|
66
|
+
evidence.
|
|
67
|
+
|
|
68
|
+
## Environment variables
|
|
69
|
+
|
|
70
|
+
| Variable | Default | Purpose |
|
|
71
|
+
| --- | --- | --- |
|
|
72
|
+
| `BROWSERR_VERSION` | latest release | pin a release tag, e.g. `v0.7.1` |
|
|
73
|
+
| `BROWSERR_HOME` | `~/.browserr/versions/<version>` | install dir; an existing unpack with the engine binary skips the download |
|
|
74
|
+
|
|
75
|
+
## First run
|
|
76
|
+
|
|
77
|
+
The bootstrap queries the official GitHub release API, downloads the
|
|
78
|
+
platform asset, verifies it against the sha256 digest GitHub reports
|
|
79
|
+
for that asset, and unpacks it under `~/.browserr/versions/<version>`.
|
|
80
|
+
A missing digest is a hard stop — the digest is the corruption guard;
|
|
81
|
+
TLS only covers the transport.
|
|
82
|
+
|
|
83
|
+
Prefer to install ahead of time?
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
uvx browserr-install
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Links
|
|
90
|
+
|
|
91
|
+
- Browserr: https://browserr.dev/
|
|
92
|
+
- Source: https://github.com/Omodaka9375/browserr
|
|
93
|
+
- Changelog: https://github.com/Omodaka9375/browserr/blob/main/CHANGELOG.md
|
|
94
|
+
|
|
95
|
+
Apache-2.0. Engine and Python tooling are stdlib-only; this bootstrap
|
|
96
|
+
adds no third-party Python dependencies.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# browserr-mcp
|
|
2
|
+
|
|
3
|
+
[MCP](https://modelcontextprotocol.io) server for
|
|
4
|
+
[Browserr](https://browserr.dev) — the Servo-based browser engine built
|
|
5
|
+
for AI agents. One command; the platform engine binary is downloaded and
|
|
6
|
+
sha256-verified on first run.
|
|
7
|
+
|
|
8
|
+
```json
|
|
9
|
+
{
|
|
10
|
+
"mcpServers": {
|
|
11
|
+
"browserr": {
|
|
12
|
+
"command": "uvx",
|
|
13
|
+
"args": ["browserr-mcp"]
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
or, installed directly:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"mcpServers": {
|
|
24
|
+
"browserr": { "command": "browserr-mcp" }
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## What you get
|
|
30
|
+
|
|
31
|
+
42 tools covering navigation, token-cheap page reads, structured page
|
|
32
|
+
inventories with stable element refs (`e7`), type-aware form filling,
|
|
33
|
+
annotated screenshots, fail-closed request interception, HTTP-auth and
|
|
34
|
+
file-picker events, evidence capture, and human handoff (pause the
|
|
35
|
+
page, let a person handle the CAPTCHA, resume).
|
|
36
|
+
|
|
37
|
+
The engine is a single Rust binary (Servo 0.8): CPU rendering by
|
|
38
|
+
default, EasyList adblock built in, no Chromium, no CDP, no driver
|
|
39
|
+
downloads. It runs on Windows, Linux (x64/ARM64) and macOS.
|
|
40
|
+
|
|
41
|
+
Browserr fails closed by design: uncertain requests are blocked, not
|
|
42
|
+
guessed; credentials are never logged; every run leaves timestamped
|
|
43
|
+
evidence.
|
|
44
|
+
|
|
45
|
+
## Environment variables
|
|
46
|
+
|
|
47
|
+
| Variable | Default | Purpose |
|
|
48
|
+
| --- | --- | --- |
|
|
49
|
+
| `BROWSERR_VERSION` | latest release | pin a release tag, e.g. `v0.7.1` |
|
|
50
|
+
| `BROWSERR_HOME` | `~/.browserr/versions/<version>` | install dir; an existing unpack with the engine binary skips the download |
|
|
51
|
+
|
|
52
|
+
## First run
|
|
53
|
+
|
|
54
|
+
The bootstrap queries the official GitHub release API, downloads the
|
|
55
|
+
platform asset, verifies it against the sha256 digest GitHub reports
|
|
56
|
+
for that asset, and unpacks it under `~/.browserr/versions/<version>`.
|
|
57
|
+
A missing digest is a hard stop — the digest is the corruption guard;
|
|
58
|
+
TLS only covers the transport.
|
|
59
|
+
|
|
60
|
+
Prefer to install ahead of time?
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
uvx browserr-install
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Links
|
|
67
|
+
|
|
68
|
+
- Browserr: https://browserr.dev/
|
|
69
|
+
- Source: https://github.com/Omodaka9375/browserr
|
|
70
|
+
- Changelog: https://github.com/Omodaka9375/browserr/blob/main/CHANGELOG.md
|
|
71
|
+
|
|
72
|
+
Apache-2.0. Engine and Python tooling are stdlib-only; this bootstrap
|
|
73
|
+
adds no third-party Python dependencies.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "browserr-mcp"
|
|
7
|
+
version = "0.7.1"
|
|
8
|
+
description = "MCP server for Browserr, the Servo-based agent browser. One command; the platform engine binary is downloaded and digest-verified on first run."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "Apache-2.0"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [{ name = "Omodaka9375" }]
|
|
13
|
+
keywords = [
|
|
14
|
+
"mcp",
|
|
15
|
+
"model-context-protocol",
|
|
16
|
+
"browser",
|
|
17
|
+
"browser-automation",
|
|
18
|
+
"servo",
|
|
19
|
+
"ai",
|
|
20
|
+
"agents",
|
|
21
|
+
"headless",
|
|
22
|
+
]
|
|
23
|
+
classifiers = [
|
|
24
|
+
"Development Status :: 4 - Beta",
|
|
25
|
+
"Intended Audience :: Developers",
|
|
26
|
+
"Operating System :: Microsoft :: Windows",
|
|
27
|
+
"Operating System :: POSIX :: Linux",
|
|
28
|
+
"Operating System :: MacOS",
|
|
29
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
30
|
+
"Programming Language :: Rust",
|
|
31
|
+
"Topic :: Internet :: WWW/HTTP :: Browsers",
|
|
32
|
+
"Topic :: Software Development :: Testing",
|
|
33
|
+
]
|
|
34
|
+
dependencies = []
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Homepage = "https://browserr.dev/"
|
|
38
|
+
Repository = "https://github.com/Omodaka9375/browserr"
|
|
39
|
+
Changelog = "https://github.com/Omodaka9375/browserr/blob/main/CHANGELOG.md"
|
|
40
|
+
Issues = "https://github.com/Omodaka9375/browserr/issues"
|
|
41
|
+
|
|
42
|
+
[project.scripts]
|
|
43
|
+
browserr-mcp = "browserr_bootstrap:main"
|
|
44
|
+
browserr-install = "browserr_bootstrap:install_main"
|
|
45
|
+
|
|
46
|
+
[tool.hatch.build.targets.wheel]
|
|
47
|
+
packages = ["src/browserr_bootstrap"]
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""Browserr MCP bootstrap: one-command install of the agent browser.
|
|
2
|
+
|
|
3
|
+
The MCP server itself is stdlib-only Python shipped in the Browserr
|
|
4
|
+
release archives; this package exists so an MCP client can be configured
|
|
5
|
+
with a single command (`browserr-mcp` / `uvx browserr-mcp`) and the
|
|
6
|
+
platform engine binary is fetched and verified automatically on first
|
|
7
|
+
run.
|
|
8
|
+
|
|
9
|
+
How it works (all stdlib):
|
|
10
|
+
|
|
11
|
+
1. Resolve the install dir:
|
|
12
|
+
- $BROWSERR_HOME if set (explicit override),
|
|
13
|
+
- else ~/.browserr/versions/<version> (per-version, atomic updates).
|
|
14
|
+
2. If the engine binary is missing there, download the platform asset
|
|
15
|
+
from the official GitHub release, verify its sha256 against the
|
|
16
|
+
digest GitHub reports for the asset, and unpack it in place.
|
|
17
|
+
3. Exec browserr_mcp.py from the unpack in-process (runpy): it speaks
|
|
18
|
+
newline JSON-RPC on stdio and needs no third-party packages.
|
|
19
|
+
|
|
20
|
+
Fail-closed rules (the whole product is fail-closed; so is this):
|
|
21
|
+
- No digest published for the asset -> refuse to install (the digest is
|
|
22
|
+
the corruption/asset-swap guard; TLS only covers the transport).
|
|
23
|
+
- Non-GitHub download URL -> refuse (a broken API response cannot send
|
|
24
|
+
the download off-site).
|
|
25
|
+
- Unreasonably large asset -> refuse.
|
|
26
|
+
|
|
27
|
+
$BROWSERR_VERSION pins a release tag ("v0.7.1" or "0.7.1"); the default
|
|
28
|
+
is the latest release. $BROWSERR_HOME points at an existing unpack or
|
|
29
|
+
any directory to use as the install root (skips the download when the
|
|
30
|
+
engine binary is already there, e.g. a manually unpacked archive).
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import hashlib
|
|
36
|
+
import json
|
|
37
|
+
import os
|
|
38
|
+
import platform
|
|
39
|
+
import runpy
|
|
40
|
+
import shutil
|
|
41
|
+
import stat
|
|
42
|
+
import sys
|
|
43
|
+
import tempfile
|
|
44
|
+
import urllib.request
|
|
45
|
+
import zipfile
|
|
46
|
+
|
|
47
|
+
__version__ = "0.7.1"
|
|
48
|
+
|
|
49
|
+
REPO = "Omodaka9375/browserr"
|
|
50
|
+
API = "https://api.github.com"
|
|
51
|
+
MAX_ASSET_BYTES = 500 * 1024 * 1024 # sanity ceiling, matches update.ps1/sh
|
|
52
|
+
ALLOWED_HOSTS = {"github.com", "objects.githubusercontent.com"}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _platform_asset(version: str) -> str:
|
|
56
|
+
machine = platform.machine().lower()
|
|
57
|
+
system = platform.system().lower()
|
|
58
|
+
if system == "windows":
|
|
59
|
+
arch = "x64" if machine in ("amd64", "x86_64") else None
|
|
60
|
+
ext = "zip"
|
|
61
|
+
elif system == "linux":
|
|
62
|
+
arch = {"x86_64": "x64", "amd64": "x64", "aarch64": "arm64"}.get(machine)
|
|
63
|
+
ext = "tar.gz"
|
|
64
|
+
elif system == "darwin":
|
|
65
|
+
arch = "arm64" if machine == "arm64" else "x64"
|
|
66
|
+
ext = "tar.gz"
|
|
67
|
+
else:
|
|
68
|
+
arch = None
|
|
69
|
+
if not arch:
|
|
70
|
+
raise SystemExit(
|
|
71
|
+
f"browserr-mcp: no release asset for {system}/{machine}")
|
|
72
|
+
return f"browserr-{version}-{system}-{arch}.{ext}"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _get_json(url: str):
|
|
76
|
+
req = urllib.request.Request(url, headers={
|
|
77
|
+
"User-Agent": "browserr-mcp-bootstrap",
|
|
78
|
+
"Accept": "application/vnd.github+json",
|
|
79
|
+
})
|
|
80
|
+
with urllib.request.urlopen(req, timeout=30) as r:
|
|
81
|
+
return json.loads(r.read().decode("utf-8"))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _resolve_release() -> tuple[str, dict]:
|
|
85
|
+
pin = os.environ.get("BROWSERR_VERSION", "").strip().lstrip("v")
|
|
86
|
+
if pin:
|
|
87
|
+
rel = _get_json(f"{API}/repos/{REPO}/releases/tags/v{pin}")
|
|
88
|
+
else:
|
|
89
|
+
rel = _get_json(f"{API}/repos/{REPO}/releases/latest")
|
|
90
|
+
tag = rel.get("tag_name", "").lstrip("v")
|
|
91
|
+
if not tag:
|
|
92
|
+
raise SystemExit("browserr-mcp: release has no tag_name")
|
|
93
|
+
return tag, rel
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _install_dir(version: str) -> str:
|
|
97
|
+
home = os.environ.get("BROWSERR_HOME")
|
|
98
|
+
if home:
|
|
99
|
+
return os.path.abspath(home)
|
|
100
|
+
base = os.path.join(os.path.expanduser("~"), ".browserr", "versions")
|
|
101
|
+
return os.path.join(base, version)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _engine_present(d: str) -> bool:
|
|
105
|
+
exe = "engine.exe" if os.name == "nt" else "engine"
|
|
106
|
+
return os.path.isfile(os.path.join(d, exe))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _unpack(path: str, dest: str) -> None:
|
|
110
|
+
if path.endswith(".zip"):
|
|
111
|
+
with zipfile.ZipFile(path) as z:
|
|
112
|
+
z.extractall(dest)
|
|
113
|
+
for info in z.infolist():
|
|
114
|
+
if info.external_attr >> 28 == 0o3:
|
|
115
|
+
target = os.path.join(dest, info.filename)
|
|
116
|
+
if os.path.exists(target):
|
|
117
|
+
os.chmod(target, os.stat(target).st_mode | stat.S_IXUSR)
|
|
118
|
+
else:
|
|
119
|
+
import tarfile
|
|
120
|
+
with tarfile.open(path) as t:
|
|
121
|
+
t.extractall(dest)
|
|
122
|
+
for m in t.getmembers():
|
|
123
|
+
if m.mode & 0o111 and m.isfile():
|
|
124
|
+
target = os.path.join(dest, m.name)
|
|
125
|
+
if os.path.exists(target):
|
|
126
|
+
os.chmod(target, os.stat(target).st_mode | stat.S_IXUSR)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def ensure_engine() -> str:
|
|
130
|
+
"""Return the unpack dir; download+verify the engine if missing."""
|
|
131
|
+
tag, release = _resolve_release()
|
|
132
|
+
dest = _install_dir(tag)
|
|
133
|
+
if _engine_present(dest):
|
|
134
|
+
return dest
|
|
135
|
+
|
|
136
|
+
asset_name = _platform_asset(tag)
|
|
137
|
+
asset = next((a for a in release.get("assets", [])
|
|
138
|
+
if a.get("name") == asset_name), None)
|
|
139
|
+
if not asset:
|
|
140
|
+
raise SystemExit(f"browserr-mcp: release v{tag} has no asset {asset_name}")
|
|
141
|
+
|
|
142
|
+
url = asset.get("browser_download_url", "")
|
|
143
|
+
host = urllib.request.urlparse(url).hostname or ""
|
|
144
|
+
if urllib.request.urlparse(url).scheme != "https" or host not in ALLOWED_HOSTS:
|
|
145
|
+
raise SystemExit(f"browserr-mcp: refusing non-GitHub download URL: {url}")
|
|
146
|
+
if asset.get("size", 0) > MAX_ASSET_BYTES:
|
|
147
|
+
raise SystemExit("browserr-mcp: refusing implausibly large asset")
|
|
148
|
+
|
|
149
|
+
expected = (asset.get("digest") or "").removeprefix("sha256:").lower()
|
|
150
|
+
if not expected:
|
|
151
|
+
raise SystemExit(
|
|
152
|
+
"browserr-mcp: release asset carries no sha256 digest; refusing "
|
|
153
|
+
"to install unverified (the digest is the corruption guard; "
|
|
154
|
+
"TLS only covers the transport)")
|
|
155
|
+
|
|
156
|
+
os.makedirs(dest, exist_ok=True)
|
|
157
|
+
tmp = tempfile.mkdtemp(prefix="browserr-bootstrap-")
|
|
158
|
+
try:
|
|
159
|
+
archive = os.path.join(tmp, asset_name)
|
|
160
|
+
print(f"browserr-mcp: downloading {asset_name} "
|
|
161
|
+
f"({asset.get('size', 0) // (1024*1024)} MB)...", file=sys.stderr)
|
|
162
|
+
req = urllib.request.Request(url, headers={"User-Agent": "browserr-mcp-bootstrap"})
|
|
163
|
+
with urllib.request.urlopen(req, timeout=600) as r, open(archive, "wb") as f:
|
|
164
|
+
shutil.copyfileobj(r, f)
|
|
165
|
+
|
|
166
|
+
h = hashlib.sha256()
|
|
167
|
+
with open(archive, "rb") as f:
|
|
168
|
+
for chunk in iter(lambda: f.read(1024 * 1024), b""):
|
|
169
|
+
h.update(chunk)
|
|
170
|
+
actual = h.hexdigest()
|
|
171
|
+
if actual != expected:
|
|
172
|
+
raise SystemExit(f"browserr-mcp: sha256 mismatch: expected {expected}, got {actual}")
|
|
173
|
+
|
|
174
|
+
# unpack to a staging dir, then move into place (all-or-nothing)
|
|
175
|
+
stage = os.path.join(tmp, "unpacked")
|
|
176
|
+
os.makedirs(stage)
|
|
177
|
+
_unpack(archive, stage)
|
|
178
|
+
if not _engine_present(stage):
|
|
179
|
+
raise SystemExit("browserr-mcp: archive has no engine binary; not a browserr release?")
|
|
180
|
+
for entry in os.listdir(stage):
|
|
181
|
+
shutil.move(os.path.join(stage, entry), os.path.join(dest, entry))
|
|
182
|
+
return dest
|
|
183
|
+
finally:
|
|
184
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def main() -> None:
|
|
188
|
+
"""MCP server entry point: ensure the engine, then run browserr_mcp."""
|
|
189
|
+
dest = ensure_engine()
|
|
190
|
+
mcp = os.path.join(dest, "browserr_mcp.py")
|
|
191
|
+
if not os.path.isfile(mcp):
|
|
192
|
+
raise SystemExit(f"browserr-mcp: browserr_mcp.py missing in {dest}")
|
|
193
|
+
os.environ["BROWSERR_ENGINE"] = os.path.join(
|
|
194
|
+
dest, "engine.exe" if os.name == "nt" else "engine")
|
|
195
|
+
sys.argv = [mcp] + sys.argv[1:]
|
|
196
|
+
sys.path.insert(0, dest)
|
|
197
|
+
runpy.run_path(mcp, run_name="__main__")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def install_main() -> None:
|
|
201
|
+
"""Standalone installer entry point: download + verify, then exit."""
|
|
202
|
+
tag, _ = _resolve_release()
|
|
203
|
+
dest = _install_dir(tag)
|
|
204
|
+
if _engine_present(dest):
|
|
205
|
+
print(f"browserr-install: v{tag} already present at {dest}")
|
|
206
|
+
return
|
|
207
|
+
ensure_engine()
|
|
208
|
+
print(f"browserr-install: v{tag} installed at {dest}")
|
|
209
|
+
print('MCP config: {"command": "browserr-mcp"}')
|