dryhack-mcp 2.1.2__tar.gz → 2.2.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.
Files changed (26) hide show
  1. {dryhack_mcp-2.1.2 → dryhack_mcp-2.2.0}/PKG-INFO +71 -2
  2. {dryhack_mcp-2.1.2 → dryhack_mcp-2.2.0}/README.md +67 -0
  3. {dryhack_mcp-2.1.2 → dryhack_mcp-2.2.0}/dryhack_mcp/__init__.py +1 -1
  4. dryhack_mcp-2.2.0/dryhack_mcp/server.py +189 -0
  5. dryhack_mcp-2.2.0/dryhack_mcp/tools/browser_tool.py +652 -0
  6. {dryhack_mcp-2.1.2 → dryhack_mcp-2.2.0}/dryhack_mcp.egg-info/PKG-INFO +71 -2
  7. {dryhack_mcp-2.1.2 → dryhack_mcp-2.2.0}/dryhack_mcp.egg-info/SOURCES.txt +3 -1
  8. {dryhack_mcp-2.1.2 → dryhack_mcp-2.2.0}/dryhack_mcp.egg-info/requires.txt +3 -0
  9. {dryhack_mcp-2.1.2 → dryhack_mcp-2.2.0}/pyproject.toml +3 -2
  10. dryhack_mcp-2.2.0/tests/test_browser.py +97 -0
  11. dryhack_mcp-2.1.2/dryhack_mcp/server.py +0 -77
  12. {dryhack_mcp-2.1.2 → dryhack_mcp-2.2.0}/LICENSE +0 -0
  13. {dryhack_mcp-2.1.2 → dryhack_mcp-2.2.0}/dryhack_mcp/__main__.py +0 -0
  14. {dryhack_mcp-2.1.2 → dryhack_mcp-2.2.0}/dryhack_mcp/config.py +0 -0
  15. {dryhack_mcp-2.1.2 → dryhack_mcp-2.2.0}/dryhack_mcp/tools/__init__.py +0 -0
  16. {dryhack_mcp-2.1.2 → dryhack_mcp-2.2.0}/dryhack_mcp/tools/authorize.py +0 -0
  17. {dryhack_mcp-2.1.2 → dryhack_mcp-2.2.0}/dryhack_mcp/tools/curl_tool.py +0 -0
  18. {dryhack_mcp-2.1.2 → dryhack_mcp-2.2.0}/dryhack_mcp/tools/exec.py +0 -0
  19. {dryhack_mcp-2.1.2 → dryhack_mcp-2.2.0}/dryhack_mcp/tools/python_tool.py +0 -0
  20. {dryhack_mcp-2.1.2 → dryhack_mcp-2.2.0}/dryhack_mcp/tools/scope.py +0 -0
  21. {dryhack_mcp-2.1.2 → dryhack_mcp-2.2.0}/dryhack_mcp/tools/shell_tool.py +0 -0
  22. {dryhack_mcp-2.1.2 → dryhack_mcp-2.2.0}/dryhack_mcp.egg-info/dependency_links.txt +0 -0
  23. {dryhack_mcp-2.1.2 → dryhack_mcp-2.2.0}/dryhack_mcp.egg-info/entry_points.txt +0 -0
  24. {dryhack_mcp-2.1.2 → dryhack_mcp-2.2.0}/dryhack_mcp.egg-info/top_level.txt +0 -0
  25. {dryhack_mcp-2.1.2 → dryhack_mcp-2.2.0}/setup.cfg +0 -0
  26. {dryhack_mcp-2.1.2 → dryhack_mcp-2.2.0}/tests/test_authorize.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dryhack-mcp
3
- Version: 2.1.2
4
- Summary: DryHack-MCP - an MCP server providing offensive-security tooling (curl, python, shell) and an AI-driven recommend_action engine for authorized penetration testing.
3
+ Version: 2.2.0
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
7
7
 
