dryhack-mcp 2.2.0__tar.gz → 2.3.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.
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.1}/PKG-INFO +20 -10
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.1}/README.md +18 -9
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.1}/dryhack_mcp/__init__.py +1 -1
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.1}/dryhack_mcp/config.py +16 -0
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.1}/dryhack_mcp/tools/browser_tool.py +82 -11
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.1}/dryhack_mcp.egg-info/PKG-INFO +20 -10
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.1}/dryhack_mcp.egg-info/requires.txt +1 -0
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.1}/pyproject.toml +4 -1
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.1}/tests/test_browser.py +71 -2
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.1}/LICENSE +0 -0
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.1}/dryhack_mcp/__main__.py +0 -0
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.1}/dryhack_mcp/server.py +0 -0
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.1}/dryhack_mcp/tools/__init__.py +0 -0
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.1}/dryhack_mcp/tools/authorize.py +0 -0
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.1}/dryhack_mcp/tools/curl_tool.py +0 -0
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.1}/dryhack_mcp/tools/exec.py +0 -0
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.1}/dryhack_mcp/tools/python_tool.py +0 -0
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.1}/dryhack_mcp/tools/scope.py +0 -0
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.1}/dryhack_mcp/tools/shell_tool.py +0 -0
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.1}/dryhack_mcp.egg-info/SOURCES.txt +0 -0
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.1}/dryhack_mcp.egg-info/dependency_links.txt +0 -0
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.1}/dryhack_mcp.egg-info/entry_points.txt +0 -0
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.1}/dryhack_mcp.egg-info/top_level.txt +0 -0
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.1}/setup.cfg +0 -0
- {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.1}/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.1
|
|
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,65 @@ 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(
|
|
453
|
+
browser_type,
|
|
454
|
+
browser_name: str,
|
|
455
|
+
launch_kwargs: Dict[str, Any],
|
|
456
|
+
session: Optional[Dict[str, Any]] = None,
|
|
457
|
+
):
|
|
458
|
+
"""Launch the browser, auto-installing its binary on first use if missing.
|
|
459
|
+
|
|
460
|
+
On a successful on-demand install, records a human-readable note in
|
|
461
|
+
``session["_install_note"]`` so the caller can surface the one-time download
|
|
462
|
+
in the tool output (otherwise the first call looks like a silent hang).
|
|
463
|
+
"""
|
|
464
|
+
try:
|
|
465
|
+
return browser_type.launch(**launch_kwargs)
|
|
466
|
+
except Exception as exc: # noqa: BLE001
|
|
467
|
+
msg = str(exc)
|
|
468
|
+
binary_missing = (
|
|
469
|
+
"Executable doesn't exist" in msg
|
|
470
|
+
or "playwright install" in msg.lower()
|
|
471
|
+
or "please run the following command" in msg.lower()
|
|
472
|
+
)
|
|
473
|
+
if binary_missing and config.BROWSER_AUTO_INSTALL:
|
|
474
|
+
ok, log = _auto_install_browser(browser_name)
|
|
475
|
+
if ok:
|
|
476
|
+
if session is not None:
|
|
477
|
+
session["_install_note"] = (
|
|
478
|
+
f"auto-installed the '{browser_name}' browser binary on "
|
|
479
|
+
"first use (one-time download)"
|
|
480
|
+
)
|
|
481
|
+
return browser_type.launch(**launch_kwargs)
|
|
482
|
+
raise RuntimeError(
|
|
483
|
+
f"{browser_name} binary missing and auto-install failed. "
|
|
484
|
+
f"Run `playwright install {browser_name}` manually.\n{log}"
|
|
485
|
+
) from exc
|
|
486
|
+
raise
|
|
487
|
+
|
|
488
|
+
|
|
427
489
|
def _run_session(
|
|
428
490
|
steps: List[Dict[str, Any]],
|
|
429
491
|
session: Dict[str, Any],
|
|
@@ -434,16 +496,19 @@ def _run_session(
|
|
|
434
496
|
except ImportError:
|
|
435
497
|
return (
|
|
436
498
|
[],
|
|
437
|
-
"playwright is not
|
|
438
|
-
"
|
|
439
|
-
"
|
|
499
|
+
"playwright is not importable (it ships with dryhack-mcp). "
|
|
500
|
+
"Reinstall with:\n"
|
|
501
|
+
" pip install --force-reinstall dryhack-mcp\n"
|
|
502
|
+
"or install directly:\n"
|
|
503
|
+
" pip install 'playwright>=1.40' && playwright install",
|
|
440
504
|
)
|
|
441
505
|
|
|
442
506
|
results: List[Dict[str, Any]] = []
|
|
443
507
|
default_timeout = session.get("timeout")
|
|
444
508
|
|
|
445
509
|
with sync_playwright() as pw:
|
|
446
|
-
|
|
510
|
+
browser_name = session.get("browser_type", "chromium")
|
|
511
|
+
browser_type = getattr(pw, browser_name)
|
|
447
512
|
launch_kwargs: Dict[str, Any] = {"headless": session.get("headless", True)}
|
|
448
513
|
if session.get("slow_mo") is not None:
|
|
449
514
|
launch_kwargs["slow_mo"] = session["slow_mo"]
|
|
@@ -454,7 +519,7 @@ def _run_session(
|
|
|
454
519
|
if session.get("proxy") is not None:
|
|
455
520
|
launch_kwargs["proxy"] = session["proxy"]
|
|
456
521
|
|
|
457
|
-
browser = browser_type
|
|
522
|
+
browser = _launch_browser(browser_type, browser_name, launch_kwargs, session)
|
|
458
523
|
try:
|
|
459
524
|
context = browser.new_context(**_build_context_options(session))
|
|
460
525
|
if default_timeout is not None:
|
|
@@ -510,8 +575,11 @@ def _run_session_threaded(
|
|
|
510
575
|
|
|
511
576
|
thread = threading.Thread(target=_worker, daemon=True)
|
|
512
577
|
thread.start()
|
|
513
|
-
# Generous ceiling so a per-step Playwright timeout fires first.
|
|
514
|
-
|
|
578
|
+
# Generous ceiling so a per-step Playwright timeout fires first. The extra
|
|
579
|
+
# allowance covers a possible first-use `playwright install` download.
|
|
580
|
+
base = (session.get("timeout") or config.COMMAND_TIMEOUT * 1000) / 1000
|
|
581
|
+
install_allowance = config.BROWSER_INSTALL_TIMEOUT if config.BROWSER_AUTO_INSTALL else 60
|
|
582
|
+
thread.join(timeout=base + install_allowance)
|
|
515
583
|
if thread.is_alive():
|
|
516
584
|
return [], "browser session exceeded the overall time budget"
|
|
517
585
|
return box.get("results", []), box.get("error")
|
|
@@ -637,6 +705,9 @@ def browser(
|
|
|
637
705
|
f"$ browser[{session['browser_type']}, "
|
|
638
706
|
f"headless={session['headless']}] {len(normalized)} step(s)"
|
|
639
707
|
)
|
|
708
|
+
install_note = session.get("_install_note")
|
|
709
|
+
if install_note:
|
|
710
|
+
header = f"{header}\n[setup] {install_note}"
|
|
640
711
|
if error:
|
|
641
712
|
return f"{header}\n[error] {error}"
|
|
642
713
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dryhack-mcp
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.1
|
|
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.1"
|
|
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,74 @@ 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_records_install_note_in_session(self):
|
|
128
|
+
state = {"launch": 0}
|
|
129
|
+
|
|
130
|
+
class FakeBrowserType:
|
|
131
|
+
def launch(self, **kwargs):
|
|
132
|
+
state["launch"] += 1
|
|
133
|
+
if state["launch"] == 1:
|
|
134
|
+
raise RuntimeError("Executable doesn't exist; playwright install")
|
|
135
|
+
return "handle"
|
|
136
|
+
|
|
137
|
+
session = {}
|
|
138
|
+
original = browser_tool._auto_install_browser
|
|
139
|
+
browser_tool._auto_install_browser = lambda name: (True, "ok")
|
|
140
|
+
original_flag = browser_tool.config.BROWSER_AUTO_INSTALL
|
|
141
|
+
browser_tool.config.BROWSER_AUTO_INSTALL = True
|
|
142
|
+
try:
|
|
143
|
+
browser_tool._launch_browser(FakeBrowserType(), "chromium", {}, session)
|
|
144
|
+
finally:
|
|
145
|
+
browser_tool._auto_install_browser = original
|
|
146
|
+
browser_tool.config.BROWSER_AUTO_INSTALL = original_flag
|
|
147
|
+
|
|
148
|
+
self.assertIn("_install_note", session)
|
|
149
|
+
self.assertIn("chromium", session["_install_note"])
|
|
150
|
+
|
|
151
|
+
def test_launch_does_not_autoinstall_when_disabled(self):
|
|
152
|
+
class FakeBrowserType:
|
|
153
|
+
def launch(self, **kwargs):
|
|
154
|
+
raise RuntimeError("Executable doesn't exist; playwright install")
|
|
155
|
+
|
|
156
|
+
original_flag = browser_tool.config.BROWSER_AUTO_INSTALL
|
|
157
|
+
browser_tool.config.BROWSER_AUTO_INSTALL = False
|
|
158
|
+
try:
|
|
159
|
+
with self.assertRaises(RuntimeError):
|
|
160
|
+
browser_tool._launch_browser(FakeBrowserType(), "chromium", {})
|
|
161
|
+
finally:
|
|
162
|
+
browser_tool.config.BROWSER_AUTO_INSTALL = original_flag
|
|
163
|
+
|
|
164
|
+
|
|
96
165
|
if __name__ == "__main__":
|
|
97
166
|
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
|