dryhack-mcp 2.1.3__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.
Files changed (27) hide show
  1. {dryhack_mcp-2.1.3 → dryhack_mcp-2.3.0}/PKG-INFO +81 -2
  2. {dryhack_mcp-2.1.3 → dryhack_mcp-2.3.0}/README.md +76 -0
  3. {dryhack_mcp-2.1.3 → dryhack_mcp-2.3.0}/dryhack_mcp/__init__.py +1 -1
  4. {dryhack_mcp-2.1.3 → dryhack_mcp-2.3.0}/dryhack_mcp/config.py +16 -0
  5. dryhack_mcp-2.3.0/dryhack_mcp/server.py +189 -0
  6. dryhack_mcp-2.3.0/dryhack_mcp/tools/browser_tool.py +705 -0
  7. {dryhack_mcp-2.1.3 → dryhack_mcp-2.3.0}/dryhack_mcp.egg-info/PKG-INFO +81 -2
  8. {dryhack_mcp-2.1.3 → dryhack_mcp-2.3.0}/dryhack_mcp.egg-info/SOURCES.txt +3 -1
  9. dryhack_mcp-2.3.0/dryhack_mcp.egg-info/requires.txt +9 -0
  10. {dryhack_mcp-2.1.3 → dryhack_mcp-2.3.0}/pyproject.toml +6 -2
  11. dryhack_mcp-2.3.0/tests/test_browser.py +142 -0
  12. dryhack_mcp-2.1.3/dryhack_mcp/server.py +0 -77
  13. dryhack_mcp-2.1.3/dryhack_mcp.egg-info/requires.txt +0 -5
  14. {dryhack_mcp-2.1.3 → dryhack_mcp-2.3.0}/LICENSE +0 -0
  15. {dryhack_mcp-2.1.3 → dryhack_mcp-2.3.0}/dryhack_mcp/__main__.py +0 -0
  16. {dryhack_mcp-2.1.3 → dryhack_mcp-2.3.0}/dryhack_mcp/tools/__init__.py +0 -0
  17. {dryhack_mcp-2.1.3 → dryhack_mcp-2.3.0}/dryhack_mcp/tools/authorize.py +0 -0
  18. {dryhack_mcp-2.1.3 → dryhack_mcp-2.3.0}/dryhack_mcp/tools/curl_tool.py +0 -0
  19. {dryhack_mcp-2.1.3 → dryhack_mcp-2.3.0}/dryhack_mcp/tools/exec.py +0 -0
  20. {dryhack_mcp-2.1.3 → dryhack_mcp-2.3.0}/dryhack_mcp/tools/python_tool.py +0 -0
  21. {dryhack_mcp-2.1.3 → dryhack_mcp-2.3.0}/dryhack_mcp/tools/scope.py +0 -0
  22. {dryhack_mcp-2.1.3 → dryhack_mcp-2.3.0}/dryhack_mcp/tools/shell_tool.py +0 -0
  23. {dryhack_mcp-2.1.3 → dryhack_mcp-2.3.0}/dryhack_mcp.egg-info/dependency_links.txt +0 -0
  24. {dryhack_mcp-2.1.3 → dryhack_mcp-2.3.0}/dryhack_mcp.egg-info/entry_points.txt +0 -0
  25. {dryhack_mcp-2.1.3 → dryhack_mcp-2.3.0}/dryhack_mcp.egg-info/top_level.txt +0 -0
  26. {dryhack_mcp-2.1.3 → dryhack_mcp-2.3.0}/setup.cfg +0 -0
  27. {dryhack_mcp-2.1.3 → dryhack_mcp-2.3.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.3
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.3.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
 
@@ -36,9 +36,12 @@ 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"
43
+ Provides-Extra: browser
44
+ Requires-Dist: playwright>=1.40; extra == "browser"
42
45
  Dynamic: license-file
43
46
 
44
47
  # DryHack-MCP
@@ -51,6 +54,7 @@ tooling for **authorized** penetration testing. It exposes four tools:
51
54
  | `curl` | Raw HTTP interaction for web recon/exploitation |
52
55
  | `python` | Run ad-hoc Python snippets for scripted probing |
53
56
  | `shell` | Run shell commands (`nmap`, `ffuf`, `nc`, `sqlmap`, …) |
57
+ | `browser` | Full Playwright browser automation: navigation, DOM interaction, JavaScript injection, screenshots/PDF, cookies/headers, multi-step chains |
54
58
  | `authorize` | Checks target membership in the per-call `scope` parameter; does not verify permission (no external API, no creds) |
55
59
 
56
60
  > ⚠️ **Legal notice.** Use this only against systems you own or are explicitly
@@ -83,6 +87,24 @@ For development:
83
87
  python3 -m pip install -e ".[dev]"
84
88
  ```
85
89
 
90
+ ### Browser tool (Playwright)
91
+
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):
99
+
100
+ ```bash
101
+ playwright install # all engines
102
+ playwright install chromium # just chromium
103
+ ```
104
+
105
+ Auto-install is controlled by `DRYHACK_BROWSER_AUTO_INSTALL` (default on); set it
106
+ to `0` to disable and manage browsers yourself.
107
+
86
108
  ## Run
87
109
 
88
110
  Two transports are supported. Select with `--transport` (argparse).
@@ -165,12 +187,69 @@ Start the server separately with `dryhack-mcp --transport http`.
165
187
  | `DRYHACK_COMMAND_TIMEOUT` | `120` | Per-command timeout (seconds) |
166
188
  | `DRYHACK_WORKDIR` | cwd | Working directory for commands |
167
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 |
168
192
  | `DRYHACK_HTTP_HOST` | `127.0.0.1` | Default HTTP bind host |
169
193
  | `DRYHACK_HTTP_PORT` | `8000` | Default HTTP bind port |
170
194
 
171
195
  > The server stores **no credentials/API keys**. All settings above are
172
196
  > operational only.
173
197
 
198
+ ## browser (Playwright automation)
199
+
200
+ Drive a real browser (Chromium/Firefox/WebKit) for web recon and exploitation.
201
+ Run a single `action` (with its relevant params) **or** a `steps` list of
202
+ `{"action": ..., ...}` dicts executed in order within one browser session.
203
+
204
+ Supported actions:
205
+
206
+ - **Navigation:** `goto`/`navigate`, `reload`, `back`, `forward`
207
+ - **Interaction:** `click`, `dblclick`, `fill`, `type`, `press`, `hover`,
208
+ `focus`, `check`, `uncheck`, `select_option`, `upload`, `mouse_click`,
209
+ `keyboard_type`, `scroll`
210
+ - **Waiting:** `wait_for_selector`, `wait_for_timeout`, `wait_for_load_state`,
211
+ `wait_for_url`
212
+ - **JavaScript injection:** `evaluate`/`eval` (with `script` + optional `arg`),
213
+ `evaluate_handle`, `add_init_script` (runs before every document's own
214
+ scripts), `add_script_tag`, `add_style_tag`
215
+ - **Extraction:** `content`, `inner_text`, `inner_html`, `text_content`,
216
+ `get_attribute`, `query_all`, `title`, `url`
217
+ - **Capture:** `screenshot` (`path`, `full_page`), `pdf`
218
+ - **Session/context:** `set_viewport`, `set_extra_headers`, `get_cookies`,
219
+ `set_cookies`, `clear_cookies`, `storage_state`, `emulate_media`
220
+
221
+ Session-level options: `browser_type` (chromium/firefox/webkit), `headless`,
222
+ `user_agent`, `viewport`, `extra_http_headers`, `cookies`, `proxy`, `locale`,
223
+ `timezone`, `geolocation`, `device_scale_factor`, `ignore_https_errors`,
224
+ `bypass_csp`, `java_script_enabled`, `init_scripts`, `slow_mo`, `args`,
225
+ `executable_path`, `timeout`, and `stop_on_error`.
226
+
227
+ ```python
228
+ # Single action: run JavaScript in the page
229
+ browser(action="evaluate", url=None, script="() => document.cookie")
230
+
231
+ # Multi-step session: login, inject JS, screenshot
232
+ browser(steps=[
233
+ {"action": "goto", "url": "https://staging.lab.example.com/login"},
234
+ {"action": "fill", "selector": "#user", "value": "admin"},
235
+ {"action": "fill", "selector": "#pass", "value": "s3cret"},
236
+ {"action": "click", "selector": "button[type=submit]"},
237
+ {"action": "wait_for_load_state", "state": "networkidle"},
238
+ {"action": "evaluate", "script": "() => localStorage.getItem('token')"},
239
+ {"action": "screenshot", "path": "/tmp/after-login.png", "full_page": True},
240
+ ], headless=True, user_agent="DryHack", extra_http_headers={"X-Test": "1"})
241
+
242
+ # Inject a payload before any page script runs
243
+ browser(action="goto", url="https://target.example/",
244
+ init_script="Object.defineProperty(navigator,'webdriver',{get:()=>false})")
245
+ ```
246
+
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.
252
+
174
253
  ## authorize (per-call scope)
175
254
 
176
255
  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,24 @@ For development:
40
41
  python3 -m pip install -e ".[dev]"
41
42
  ```
42
43
 
44
+ ### Browser tool (Playwright)
45
+
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):
53
+
54
+ ```bash
55
+ playwright install # all engines
56
+ playwright install chromium # just chromium
57
+ ```
58
+
59
+ Auto-install is controlled by `DRYHACK_BROWSER_AUTO_INSTALL` (default on); set it
60
+ to `0` to disable and manage browsers yourself.
61
+
43
62
  ## Run