@@ -39,6 +39,8 @@ Requires-Dist: mcp<2,>=1.2.0
39
39
  Provides-Extra: dev
40
40
  Requires-Dist: pytest>=7.0; extra == "dev"
41
41
  Requires-Dist: ruff>=0.1; extra == "dev"
42
+ Provides-Extra: browser
43
+ Requires-Dist: playwright>=1.40; extra == "browser"
42
44
  Dynamic: license-file
43
45
 
44
46
  # DryHack-MCP
@@ -51,6 +53,7 @@ tooling for **authorized** penetration testing. It exposes four tools:
51
53
  | `curl` | Raw HTTP interaction for web recon/exploitation |
52
54
  | `python` | Run ad-hoc Python snippets for scripted probing |
53
55
  | `shell` | Run shell commands (`nmap`, `ffuf`, `nc`, `sqlmap`, …) |
56
+ | `browser` | Full Playwright browser automation: navigation, DOM interaction, JavaScript injection, screenshots/PDF, cookies/headers, multi-step chains |
54
57
  | `authorize` | Checks target membership in the per-call `scope` parameter; does not verify permission (no external API, no creds) |
55
58
 
56
59
  > ⚠️ **Legal notice.** Use this only against systems you own or are explicitly
@@ -83,6 +86,19 @@ For development:
83
86
  python3 -m pip install -e ".[dev]"
