dryhack-mcp 2.3.0__tar.gz → 2.3.2__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 (25) hide show
  1. {dryhack_mcp-2.3.0 → dryhack_mcp-2.3.2}/PKG-INFO +26 -9
  2. {dryhack_mcp-2.3.0 → dryhack_mcp-2.3.2}/README.md +25 -8
  3. {dryhack_mcp-2.3.0 → dryhack_mcp-2.3.2}/dryhack_mcp/__init__.py +1 -1
  4. {dryhack_mcp-2.3.0 → dryhack_mcp-2.3.2}/dryhack_mcp/tools/browser_tool.py +21 -3
  5. {dryhack_mcp-2.3.0 → dryhack_mcp-2.3.2}/dryhack_mcp.egg-info/PKG-INFO +26 -9
  6. {dryhack_mcp-2.3.0 → dryhack_mcp-2.3.2}/pyproject.toml +1 -1
  7. {dryhack_mcp-2.3.0 → dryhack_mcp-2.3.2}/tests/test_browser.py +24 -0
  8. {dryhack_mcp-2.3.0 → dryhack_mcp-2.3.2}/LICENSE +0 -0
  9. {dryhack_mcp-2.3.0 → dryhack_mcp-2.3.2}/dryhack_mcp/__main__.py +0 -0
  10. {dryhack_mcp-2.3.0 → dryhack_mcp-2.3.2}/dryhack_mcp/config.py +0 -0
  11. {dryhack_mcp-2.3.0 → dryhack_mcp-2.3.2}/dryhack_mcp/server.py +0 -0
  12. {dryhack_mcp-2.3.0 → dryhack_mcp-2.3.2}/dryhack_mcp/tools/__init__.py +0 -0
  13. {dryhack_mcp-2.3.0 → dryhack_mcp-2.3.2}/dryhack_mcp/tools/authorize.py +0 -0
  14. {dryhack_mcp-2.3.0 → dryhack_mcp-2.3.2}/dryhack_mcp/tools/curl_tool.py +0 -0
  15. {dryhack_mcp-2.3.0 → dryhack_mcp-2.3.2}/dryhack_mcp/tools/exec.py +0 -0
  16. {dryhack_mcp-2.3.0 → dryhack_mcp-2.3.2}/dryhack_mcp/tools/python_tool.py +0 -0
  17. {dryhack_mcp-2.3.0 → dryhack_mcp-2.3.2}/dryhack_mcp/tools/scope.py +0 -0
  18. {dryhack_mcp-2.3.0 → dryhack_mcp-2.3.2}/dryhack_mcp/tools/shell_tool.py +0 -0
  19. {dryhack_mcp-2.3.0 → dryhack_mcp-2.3.2}/dryhack_mcp.egg-info/SOURCES.txt +0 -0
  20. {dryhack_mcp-2.3.0 → dryhack_mcp-2.3.2}/dryhack_mcp.egg-info/dependency_links.txt +0 -0
  21. {dryhack_mcp-2.3.0 → dryhack_mcp-2.3.2}/dryhack_mcp.egg-info/entry_points.txt +0 -0
  22. {dryhack_mcp-2.3.0 → dryhack_mcp-2.3.2}/dryhack_mcp.egg-info/requires.txt +0 -0
  23. {dryhack_mcp-2.3.0 → dryhack_mcp-2.3.2}/dryhack_mcp.egg-info/top_level.txt +0 -0
  24. {dryhack_mcp-2.3.0 → dryhack_mcp-2.3.2}/setup.cfg +0 -0
  25. {dryhack_mcp-2.3.0 → dryhack_mcp-2.3.2}/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.0