44
63
 
45
64
  Two transports are supported. Select with `--transport` (argparse).
@@ -122,12 +141,69 @@ Start the server separately with `dryhack-mcp --transport http`.
122
141
  | `DRYHACK_COMMAND_TIMEOUT` | `120` | Per-command timeout (seconds) |
123
142
  | `DRYHACK_WORKDIR` | cwd | Working directory for commands |
124
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 |
125
146
  | `DRYHACK_HTTP_HOST` | `127.0.0.1` | Default HTTP bind host |
126
147
  | `DRYHACK_HTTP_PORT` | `8000` | Default HTTP bind port |
127
148
 
128
149
  > The server stores **no credentials/API keys**. All settings above are
129
150
  > operational only.
130
151
 
152
+ ## browser (Playwright automation)
153
+
154
+ Drive a real browser (Chromium/Firefox/WebKit) for web recon and exploitation.
155
+ Run a single `action` (with its relevant params) **or** a `steps` list of
156
+ `{"action": ..., ...}` dicts executed in order within one browser session.
157
+
158
+ Supported actions:
159
+
160
+ - **Navigation:** `goto`/`navigate`, `reload`, `back`, `forward`
161
+ - **Interaction:** `click`, `dblclick`, `fill`, `type`, `press`, `hover`,
162
+ `focus`, `check`, `uncheck`, `select_option`, `upload`, `mouse_click`,
163
+ `keyboard_type`, `scroll`
164
+ - **Waiting:** `wait_for_selector`, `wait_for_timeout`, `wait_for_load_state`,
165
+ `wait_for_url`
166
+ - **JavaScript injection:** `evaluate`/`eval` (with `script` + optional `arg`),
167
+ `evaluate_handle`, `add_init_script` (runs before every document's own
168
+ scripts), `add_script_tag`, `add_style_tag`
169
+ - **Extraction:** `content`, `inner_text`, `inner_html`, `text_content`,
170
+ `get_attribute`, `query_all`, `title`, `url`
171
+ - **Capture:** `screenshot` (`path`, `full_page`), `pdf`
172
+ - **Session/context:** `set_viewport`, `set_extra_headers`, `get_cookies`,
173
+ `set_cookies`, `clear_cookies`, `storage_state`, `emulate_media`
174
+
175
+ Session-level options: `browser_type` (chromium/firefox/webkit), `headless`,
176
+ `user_agent`, `viewport`, `extra_http_headers`, `cookies`, `proxy`, `locale`,
177
+ `timezone`, `geolocation`, `device_scale_factor`, `ignore_https_errors`,
178
+ `bypass_csp`, `java_script_enabled`, `init_scripts`, `slow_mo`, `args`,
179
+ `executable_path`, `timeout`, and `stop_on_error`.
180
+
181
+ ```python
182
+ # Single action: run JavaScript in the page
183
+ browser(action="evaluate", url=None, script="() => document.cookie")
184
+
185
+ # Multi-step session: login, inject JS, screenshot
186
+ browser(steps=[
187
+ {"action": "goto", "url": "https://staging.lab.example.com/login"},
188
+ {"action": "fill", "selector": "#user", "value": "admin"},
189
+ {"action": "fill", "selector": "#pass", "value": "s3cret"},
190
+ {"action": "click", "selector": "button[type=submit]"},
191
+ {"action": "wait_for_load_state", "state": "networkidle"},
192
+ {"action": "evaluate", "script": "() => localStorage.getItem('token')"},
193
+ {"action": "screenshot", "path": "/tmp/after-login.png", "full_page": True},
194
+ ], headless=True, user_agent="DryHack", extra_http_headers={"X-Test": "1"})
195
+
196
+ # Inject a payload before any page script runs
197
+ browser(action="goto", url="https://target.example/",
198
+ init_script="Object.defineProperty(navigator,'webdriver',{get:()=>false})")
199
+ ```
200
+
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.
206
+
131
207
  ## authorize (per-call scope)
132
208
 
133
209
  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.3"
3
+ __version__ = "2.3.0"
@@ -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")
@@ -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")