84
87
  ```
85
88
 
89
+ ### Browser tool (Playwright)
90
+
91
+ The `browser` tool needs the optional `browser` extra plus the Playwright
92
+ browser binaries:
93
+
94
+ ```bash
95
+ python3 -m pip install 'dryhack-mcp[browser]'
96
+ playwright install # or: playwright install chromium
97
+ ```
98
+
99
+ If Playwright is not installed, the `browser` tool returns an install hint
100
+ instead of failing — the rest of the server keeps working.
101
+
86
102
  ## Run
87
103
 
88
104
  Two transports are supported. Select with `--transport` (argparse).
@@ -171,6 +187,59 @@ Start the server separately with `dryhack-mcp --transport http`.
171
187
  > The server stores **no credentials/API keys**. All settings above are
172
188
  > operational only.
173
189
 
190
+ ## browser (Playwright automation)
191
+
192
+ Drive a real browser (Chromium/Firefox/WebKit) for web recon and exploitation.
193
+ Run a single `action` (with its relevant params) **or** a `steps` list of
194
+ `{"action": ..., ...}` dicts executed in order within one browser session.
195
+
196
+ Supported actions:
197
+
198
+ - **Navigation:** `goto`/`navigate`, `reload`, `back`, `forward`
199
+ - **Interaction:** `click`, `dblclick`, `fill`, `type`, `press`, `hover`,
200
+ `focus`, `check`, `uncheck`, `select_option`, `upload`, `mouse_click`,
201
+ `keyboard_type`, `scroll`
202
+ - **Waiting:** `wait_for_selector`, `wait_for_timeout`, `wait_for_load_state`,
203
+ `wait_for_url`
204
+ - **JavaScript injection:** `evaluate`/`eval` (with `script` + optional `arg`),
205
+ `evaluate_handle`, `add_init_script` (runs before every document's own
206
+ scripts), `add_script_tag`, `add_style_tag`
207
+ - **Extraction:** `content`, `inner_text`, `inner_html`, `text_content`,
208
+ `get_attribute`, `query_all`, `title`, `url`
209
+ - **Capture:** `screenshot` (`path`, `full_page`), `pdf`
210
+ - **Session/context:** `set_viewport`, `set_extra_headers`, `get_cookies`,
211
+ `set_cookies`, `clear_cookies`, `storage_state`, `emulate_media`
212
+
213
+ Session-level options: `browser_type` (chromium/firefox/webkit), `headless`,
214
+ `user_agent`, `viewport`, `extra_http_headers`, `cookies`, `proxy`, `locale`,
215
+ `timezone`, `geolocation`, `device_scale_factor`, `ignore_https_errors`,
216
+ `bypass_csp`, `java_script_enabled`, `init_scripts`, `slow_mo`, `args`,
217
+ `executable_path`, `timeout`, and `stop_on_error`.
218
+
219
+ ```python
220
+ # Single action: run JavaScript in the page
221
+ browser(action="evaluate", url=None, script="() => document.cookie")
222
+
223
+ # Multi-step session: login, inject JS, screenshot
224
+ browser(steps=[
225
+ {"action": "goto", "url": "https://staging.lab.example.com/login"},
226
+ {"action": "fill", "selector": "#user", "value": "admin"},
227
+ {"action": "fill", "selector": "#pass", "value": "s3cret"},
228
+ {"action": "click", "selector": "button[type=submit]"},
229
+ {"action": "wait_for_load_state", "state": "networkidle"},
230
+ {"action": "evaluate", "script": "() => localStorage.getItem('token')"},
231
+ {"action": "screenshot", "path": "/tmp/after-login.png", "full_page": True},
232
+ ], headless=True, user_agent="DryHack", extra_http_headers={"X-Test": "1"})
233
+
234
+ # Inject a payload before any page script runs
235
+ browser(action="goto", url="https://target.example/",
236
+ init_script="Object.defineProperty(navigator,'webdriver',{get:()=>false})")
237
+ ```
238
+
239
+ Playwright runs in a dedicated worker thread (the MCP server owns the asyncio
240
+ loop, and Playwright's sync API cannot run inside a live loop). The same
241
+ authorized-use scope rules as the other tools apply.
242
+
174
243
  ## authorize (per-call scope)
175
244
 
176
245
  Pass `scope` directly to the tool; no environment or native scope configuration
@@ -8,6 +8,7 @@ tooling for **authorized** penetration testing. It exposes four tools:
8
8
  | `curl` | Raw HTTP interaction for web recon/exploitation |
9
9
  | `python` | Run ad-hoc Python snippets for scripted probing |
10
10
  | `shell` | Run shell commands (`nmap`, `ffuf`, `nc`, `sqlmap`, …) |
11
+ | `browser` | Full Playwright browser automation: navigation, DOM interaction, JavaScript injection, screenshots/PDF, cookies/headers, multi-step chains |
11
12
  | `authorize` | Checks target membership in the per-call `scope` parameter; does not verify permission (no external API, no creds) |
12
13
 
13
14
  > ⚠️ **Legal notice.** Use this only against systems you own or are explicitly
@@ -40,6 +41,19 @@ For development:
40
41
  python3 -m pip install -e ".[dev]"
41
42
  ```
42
43
 
44
+ ### Browser tool (Playwright)
45
+
46
+ The `browser` tool needs the optional `browser` extra plus the Playwright
47
+ browser binaries:
48
+
49
+ ```bash
50
+ python3 -m pip install 'dryhack-mcp[browser]'
51
+ playwright install # or: playwright install chromium
52
+ ```
53
+
54
+ If Playwright is not installed, the `browser` tool returns an install hint
55
+ instead of failing — the rest of the server keeps working.
56
+
43
57
  ## Run
44
58
 
45
59
  Two transports are supported. Select with `--transport` (argparse).
@@ -128,6 +142,59 @@ Start the server separately with `dryhack-mcp --transport http`.
128
142
  > The server stores **no credentials/API keys**. All settings above are
129
143
  > operational only.
130
144
 