3
+ Version: 2.3.2
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
@@ -74,13 +74,19 @@ With [uv](https://docs.astral.sh/uv/) you can run the server directly from PyPI
74
74
  — no manual install needed:
75
75
 
76
76
  ```bash
77
- uvx dryhack-mcp
77
+ # recommended: pull the browser extra so the `browser` tool is guaranteed
78
+ uvx --from 'dryhack-mcp[browser]' dryhack-mcp
78
79
  # http transport
79
- uvx dryhack-mcp --transport http --host 0.0.0.0 --port 8000
80
+ uvx --from 'dryhack-mcp[browser]' dryhack-mcp --transport http --host 0.0.0.0 --port 8000
80
81
  # pin a version
81
- uvx dryhack-mcp@0.1.3
82
+ uvx --from 'dryhack-mcp[browser]==2.3.2' dryhack-mcp
82
83
  ```
83
84
 
85
+ > `uvx dryhack-mcp` (without `--from`) also works — Playwright is a core
86
+ > dependency — but the explicit `--from 'dryhack-mcp[browser]'` form is the
87
+ > recommended, unambiguous way to get everything the `browser` tool needs even
88
+ > if a stale cache is involved.
89
+
84
90
  For development:
85
91
 
86
92
  ```bash
@@ -90,10 +96,17 @@ python3 -m pip install -e ".[dev]"
90
96
  ### Browser tool (Playwright)
91
97
 
92
98
  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.
99
+ `uvx dryhack-mcp` already ships it. For MCP clients using uvx, the recommended,
100
+ unambiguous invocation is:
101
+
102
+ ```bash
103
+ uvx --from 'dryhack-mcp[browser]' dryhack-mcp
104
+ ```
105
+
106
+ The browser *binaries* (Chromium/Firefox/WebKit) are not distributed via pip, so
107
+ the first time the `browser` tool launches an engine it **auto-runs
108
+ `playwright install <browser>`** if the binary is missing (you'll see a
109
+ `[setup] auto-installed ...` note in the tool output on that first call).
97
110
 
98
111
  To pre-install the binaries (optional, avoids the one-time first-use download):
99
112
 
@@ -157,7 +170,7 @@ CLI options:
157
170
  "mcpServers": {
158
171
  "dryhack": {
159
172
  "command": "uvx",
160
- "args": ["dryhack-mcp"],
173
+ "args": ["--from", "dryhack-mcp[browser]", "dryhack-mcp"],
161
174
  "env": {
162
175
  "DRYHACK_COMMAND_TIMEOUT": "120"
163
176
  }
@@ -166,6 +179,10 @@ CLI options:
166
179
  }
167
180
  ```
168
181
 
182
+ > The `--from dryhack-mcp[browser]` args ensure the `browser` tool's Playwright
183
+ > dependency is present. The browser binary still auto-installs on first use
184
+ > (`DRYHACK_BROWSER_AUTO_INSTALL`, default on).
185
+
169
186
  ### http
170
187
 
171
188
  ```json
@@ -28,13 +28,19 @@ With [uv](https://docs.astral.sh/uv/) you can run the server directly from PyPI
28
28
  — no manual install needed:
29
29
 
30
30
  ```bash
31
- uvx dryhack-mcp
31
+ # recommended: pull the browser extra so the `browser` tool is guaranteed
32
+ uvx --from 'dryhack-mcp[browser]' dryhack-mcp
32
33
  # http transport
33
- uvx dryhack-mcp --transport http --host 0.0.0.0 --port 8000
34
+ uvx --from 'dryhack-mcp[browser]' dryhack-mcp --transport http --host 0.0.0.0 --port 8000
34
35
  # pin a version
35
- uvx dryhack-mcp@0.1.3
36
+ uvx --from 'dryhack-mcp[browser]==2.3.2' dryhack-mcp
36
37
  ```
37
38
 
39
+ > `uvx dryhack-mcp` (without `--from`) also works — Playwright is a core
40
+ > dependency — but the explicit `--from 'dryhack-mcp[browser]'` form is the
41
+ > recommended, unambiguous way to get everything the `browser` tool needs even
42
+ > if a stale cache is involved.
43
+
38
44
  For development:
39
45
 
40
46
  ```bash
@@ -44,10 +50,17 @@ python3 -m pip install -e ".[dev]"
44
50
  ### Browser tool (Playwright)
45
51
 
46
52
  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.
53
+ `uvx dryhack-mcp` already ships it. For MCP clients using uvx, the recommended,
54
+ unambiguous invocation is:
55
+
56
+ ```bash
57
+ uvx --from 'dryhack-mcp[browser]' dryhack-mcp
58
+ ```
59
+
60
+ The browser *binaries* (Chromium/Firefox/WebKit) are not distributed via pip, so
61
+ the first time the `browser` tool launches an engine it **auto-runs
62
+ `playwright install <browser>`** if the binary is missing (you'll see a
63
+ `[setup] auto-installed ...` note in the tool output on that first call).
51
64
 
52
65
  To pre-install the binaries (optional, avoids the one-time first-use download):
53
66
 
@@ -111,7 +124,7 @@ CLI options:
111
124
  "mcpServers": {
112
125
  "dryhack": {
113
126
  "command": "uvx",
114
- "args": ["dryhack-mcp"],
127
+ "args": ["--from", "dryhack-mcp[browser]", "dryhack-mcp"],
115
128
  "env": {
116
129
  "DRYHACK_COMMAND_TIMEOUT": "120"
117
130
  }
@@ -120,6 +133,10 @@ CLI options:
120
133
  }
121
134
  ```
122
135
 
136
+ > The `--from dryhack-mcp[browser]` args ensure the `browser` tool's Playwright
137
+ > dependency is present. The browser binary still auto-installs on first use
138
+ > (`DRYHACK_BROWSER_AUTO_INSTALL`, default on).
139
+
123
140
  ### http
124
141
 
125
142
  ```json
@@ -1,3 +1,3 @@
1
1
  """DryHack-MCP: offensive-security MCP server for authorized penetration testing."""
2
2
 
3
- __version__ = "2.3.0"
3
+ __version__ = "2.3.2"
@@ -449,8 +449,18 @@ def _auto_install_browser(browser_name: str) -> Tuple[bool, str]:
449
449
  return False, f"{type(exc).__name__}: {exc}"
450
450
 
451
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."""
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
+ """
454
464
  try:
455
465
  return browser_type.launch(**launch_kwargs)
456
466
  except Exception as exc: # noqa: BLE001
@@ -463,6 +473,11 @@ def _launch_browser(browser_type, browser_name: str, launch_kwargs: Dict[str, An
463
473
  if binary_missing and config.BROWSER_AUTO_INSTALL:
464
474
  ok, log = _auto_install_browser(browser_name)
465
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
+ )
466
481
  return browser_type.launch(**launch_kwargs)
467
482
  raise RuntimeError(
468
483
  f"{browser_name} binary missing and auto-install failed. "
@@ -504,7 +519,7 @@ def _run_session(
504
519
  if session.get("proxy") is not None:
505
520
  launch_kwargs["proxy"] = session["proxy"]
506
521
 
507
- browser = _launch_browser(browser_type, browser_name, launch_kwargs)
522
+ browser = _launch_browser(browser_type, browser_name, launch_kwargs, session)
508
523
  try:
509
524
  context = browser.new_context(**_build_context_options(session))
510
525
  if default_timeout is not None:
@@ -690,6 +705,9 @@ def browser(
690
705
  f"$ browser[{session['browser_type']}, "
691
706
  f"headless={session['headless']}] {len(normalized)} step(s)"
692
707
  )
708
+ install_note = session.get("_install_note")
709
+ if install_note:
710
+ header = f"{header}\n[setup] {install_note}"
693
711
  if error:
694
712
  return f"{header}\n[error] {error}"
695
713
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dryhack-mcp
3
- Version: 2.3.0
3
+ Version: 2.3.2
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
@@ -74,13 +74,19 @@ With [uv](https://docs.astral.sh/uv/) you can run the server directly from PyPI
74
74
  — no manual install needed:
75
75
 
76
76
  ```bash
77
- uvx dryhack-mcp
77
+ # recommended: pull the browser extra so the `browser` tool is guaranteed
78
+ uvx --from 'dryhack-mcp[browser]' dryhack-mcp
78
79
  # http transport
79
- uvx dryhack-mcp --transport http --host 0.0.0.0 --port 8000
80
+ uvx --from 'dryhack-mcp[browser]' dryhack-mcp --transport http --host 0.0.0.0 --port 8000
80
81
  # pin a version
81
- uvx dryhack-mcp@0.1.3
82
+ uvx --from 'dryhack-mcp[browser]==2.3.2' dryhack-mcp
82
83
  ```
83
84
 
85
+ > `uvx dryhack-mcp` (without `--from`) also works — Playwright is a core
86
+ > dependency — but the explicit `--from 'dryhack-mcp[browser]'` form is the
87
+ > recommended, unambiguous way to get everything the `browser` tool needs even
88
+ > if a stale cache is involved.
89
+
84
90
  For development:
85
91
 
86
92
  ```bash
@@ -90,10 +96,17 @@ python3 -m pip install -e ".[dev]"
90
96
  ### Browser tool (Playwright)
91
97
 
92
98
  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.
99
+ `uvx dryhack-mcp` already ships it. For MCP clients using uvx, the recommended,
100
+ unambiguous invocation is:
101
+
102
+ ```bash
103
+ uvx --from 'dryhack-mcp[browser]' dryhack-mcp
104
+ ```
105
+
106
+ The browser *binaries* (Chromium/Firefox/WebKit) are not distributed via pip, so
107
+ the first time the `browser` tool launches an engine it **auto-runs
108
+ `playwright install <browser>`** if the binary is missing (you'll see a
109
+ `[setup] auto-installed ...` note in the tool output on that first call).
97
110
 
98
111
  To pre-install the binaries (optional, avoids the one-time first-use download):
99
112
 
@@ -157,7 +170,7 @@ CLI options:
157
170
  "mcpServers": {
158
171
  "dryhack": {
159
172
  "command": "uvx",
160
- "args": ["dryhack-mcp"],
173
+ "args": ["--from", "dryhack-mcp[browser]", "dryhack-mcp"],
161
174
  "env": {
162
175
  "DRYHACK_COMMAND_TIMEOUT": "120"
163
176
  }
@@ -166,6 +179,10 @@ CLI options:
166
179
  }
167
180
  ```
168
181
 
182
+ > The `--from dryhack-mcp[browser]` args ensure the `browser` tool's Playwright
183
+ > dependency is present. The browser binary still auto-installs on first use
184
+ > (`DRYHACK_BROWSER_AUTO_INSTALL`, default on).
185
+
169
186
  ### http
170
187
 
171
188
  ```json
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dryhack-mcp"
7
- version = "2.3.0"
7
+ version = "2.3.2"
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" }
@@ -124,6 +124,30 @@ class BrowserToolTests(unittest.TestCase):
124
124
  self.assertEqual(calls["launch"], 2) # failed once, retried once
125
125
  self.assertEqual(calls["install"], 1)
126
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
+
127
151
  def test_launch_does_not_autoinstall_when_disabled(self):
128
152
  class FakeBrowserType:
129
153
  def launch(self, **kwargs):
File without changes
File without changes