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`, …) |
| `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
uvx dryhack-mcp
# http transport
uvx dryhack-mcp --transport http --host 0.0.0.0 --port 8000
# pin a version
uvx dryhack-mcp@0.1.3
```

For development:

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

## 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": ["dryhack-mcp"],
      "env": {
        "DRYHACK_COMMAND_TIMEOUT": "120"
      }
    }
  }
}
```

### 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_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.

## 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.