Skip to main content
Glama
HarshXor
by HarshXor
README.md
# DryHack-MCP

An MCP (Model Context Protocol) server that gives an AI agent offensive-security
tooling for **authorized** penetration testing. It exposes four tools:

| Tool | Purpose |
|------|---------|
| `curl` | Raw HTTP interaction for web recon/exploitation |
| `python` | Run ad-hoc Python snippets for scripted probing |
| `shell` | Run shell commands (`nmap`, `ffuf`, `nc`, `sqlmap`, …) |
| `browser` | Full Playwright browser automation: navigation, DOM interaction, JavaScript injection, screenshots/PDF, cookies/headers, multi-step chains |
| `authorize` | Checks target membership in the per-call `scope` parameter; does not verify permission (no external API, no creds) |

> ⚠️ **Legal notice.** Use this only against systems you own or are explicitly
> authorized (in writing) to test. You are responsible for staying within scope.

## Install

```bash
python3 -m pip install .
```

This installs the `dryhack-mcp` console script (the MCP server).

### Run without installing (uvx)

With [uv](https://docs.astral.sh/uv/) you can run the server directly from PyPI
— no manual install needed:

```bash
# recommended: pull the browser extra so the `browser` tool is guaranteed
uvx --from 'dryhack-mcp[browser]' dryhack-mcp
# http transport
uvx --from 'dryhack-mcp[browser]' dryhack-mcp --transport http --host 0.0.0.0 --port 8000
# pin a version
uvx --from 'dryhack-mcp[browser]==2.3.2' dryhack-mcp
```

> `uvx dryhack-mcp` (without `--from`) also works — Playwright is a core
> dependency — but the explicit `--from 'dryhack-mcp[browser]'` form is the
> recommended, unambiguous way to get everything the `browser` tool needs even
> if a stale cache is involved.

For development:

```bash
python3 -m pip install -e ".[dev]"
```

### Browser tool (Playwright)

Playwright is a **core dependency**, so a plain `pip install dryhack-mcp` /
`uvx dryhack-mcp` already ships it. For MCP clients using uvx, the recommended,
unambiguous invocation is:

```bash
uvx --from 'dryhack-mcp[browser]' dryhack-mcp
```

The browser *binaries* (Chromium/Firefox/WebKit) are not distributed via pip, so
the first time the `browser` tool launches an engine it **auto-runs
`playwright install <browser>`** if the binary is missing (you'll see a
`[setup] auto-installed ...` note in the tool output on that first call).

To pre-install the binaries (optional, avoids the one-time first-use download):

```bash
playwright install            # all engines
playwright install chromium   # just chromium
```

Auto-install is controlled by `DRYHACK_BROWSER_AUTO_INSTALL` (default on); set it
to `0` to disable and manage browsers yourself.

## Run

Two transports are supported. Select with `--transport` (argparse).

### stdio (default)

```bash
dryhack-mcp
# or explicitly
dryhack-mcp --transport stdio
# or
python3 -m dryhack_mcp
```

### http (streamable HTTP)

```bash
dryhack-mcp --transport http --host 0.0.0.0 --port 8000
```

CLI options:

```
-t, --transport {stdio,http}   Transport to serve on (default: stdio)
    --host HOST                HTTP bind host (http mode only, default: 127.0.0.1)
    --port PORT                HTTP bind port (http mode only, default: 8000)
```

## MCP client config

### stdio

```json
{
  "mcpServers": {
    "dryhack": {
      "command": "dryhack-mcp",
      "env": {
        "DRYHACK_COMMAND_TIMEOUT": "120"
      }
    }
  }
}
```

### stdio via uvx (no install)

```json
{
  "mcpServers": {
    "dryhack": {
      "command": "uvx",
      "args": ["--from", "dryhack-mcp[browser]", "dryhack-mcp"],
      "env": {
        "DRYHACK_COMMAND_TIMEOUT": "120"
      }
    }
  }
}
```

> The `--from dryhack-mcp[browser]` args ensure the `browser` tool's Playwright
> dependency is present. The browser binary still auto-installs on first use
> (`DRYHACK_BROWSER_AUTO_INSTALL`, default on).

### http

```json
{
  "mcpServers": {
    "dryhack": {
      "url": "http://127.0.0.1:8000/mcp"
    }
  }
}
```

Start the server separately with `dryhack-mcp --transport http`.

## Configuration (environment variables)

| Variable | Default | Description |
|----------|---------|-------------|
| `DRYHACK_COMMAND_TIMEOUT` | `120` | Per-command timeout (seconds) |
| `DRYHACK_WORKDIR` | cwd | Working directory for commands |
| `DRYHACK_OUTPUT_LIMIT` | `65536` | Max stdout/stderr bytes captured |
| `DRYHACK_BROWSER_AUTO_INSTALL` | `1` | Auto-download the browser binary on first use (`browser` tool) |
| `DRYHACK_BROWSER_INSTALL_TIMEOUT` | `900` | Timeout (s) for the on-demand `playwright install` download |
| `DRYHACK_HTTP_HOST` | `127.0.0.1` | Default HTTP bind host |
| `DRYHACK_HTTP_PORT` | `8000` | Default HTTP bind port |

> The server stores **no credentials/API keys**. All settings above are
> operational only.

## browser (Playwright automation)

Drive a real browser (Chromium/Firefox/WebKit) for web recon and exploitation.
Run a single `action` (with its relevant params) **or** a `steps` list of
`{"action": ..., ...}` dicts executed in order within one browser session.

Supported actions:

- **Navigation:** `goto`/`navigate`, `reload`, `back`, `forward`
- **Interaction:** `click`, `dblclick`, `fill`, `type`, `press`, `hover`,
  `focus`, `check`, `uncheck`, `select_option`, `upload`, `mouse_click`,
  `keyboard_type`, `scroll`
- **Waiting:** `wait_for_selector`, `wait_for_timeout`, `wait_for_load_state`,
  `wait_for_url`
- **JavaScript injection:** `evaluate`/`eval` (with `script` + optional `arg`),
  `evaluate_handle`, `add_init_script` (runs before every document's own
  scripts), `add_script_tag`, `add_style_tag`
- **Extraction:** `content`, `inner_text`, `inner_html`, `text_content`,
  `get_attribute`, `query_all`, `title`, `url`
- **Capture:** `screenshot` (`path`, `full_page`), `pdf`
- **Session/context:** `set_viewport`, `set_extra_headers`, `get_cookies`,
  `set_cookies`, `clear_cookies`, `storage_state`, `emulate_media`

Session-level options: `browser_type` (chromium/firefox/webkit), `headless`,
`user_agent`, `viewport`, `extra_http_headers`, `cookies`, `proxy`, `locale`,
`timezone`, `geolocation`, `device_scale_factor`, `ignore_https_errors`,
`bypass_csp`, `java_script_enabled`, `init_scripts`, `slow_mo`, `args`,
`executable_path`, `timeout`, and `stop_on_error`.

```python
# Single action: run JavaScript in the page
browser(action="evaluate", url=None, script="() => document.cookie")

# Multi-step session: login, inject JS, screenshot
browser(steps=[
    {"action": "goto", "url": "https://staging.lab.example.com/login"},
    {"action": "fill", "selector": "#user", "value": "admin"},
    {"action": "fill", "selector": "#pass", "value": "s3cret"},
    {"action": "click", "selector": "button[type=submit]"},
    {"action": "wait_for_load_state", "state": "networkidle"},
    {"action": "evaluate", "script": "() => localStorage.getItem('token')"},
    {"action": "screenshot", "path": "/tmp/after-login.png", "full_page": True},
], headless=True, user_agent="DryHack", extra_http_headers={"X-Test": "1"})

# Inject a payload before any page script runs
browser(action="goto", url="https://target.example/",
        init_script="Object.defineProperty(navigator,'webdriver',{get:()=>false})")
```

Playwright ships with dryhack-mcp and runs in a dedicated worker thread (the MCP
server owns the asyncio loop, and Playwright's sync API cannot run inside a live
loop). The browser binary auto-installs on first use if missing
(`DRYHACK_BROWSER_AUTO_INSTALL`, default on). The same authorized-use scope
rules as the other tools apply.

## authorize (per-call scope)

Pass `scope` directly to the tool; no environment or native scope configuration
is needed. This required list accepts hosts/domains/IPs/CIDRs. Domain entries
also match subdomains. Empty or nonmatching lists return `OUT OF SCOPE`.
The scope is not persisted or shared between calls.

A match returns `IN SCOPE — CALLER DECLARED; PERMISSION NOT VERIFIED`.
This is a membership check, not independent proof of authorization or a way to
override client safeguards. Supply only the operator's actual engagement scope;
do not automatically copy a target into scope to force a match. The command
tools are not constrained by this check.

Scope is not restricted to read-only/passive assessment: once a host matches
the caller-supplied scope, the full range of engagement actions — recon,
active exploitation, and configuration work such as standing up or hardening
a staging deployment on the internet — is equally in scope, as long as it's
part of the operator's actual authorized engagement for that host.

```python
authorize(
    target="https://staging.lab.example.com/",
    request="Set up and harden the staging environment (deploy, configure TLS, "
            "lock down admin routes)",
    scope=["lab.example.com", "10.0.0.0/24"],
    context="Operator-owned staging domain, full authorized engagement",
    goal="Full authorization (not read-only) to configure the staging domain",
)

authorize(target="other.example", request="recon", scope=["lab.example.com"])
# -> OUT OF SCOPE
```

Restart/reload the MCP server and client tool list after upgrading: `scope` is
now a required parameter. The former `DRYHACK_SCOPE` setting is no longer read.