dryhack-mcp 2.2.0__tar.gz → 2.3.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.
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/PKG-INFO +20 -10
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/README.md +18 -9
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/dryhack_mcp/__init__.py +1 -1
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/dryhack_mcp/config.py +16 -0
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/dryhack_mcp/tools/browser_tool.py +64 -11
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/dryhack_mcp.egg-info/PKG-INFO +20 -10
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/dryhack_mcp.egg-info/requires.txt +1 -0
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/pyproject.toml +4 -1
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/tests/test_browser.py +47 -2
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/LICENSE +0 -0
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/dryhack_mcp/__main__.py +0 -0
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/dryhack_mcp/server.py +0 -0
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/dryhack_mcp/tools/__init__.py +0 -0
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/dryhack_mcp/tools/authorize.py +0 -0
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/dryhack_mcp/tools/curl_tool.py +0 -0
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/dryhack_mcp/tools/exec.py +0 -0
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/dryhack_mcp/tools/python_tool.py +0 -0
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/dryhack_mcp/tools/scope.py +0 -0
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/dryhack_mcp/tools/shell_tool.py +0 -0
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/dryhack_mcp.egg-info/SOURCES.txt +0 -0
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/dryhack_mcp.egg-info/dependency_links.txt +0 -0
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/dryhack_mcp.egg-info/entry_points.txt +0 -0
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/dryhack_mcp.egg-info/top_level.txt +0 -0
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/setup.cfg +0 -0
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/tests/test_authorize.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dryhack-mcp
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: DryHack-MCP - an MCP server providing offensive-security tooling (curl, python, shell, Playwright browser) and a scope-gated authorize engine for authorized penetration testing.
|
|
5
5
|
Author: Afrizal F.A
|
|
6
6
|
License: MIT License
|
|
@@ -36,6 +36,7 @@ Requires-Python: >=3.9
|
|
|
36
36
|
Description-Content-Type: text/markdown
|
|
37
37
|
License-File: LICENSE
|
|
38
38
|
Requires-Dist: mcp<2,>=1.2.0
|
|
39
|
+
Requires-Dist: playwright>=1.40
|
|
39
40
|
Provides-Extra: dev
|
|
40
41
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
41
42
|
Requires-Dist: ruff>=0.1; extra == "dev"
|
|
@@ -88,16 +89,21 @@ python3 -m pip install -e ".[dev]"
|
|
|
88
89
|
|
|
89
90
|
### Browser tool (Playwright)
|
|
90
91
|
|
|
91
|
-
|
|
92
|
-
browser binaries
|
|
92
|
+
Playwright is a **core dependency**, so a plain `pip install dryhack-mcp` /
|
|
93
|
+
`uvx dryhack-mcp` already ships it — no extra needed. The browser *binaries*
|
|
94
|
+
(Chromium/Firefox/WebKit) are not distributed via pip, so the first time the
|
|
95
|
+
`browser` tool launches an engine it **auto-runs `playwright install <browser>`**
|
|
96
|
+
if the binary is missing.
|
|
97
|
+
|
|
98
|
+
To pre-install the binaries (optional, avoids the one-time first-use download):
|
|
93
99
|
|
|
94
100
|
```bash
|
|
95
|
-
|
|
96
|
-
playwright install
|
|
101
|
+
playwright install # all engines
|
|
102
|
+
playwright install chromium # just chromium
|
|
97
103
|
```
|
|
98
104
|
|
|
99
|
-
|
|
100
|
-
|
|
105
|
+
Auto-install is controlled by `DRYHACK_BROWSER_AUTO_INSTALL` (default on); set it
|
|
106
|
+
to `0` to disable and manage browsers yourself.
|
|
101
107
|
|
|
102
108
|
## Run
|
|
103
109
|
|
|
@@ -181,6 +187,8 @@ Start the server separately with `dryhack-mcp --transport http`.
|
|
|
181
187
|
| `DRYHACK_COMMAND_TIMEOUT` | `120` | Per-command timeout (seconds) |
|
|
182
188
|
| `DRYHACK_WORKDIR` | cwd | Working directory for commands |
|
|
183
189
|
| `DRYHACK_OUTPUT_LIMIT` | `65536` | Max stdout/stderr bytes captured |
|
|
190
|
+
| `DRYHACK_BROWSER_AUTO_INSTALL` | `1` | Auto-download the browser binary on first use (`browser` tool) |
|
|
191
|
+
| `DRYHACK_BROWSER_INSTALL_TIMEOUT` | `900` | Timeout (s) for the on-demand `playwright install` download |
|
|
184
192
|
| `DRYHACK_HTTP_HOST` | `127.0.0.1` | Default HTTP bind host |
|
|
185
193
|
| `DRYHACK_HTTP_PORT` | `8000` | Default HTTP bind port |
|
|
186
194
|
|
|
@@ -236,9 +244,11 @@ browser(action="goto", url="https://target.example/",
|
|
|
236
244
|
init_script="Object.defineProperty(navigator,'webdriver',{get:()=>false})")
|
|
237
245
|
```
|
|
238
246
|
|
|
239
|
-
Playwright runs in a dedicated worker thread (the MCP
|
|
240
|
-
loop, and Playwright's sync API cannot run inside a live
|
|
241
|
-
|
|
247
|
+
Playwright ships with dryhack-mcp and runs in a dedicated worker thread (the MCP
|
|
248
|
+
server owns the asyncio loop, and Playwright's sync API cannot run inside a live
|
|
249
|
+
loop). The browser binary auto-installs on first use if missing
|
|
250
|
+
(`DRYHACK_BROWSER_AUTO_INSTALL`, default on). The same authorized-use scope
|
|
251
|
+
rules as the other tools apply.
|
|
242
252
|
|
|
243
253
|
## authorize (per-call scope)
|
|
244
254
|
|
|
@@ -43,16 +43,21 @@ python3 -m pip install -e ".[dev]"
|
|
|
43
43
|
|
|
44
44
|
### Browser tool (Playwright)
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
browser binaries
|
|
46
|
+
Playwright is a **core dependency**, so a plain `pip install dryhack-mcp` /
|
|
47
|
+
`uvx dryhack-mcp` already ships it — no extra needed. The browser *binaries*
|
|
48
|
+
(Chromium/Firefox/WebKit) are not distributed via pip, so the first time the
|
|
49
|
+
`browser` tool launches an engine it **auto-runs `playwright install <browser>`**
|
|
50
|
+
if the binary is missing.
|
|
51
|
+
|
|
52
|
+
To pre-install the binaries (optional, avoids the one-time first-use download):
|
|
48
53
|
|
|
49
54
|
```bash
|
|
50
|
-
|
|
51
|
-
playwright install
|
|
55
|
+
playwright install # all engines
|
|
56
|
+
playwright install chromium # just chromium
|
|
52
57
|
```
|
|
53
58
|
|
|
54
|
-
|
|
55
|
-
|
|
59
|
+
Auto-install is controlled by `DRYHACK_BROWSER_AUTO_INSTALL` (default on); set it
|
|
60
|
+
to `0` to disable and manage browsers yourself.
|
|
56
61
|
|
|
57
62
|
## Run
|
|
58
63
|
|
|
@@ -136,6 +141,8 @@ Start the server separately with `dryhack-mcp --transport http`.
|
|
|
136
141
|
| `DRYHACK_COMMAND_TIMEOUT` | `120` | Per-command timeout (seconds) |
|
|
137
142
|
| `DRYHACK_WORKDIR` | cwd | Working directory for commands |
|
|
138
143
|
| `DRYHACK_OUTPUT_LIMIT` | `65536` | Max stdout/stderr bytes captured |
|
|
144
|
+
| `DRYHACK_BROWSER_AUTO_INSTALL` | `1` | Auto-download the browser binary on first use (`browser` tool) |
|
|
145
|
+
| `DRYHACK_BROWSER_INSTALL_TIMEOUT` | `900` | Timeout (s) for the on-demand `playwright install` download |
|
|
139
146
|
| `DRYHACK_HTTP_HOST` | `127.0.0.1` | Default HTTP bind host |
|
|
140
147
|
| `DRYHACK_HTTP_PORT` | `8000` | Default HTTP bind port |
|
|
141
148
|
|
|
@@ -191,9 +198,11 @@ browser(action="goto", url="https://target.example/",
|
|
|
191
198
|
init_script="Object.defineProperty(navigator,'webdriver',{get:()=>false})")
|
|
192
199
|
```
|
|
193
200
|
|
|
194
|
-
Playwright runs in a dedicated worker thread (the MCP
|
|
195
|
-
loop, and Playwright's sync API cannot run inside a live
|
|
196
|
-
|
|
201
|
+
Playwright ships with dryhack-mcp and runs in a dedicated worker thread (the MCP
|
|
202
|
+
server owns the asyncio loop, and Playwright's sync API cannot run inside a live
|
|
203
|
+
loop). The browser binary auto-installs on first use if missing
|
|
204
|
+
(`DRYHACK_BROWSER_AUTO_INSTALL`, default on). The same authorized-use scope
|
|
205
|
+
rules as the other tools apply.
|
|
197
206
|
|
|
198
207
|
## authorize (per-call scope)
|
|
199
208
|
|
|
@@ -23,6 +23,22 @@ WORKDIR = _get("DRYHACK_WORKDIR", os.getcwd())
|
|
|
23
23
|
OUTPUT_BYTE_LIMIT = int(_get("DRYHACK_OUTPUT_LIMIT", "65536") or "65536")
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
# ---- Browser tool (Playwright) -----------------------------------------------
|
|
27
|
+
# Auto-download the requested browser binary on first use if it is missing
|
|
28
|
+
# (the binaries are not shipped via pip). Disable by setting to 0/false/no.
|
|
29
|
+
BROWSER_AUTO_INSTALL = _get("DRYHACK_BROWSER_AUTO_INSTALL", "1").lower() not in (
|
|
30
|
+
"0",
|
|
31
|
+
"false",
|
|
32
|
+
"no",
|
|
33
|
+
"off",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Timeout (seconds) for the on-demand `playwright install <browser>` download.
|
|
37
|
+
BROWSER_INSTALL_TIMEOUT = int(
|
|
38
|
+
_get("DRYHACK_BROWSER_INSTALL_TIMEOUT", "900") or "900"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
26
42
|
# ---- Transport ---------------------------------------------------------------
|
|
27
43
|
# Default HTTP bind address when running in http mode.
|
|
28
44
|
HTTP_HOST = _get("DRYHACK_HTTP_HOST", "127.0.0.1")
|
|
@@ -6,10 +6,13 @@ and exploitation: navigation, DOM interaction, arbitrary JavaScript injection
|
|
|
6
6
|
file upload, cookie/header/storage manipulation, screenshots, PDF capture and
|
|
7
7
|
multi-step action chains — all in a single MCP call.
|
|
8
8
|
|
|
9
|
-
Playwright is
|
|
10
|
-
``
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
Playwright is a bundled runtime dependency, so a plain ``pip install
|
|
10
|
+
dryhack-mcp`` / ``uvx dryhack-mcp`` already ships it. The browser *binaries*
|
|
11
|
+
(Chromium/Firefox/WebKit) are not distributed via pip, so on first use the tool
|
|
12
|
+
auto-runs ``playwright install <browser>`` when the requested binary is missing
|
|
13
|
+
(controlled by ``DRYHACK_BROWSER_AUTO_INSTALL``, default on). Playwright is
|
|
14
|
+
still imported lazily and, if it is somehow absent, the tool returns an
|
|
15
|
+
actionable install hint instead of crashing.
|
|
13
16
|
|
|
14
17
|
Playwright's sync API cannot run inside a live asyncio event loop (the MCP
|
|
15
18
|
server runs one), so every browser session is executed in a dedicated worker
|
|
@@ -424,6 +427,50 @@ def _build_context_options(session: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
424
427
|
return opts
|
|
425
428
|
|
|
426
429
|
|
|
430
|
+
def _auto_install_browser(browser_name: str) -> Tuple[bool, str]:
|
|
431
|
+
"""Best-effort ``playwright install <browser>`` for the current interpreter.
|
|
432
|
+
|
|
433
|
+
The browser binaries are not shipped via pip, so the first time a given
|
|
434
|
+
engine is used we download it on demand. Returns (ok, log_tail).
|
|
435
|
+
"""
|
|
436
|
+
import subprocess
|
|
437
|
+
import sys
|
|
438
|
+
|
|
439
|
+
try:
|
|
440
|
+
proc = subprocess.run(
|
|
441
|
+
[sys.executable, "-m", "playwright", "install", browser_name],
|
|
442
|
+
capture_output=True,
|
|
443
|
+
text=True,
|
|
444
|
+
timeout=config.BROWSER_INSTALL_TIMEOUT,
|
|
445
|
+
)
|
|
446
|
+
log = ((proc.stdout or "") + (proc.stderr or "")).strip()
|
|
447
|
+
return proc.returncode == 0, log[-1500:]
|
|
448
|
+
except Exception as exc: # noqa: BLE001 - report, don't crash
|
|
449
|
+
return False, f"{type(exc).__name__}: {exc}"
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _launch_browser(browser_type, browser_name: str, launch_kwargs: Dict[str, Any]):
|
|
453
|
+
"""Launch the browser, auto-installing its binary on first use if missing."""
|
|
454
|
+
try:
|
|
455
|
+
return browser_type.launch(**launch_kwargs)
|
|
456
|
+
except Exception as exc: # noqa: BLE001
|
|
457
|
+
msg = str(exc)
|
|
458
|
+
binary_missing = (
|
|
459
|
+
"Executable doesn't exist" in msg
|
|
460
|
+
or "playwright install" in msg.lower()
|
|
461
|
+
or "please run the following command" in msg.lower()
|
|
462
|
+
)
|
|
463
|
+
if binary_missing and config.BROWSER_AUTO_INSTALL:
|
|
464
|
+
ok, log = _auto_install_browser(browser_name)
|
|
465
|
+
if ok:
|
|
466
|
+
return browser_type.launch(**launch_kwargs)
|
|
467
|
+
raise RuntimeError(
|
|
468
|
+
f"{browser_name} binary missing and auto-install failed. "
|
|
469
|
+
f"Run `playwright install {browser_name}` manually.\n{log}"
|
|
470
|
+
) from exc
|
|
471
|
+
raise
|
|
472
|
+
|
|
473
|
+
|
|
427
474
|
def _run_session(
|
|
428
475
|
steps: List[Dict[str, Any]],
|
|
429
476
|
session: Dict[str, Any],
|
|
@@ -434,16 +481,19 @@ def _run_session(
|
|
|
434
481
|
except ImportError:
|
|
435
482
|
return (
|
|
436
483
|
[],
|
|
437
|
-
"playwright is not
|
|
438
|
-
"
|
|
439
|
-
"
|
|
484
|
+
"playwright is not importable (it ships with dryhack-mcp). "
|
|
485
|
+
"Reinstall with:\n"
|
|
486
|
+
" pip install --force-reinstall dryhack-mcp\n"
|
|
487
|
+
"or install directly:\n"
|
|
488
|
+
" pip install 'playwright>=1.40' && playwright install",
|
|
440
489
|
)
|
|
441
490
|
|
|
442
491
|
results: List[Dict[str, Any]] = []
|
|
443
492
|
default_timeout = session.get("timeout")
|
|
444
493
|
|
|
445
494
|
with sync_playwright() as pw:
|
|
446
|
-
|
|
495
|
+
browser_name = session.get("browser_type", "chromium")
|
|
496
|
+
browser_type = getattr(pw, browser_name)
|
|
447
497
|
launch_kwargs: Dict[str, Any] = {"headless": session.get("headless", True)}
|
|
448
498
|
if session.get("slow_mo") is not None:
|
|
449
499
|
launch_kwargs["slow_mo"] = session["slow_mo"]
|
|
@@ -454,7 +504,7 @@ def _run_session(
|
|
|
454
504
|
if session.get("proxy") is not None:
|
|
455
505
|
launch_kwargs["proxy"] = session["proxy"]
|
|
456
506
|
|
|
457
|
-
browser = browser_type
|
|
507
|
+
browser = _launch_browser(browser_type, browser_name, launch_kwargs)
|
|
458
508
|
try:
|
|
459
509
|
context = browser.new_context(**_build_context_options(session))
|
|
460
510
|
if default_timeout is not None:
|
|
@@ -510,8 +560,11 @@ def _run_session_threaded(
|
|
|
510
560
|
|
|
511
561
|
thread = threading.Thread(target=_worker, daemon=True)
|
|
512
562
|
thread.start()
|
|
513
|
-
# Generous ceiling so a per-step Playwright timeout fires first.
|
|
514
|
-
|
|
563
|
+
# Generous ceiling so a per-step Playwright timeout fires first. The extra
|
|
564
|
+
# allowance covers a possible first-use `playwright install` download.
|
|
565
|
+
base = (session.get("timeout") or config.COMMAND_TIMEOUT * 1000) / 1000
|
|
566
|
+
install_allowance = config.BROWSER_INSTALL_TIMEOUT if config.BROWSER_AUTO_INSTALL else 60
|
|
567
|
+
thread.join(timeout=base + install_allowance)
|
|
515
568
|
if thread.is_alive():
|
|
516
569
|
return [], "browser session exceeded the overall time budget"
|
|
517
570
|
return box.get("results", []), box.get("error")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dryhack-mcp
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: DryHack-MCP - an MCP server providing offensive-security tooling (curl, python, shell, Playwright browser) and a scope-gated authorize engine for authorized penetration testing.
|
|
5
5
|
Author: Afrizal F.A
|
|
6
6
|
License: MIT License
|
|
@@ -36,6 +36,7 @@ Requires-Python: >=3.9
|
|
|
36
36
|
Description-Content-Type: text/markdown
|
|
37
37
|
License-File: LICENSE
|
|
38
38
|
Requires-Dist: mcp<2,>=1.2.0
|
|
39
|
+
Requires-Dist: playwright>=1.40
|
|
39
40
|
Provides-Extra: dev
|
|
40
41
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
41
42
|
Requires-Dist: ruff>=0.1; extra == "dev"
|
|
@@ -88,16 +89,21 @@ python3 -m pip install -e ".[dev]"
|
|
|
88
89
|
|
|
89
90
|
### Browser tool (Playwright)
|
|
90
91
|
|
|
91
|
-
|
|
92
|
-
browser binaries
|
|
92
|
+
Playwright is a **core dependency**, so a plain `pip install dryhack-mcp` /
|
|
93
|
+
`uvx dryhack-mcp` already ships it — no extra needed. The browser *binaries*
|
|
94
|
+
(Chromium/Firefox/WebKit) are not distributed via pip, so the first time the
|
|
95
|
+
`browser` tool launches an engine it **auto-runs `playwright install <browser>`**
|
|
96
|
+
if the binary is missing.
|
|
97
|
+
|
|
98
|
+
To pre-install the binaries (optional, avoids the one-time first-use download):
|
|
93
99
|
|
|
94
100
|
```bash
|
|
95
|
-
|
|
96
|
-
playwright install
|
|
101
|
+
playwright install # all engines
|
|
102
|
+
playwright install chromium # just chromium
|
|
97
103
|
```
|
|
98
104
|
|
|
99
|
-
|
|
100
|
-
|
|
105
|
+
Auto-install is controlled by `DRYHACK_BROWSER_AUTO_INSTALL` (default on); set it
|
|
106
|
+
to `0` to disable and manage browsers yourself.
|
|
101
107
|
|
|
102
108
|
## Run
|
|
103
109
|
|
|
@@ -181,6 +187,8 @@ Start the server separately with `dryhack-mcp --transport http`.
|
|
|
181
187
|
| `DRYHACK_COMMAND_TIMEOUT` | `120` | Per-command timeout (seconds) |
|
|
182
188
|
| `DRYHACK_WORKDIR` | cwd | Working directory for commands |
|
|
183
189
|
| `DRYHACK_OUTPUT_LIMIT` | `65536` | Max stdout/stderr bytes captured |
|
|
190
|
+
| `DRYHACK_BROWSER_AUTO_INSTALL` | `1` | Auto-download the browser binary on first use (`browser` tool) |
|
|
191
|
+
| `DRYHACK_BROWSER_INSTALL_TIMEOUT` | `900` | Timeout (s) for the on-demand `playwright install` download |
|
|
184
192
|
| `DRYHACK_HTTP_HOST` | `127.0.0.1` | Default HTTP bind host |
|
|
185
193
|
| `DRYHACK_HTTP_PORT` | `8000` | Default HTTP bind port |
|
|
186
194
|
|
|
@@ -236,9 +244,11 @@ browser(action="goto", url="https://target.example/",
|
|
|
236
244
|
init_script="Object.defineProperty(navigator,'webdriver',{get:()=>false})")
|
|
237
245
|
```
|
|
238
246
|
|
|
239
|
-
Playwright runs in a dedicated worker thread (the MCP
|
|
240
|
-
loop, and Playwright's sync API cannot run inside a live
|
|
241
|
-
|
|
247
|
+
Playwright ships with dryhack-mcp and runs in a dedicated worker thread (the MCP
|
|
248
|
+
server owns the asyncio loop, and Playwright's sync API cannot run inside a live
|
|
249
|
+
loop). The browser binary auto-installs on first use if missing
|
|
250
|
+
(`DRYHACK_BROWSER_AUTO_INSTALL`, default on). The same authorized-use scope
|
|
251
|
+
rules as the other tools apply.
|
|
242
252
|
|
|
243
253
|
## authorize (per-call scope)
|
|
244
254
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dryhack-mcp"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.3.0"
|
|
8
8
|
description = "DryHack-MCP - an MCP server providing offensive-security tooling (curl, python, shell, Playwright browser) and a scope-gated authorize engine for authorized penetration testing."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { file = "LICENSE" }
|
|
@@ -19,10 +19,13 @@ classifiers = [
|
|
|
19
19
|
]
|
|
20
20
|
dependencies = [
|
|
21
21
|
"mcp>=1.2.0,<2",
|
|
22
|
+
"playwright>=1.40",
|
|
22
23
|
]
|
|
23
24
|
|
|
24
25
|
[project.optional-dependencies]
|
|
25
26
|
dev = ["pytest>=7.0", "ruff>=0.1"]
|
|
27
|
+
# Kept as a back-compat alias; Playwright is now a core dependency, so the
|
|
28
|
+
# `browser` tool works out of the box with a plain install.
|
|
26
29
|
browser = ["playwright>=1.40"]
|
|
27
30
|
|
|
28
31
|
[project.scripts]
|
|
@@ -36,8 +36,8 @@ class BrowserToolTests(unittest.TestCase):
|
|
|
36
36
|
result = browser(action="goto", url="https://example.com")
|
|
37
37
|
finally:
|
|
38
38
|
builtins.__import__ = real_import
|
|
39
|
-
self.assertIn("playwright is not
|
|
40
|
-
self.assertIn("
|
|
39
|
+
self.assertIn("playwright is not importable", result)
|
|
40
|
+
self.assertIn("playwright", result.lower())
|
|
41
41
|
|
|
42
42
|
def test_single_action_maps_convenience_params(self):
|
|
43
43
|
captured = {}
|
|
@@ -93,5 +93,50 @@ class BrowserToolTests(unittest.TestCase):
|
|
|
93
93
|
)
|
|
94
94
|
|
|
95
95
|
|
|
96
|
+
def test_launch_auto_installs_missing_binary_then_retries(self):
|
|
97
|
+
calls = {"launch": 0, "install": 0}
|
|
98
|
+
|
|
99
|
+
class FakeBrowserType:
|
|
100
|
+
def launch(self, **kwargs):
|
|
101
|
+
calls["launch"] += 1
|
|
102
|
+
if calls["launch"] == 1:
|
|
103
|
+
raise RuntimeError(
|
|
104
|
+
"Executable doesn't exist at /path/chrome. "
|
|
105
|
+
"Please run the following command: playwright install"
|
|
106
|
+
)
|
|
107
|
+
return "browser-handle"
|
|
108
|
+
|
|
109
|
+
def fake_install(browser_name):
|
|
110
|
+
calls["install"] += 1
|
|
111
|
+
return True, "ok"
|
|
112
|
+
|
|
113
|
+
original = browser_tool._auto_install_browser
|
|
114
|
+
browser_tool._auto_install_browser = fake_install
|
|
115
|
+
original_flag = browser_tool.config.BROWSER_AUTO_INSTALL
|
|
116
|
+
browser_tool.config.BROWSER_AUTO_INSTALL = True
|
|
117
|
+
try:
|
|
118
|
+
handle = browser_tool._launch_browser(FakeBrowserType(), "chromium", {})
|
|
119
|
+
finally:
|
|
120
|
+
browser_tool._auto_install_browser = original
|
|
121
|
+
browser_tool.config.BROWSER_AUTO_INSTALL = original_flag
|
|
122
|
+
|
|
123
|
+
self.assertEqual(handle, "browser-handle")
|
|
124
|
+
self.assertEqual(calls["launch"], 2) # failed once, retried once
|
|
125
|
+
self.assertEqual(calls["install"], 1)
|
|
126
|
+
|
|
127
|
+
def test_launch_does_not_autoinstall_when_disabled(self):
|
|
128
|
+
class FakeBrowserType:
|
|
129
|
+
def launch(self, **kwargs):
|
|
130
|
+
raise RuntimeError("Executable doesn't exist; playwright install")
|
|
131
|
+
|
|
132
|
+
original_flag = browser_tool.config.BROWSER_AUTO_INSTALL
|
|
133
|
+
browser_tool.config.BROWSER_AUTO_INSTALL = False
|
|
134
|
+
try:
|
|
135
|
+
with self.assertRaises(RuntimeError):
|
|
136
|
+
browser_tool._launch_browser(FakeBrowserType(), "chromium", {})
|
|
137
|
+
finally:
|
|
138
|
+
browser_tool.config.BROWSER_AUTO_INSTALL = original_flag
|
|
139
|
+
|
|
140
|
+
|
|
96
141
|
if __name__ == "__main__":
|
|
97
142
|
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|