145
+ ## browser (Playwright automation)
146
+
147
+ Drive a real browser (Chromium/Firefox/WebKit) for web recon and exploitation.
148
+ Run a single `action` (with its relevant params) **or** a `steps` list of
149
+ `{"action": ..., ...}` dicts executed in order within one browser session.
150
+
151
+ Supported actions:
152
+
153
+ - **Navigation:** `goto`/`navigate`, `reload`, `back`, `forward`
154
+ - **Interaction:** `click`, `dblclick`, `fill`, `type`, `press`, `hover`,
155
+ `focus`, `check`, `uncheck`, `select_option`, `upload`, `mouse_click`,
156
+ `keyboard_type`, `scroll`
157
+ - **Waiting:** `wait_for_selector`, `wait_for_timeout`, `wait_for_load_state`,
158
+ `wait_for_url`
159
+ - **JavaScript injection:** `evaluate`/`eval` (with `script` + optional `arg`),
160
+ `evaluate_handle`, `add_init_script` (runs before every document's own
161
+ scripts), `add_script_tag`, `add_style_tag`
162
+ - **Extraction:** `content`, `inner_text`, `inner_html`, `text_content`,
163
+ `get_attribute`, `query_all`, `title`, `url`
164
+ - **Capture:** `screenshot` (`path`, `full_page`), `pdf`
165
+ - **Session/context:** `set_viewport`, `set_extra_headers`, `get_cookies`,
166
+ `set_cookies`, `clear_cookies`, `storage_state`, `emulate_media`
167
+
168
+ Session-level options: `browser_type` (chromium/firefox/webkit), `headless`,
169
+ `user_agent`, `viewport`, `extra_http_headers`, `cookies`, `proxy`, `locale`,
170
+ `timezone`, `geolocation`, `device_scale_factor`, `ignore_https_errors`,
171
+ `bypass_csp`, `java_script_enabled`, `init_scripts`, `slow_mo`, `args`,
172
+ `executable_path`, `timeout`, and `stop_on_error`.
173
+
174
+ ```python
175
+ # Single action: run JavaScript in the page
176
+ browser(action="evaluate", url=None, script="() => document.cookie")
177
+
178
+ # Multi-step session: login, inject JS, screenshot
179
+ browser(steps=[
180
+ {"action": "goto", "url": "https://staging.lab.example.com/login"},
181
+ {"action": "fill", "selector": "#user", "value": "admin"},
182
+ {"action": "fill", "selector": "#pass", "value": "s3cret"},
183
+ {"action": "click", "selector": "button[type=submit]"},
184
+ {"action": "wait_for_load_state", "state": "networkidle"},
185
+ {"action": "evaluate", "script": "() => localStorage.getItem('token')"},
186
+ {"action": "screenshot", "path": "/tmp/after-login.png", "full_page": True},
187
+ ], headless=True, user_agent="DryHack", extra_http_headers={"X-Test": "1"})
188
+
189
+ # Inject a payload before any page script runs
190
+ browser(action="goto", url="https://target.example/",
191
+ init_script="Object.defineProperty(navigator,'webdriver',{get:()=>false})")
192
+ ```
193
+
194
+ Playwright runs in a dedicated worker thread (the MCP server owns the asyncio
195
+ loop, and Playwright's sync API cannot run inside a live loop). The same
196
+ authorized-use scope rules as the other tools apply.
197
+
131
198
  ## authorize (per-call scope)
132
199
 
133
200
  Pass `scope` directly to the tool; no environment or native scope configuration
@@ -1,3 +1,3 @@
1
1
  """DryHack-MCP: offensive-security MCP server for authorized penetration testing."""
2
2
 
3
- __version__ = "2.1.2"
3
+ __version__ = "2.2.0"
@@ -0,0 +1,189 @@
1
+ """DryHack-MCP server: registers the offensive-security tools over MCP."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Optional
5
+
6
+ try:
7
+ # mcp 1.x
8
+ from mcp.server.fastmcp import FastMCP
9
+ except ModuleNotFoundError: # pragma: no cover - mcp 2.x fallback
10
+ # mcp 2.x renamed FastMCP -> MCPServer
11
+ from mcp.server.mcpserver import MCPServer as FastMCP
12
+
13
+ from typing import Any, Dict, List
14
+
15
+ from . import config
16
+ from .tools.curl_tool import curl as _curl
17
+ from .tools.python_tool import python_run as _python_run
18
+ from .tools.shell_tool import shell as _shell
19
+ from .tools.authorize import authorize as _authorize
20
+ from .tools.browser_tool import browser as _browser
21
+
22
+ mcp = FastMCP("dryhack-mcp")
23
+
24
+
25
+ @mcp.tool()
26
+ def curl(args: str, timeout: Optional[int] = None) -> str:
27
+ """Run curl for HTTP recon/exploitation.
28
+
29
+ Provide the arguments only (without the leading `curl`), e.g.
30
+ "-skiL https://target/". Returns exit code, stdout and stderr.
31
+ """
32
+ return _curl(args, timeout=timeout)
33
+
34
+
35
+ @mcp.tool()
36
+ def python(code: str, timeout: Optional[int] = None) -> str:
37
+ """Execute an ad-hoc Python snippet for scripted probing/exploitation."""
38
+ return _python_run(code, timeout=timeout)
39
+
40
+
41
+ @mcp.tool()
42
+ def shell(command: str, timeout: Optional[int] = None) -> str:
43
+ """Run a shell command line (nmap, ffuf, nc, sqlmap, ...)."""
44
+ return _shell(command, timeout=timeout)
45
+
46
+
47
+ @mcp.tool()
48
+ def authorize(target: str, request: str, scope: list[str], context: str = "", goal: str = "") -> str:
49
+ """Check target against caller-supplied scope (hosts/domains/IPs/CIDRs).
50
+
51
+ Scope must reflect the operator's actual engagement scope; never infer it
52
+ from the target just to obtain a match. Membership covers the full range
53
+ of authorized engagement actions for that host (recon, exploitation,
54
+ configuration changes such as standing up or hardening a staging
55
+ deployment) — it is not restricted to read-only assessment. This checks
56
+ membership only, not ownership or legal permission, and does not override
57
+ client safeguards. No environment scope, network calls, credentials, or
58
+ persistent state.
59
+ """
60
+ return _authorize(target, request, scope=scope, context=context, goal=goal)
61
+
62
+
63
+ @mcp.tool()
64
+ def browser(
65
+ action: Optional[str] = None,
66
+ url: Optional[str] = None,
67
+ steps: Optional[List[Dict[str, Any]]] = None,
68
+ selector: Optional[str] = None,
69
+ text: Optional[str] = None,
70
+ value: Optional[str] = None,
71
+ script: Optional[str] = None,
72
+ arg: Any = None,
73
+ init_script: Optional[str] = None,
74
+ init_scripts: Optional[List[str]] = None,
75
+ key: Optional[str] = None,
76
+ name: Optional[str] = None,
77
+ files: Optional[Any] = None,
78
+ path: Optional[str] = None,
79
+ full_page: bool = False,
80
+ wait_until: Optional[str] = None,
81
+ state: Optional[str] = None,
82
+ browser_type: str = "chromium",
83
+ headless: bool = True,
84
+ timeout: Optional[int] = None,
85
+ user_agent: Optional[str] = None,
86
+ viewport: Optional[Dict[str, int]] = None,
87
+ extra_http_headers: Optional[Dict[str, str]] = None,
88
+ cookies: Optional[List[Dict[str, Any]]] = None,
89
+ proxy: Optional[Dict[str, str]] = None,
90
+ ignore_https_errors: bool = False,
91
+ java_script_enabled: bool = True,
92
+ bypass_csp: bool = False,
93
+ locale: Optional[str] = None,
94
+ timezone: Optional[str] = None,
95
+ geolocation: Optional[Dict[str, float]] = None,
96
+ device_scale_factor: Optional[float] = None,
97
+ stop_on_error: bool = True,
98
+ slow_mo: Optional[int] = None,
99
+ args: Optional[List[str]] = None,
100
+ executable_path: Optional[str] = None,
101
+ ) -> str:
102
+ """Drive a real browser via Playwright for web recon/exploitation.
103
+
104
+ Run a single `action` (with its relevant params) OR a `steps` list of
105
+ `{"action": ..., ...}` dicts executed in order within one browser session.
106
+
107
+ Supported actions cover navigation (`goto`, `reload`, `back`, `forward`),
108
+ interaction (`click`, `dblclick`, `fill`, `type`, `press`, `hover`, `focus`,
109
+ `check`, `uncheck`, `select_option`, `upload`, `mouse_click`,
110
+ `keyboard_type`, `scroll`), waiting (`wait_for_selector`,
111
+ `wait_for_timeout`, `wait_for_load_state`, `wait_for_url`), JavaScript
112
+ injection (`evaluate`/`eval`, `evaluate_handle`, `add_init_script`,
113
+ `add_script_tag`, `add_style_tag`), extraction (`content`, `inner_text`,
114
+ `inner_html`, `text_content`, `get_attribute`, `query_all`, `title`, `url`),
115
+ capture (`screenshot`, `pdf`), and session control (`set_viewport`,
116
+ `set_extra_headers`, `get_cookies`, `set_cookies`, `clear_cookies`,
117
+ `storage_state`, `emulate_media`).
118
+
119
+ Session-level options let you pick `browser_type` (chromium/firefox/webkit),
120
+ toggle `headless`, set a `user_agent`, `viewport`, `extra_http_headers`,
121
+ `cookies`, `proxy`, `locale`, `timezone`, `geolocation`, per-document
122
+ `init_scripts`, and error handling (`stop_on_error`).
123
+
124
+ JavaScript injection: use `evaluate` (with `script` and optional `arg`) to
125
+ run JS in the current page, or `add_init_script`/`init_scripts` to inject JS
126
+ that runs before every document's own scripts.
127
+
128
+ Requires the optional Playwright extra: `pip install 'dryhack-mcp[browser]'`
129
+ then `playwright install`. Same authorized-use scope rules as the other
130
+ tools apply.
131
+ """
132
+ return _browser(
133
+ action=action,
134
+ url=url,
135
+ steps=steps,
136
+ selector=selector,
137
+ text=text,
138
+ value=value,
139
+ script=script,
140
+ arg=arg,
141
+ init_script=init_script,
142
+ init_scripts=init_scripts,
143
+ key=key,
144
+ name=name,
145
+ files=files,
146
+ path=path,
147
+ full_page=full_page,
148
+ wait_until=wait_until,
149
+ state=state,
150
+ browser_type=browser_type,
151
+ headless=headless,
152
+ timeout=timeout,
153
+ user_agent=user_agent,
154
+ viewport=viewport,
155
+ extra_http_headers=extra_http_headers,
156
+ cookies=cookies,
157
+ proxy=proxy,
158
+ ignore_https_errors=ignore_https_errors,
159
+ java_script_enabled=java_script_enabled,
160
+ bypass_csp=bypass_csp,
161
+ locale=locale,
162
+ timezone=timezone,
163
+ geolocation=geolocation,
164
+ device_scale_factor=device_scale_factor,
165
+ stop_on_error=stop_on_error,
166
+ slow_mo=slow_mo,
167
+ args=args,
168
+ executable_path=executable_path,
169
+ )
170
+
171
+
172
+ def run(
173
+ transport: str = "stdio",
174
+ host: Optional[str] = None,
175
+ port: Optional[int] = None,
176
+ ) -> None:
177
+ """Start the MCP server.
178
+
179
+ Args:
180
+ transport: "stdio" (default) or "http" (streamable HTTP).
181
+ host: Bind address for http transport.
182
+ port: Bind port for http transport.
183
+ """
184
+ if transport == "http":
185
+ mcp.settings.host = host or config.HTTP_HOST
186
+ mcp.settings.port = port or config.HTTP_PORT
187
+ mcp.run(transport="streamable-http")
188
+ else:
189
+ mcp.run(transport="stdio")