Skip to main content
Glama
README.md
# ctfd-mcp

A tiny, **standalone MCP server for any [CTFd](https://ctfd.io/)-based CTF.** Point your
agent (opencode, Claude Code, Claude Desktop, Cursor, …) at it and it can browse and read
challenges, download their files, **submit flags**, watch the scoreboard — and, when the
CTF uses the **ctfd-whale** plugin, start/stop your per-team instances. Two env vars, no
framework to learn, no repo to graft into, no hours of debugging.

- **Self-contained** — the only dependencies are `mcp` and `httpx`.
- **Works on any CTFd** — token auth, team *or* user mode (auto-detected), pagination.
- **Robust** — every tool returns clean JSON, even on errors (a closed CTF returns
  `{"error":"HTTP 403", ...}` instead of crashing your session).
- **ctfd-whale aware** — dynamic instance tools auto-disable when the plugin isn't present.

## Install

```bash
# Recommended — installs from THIS repo and puts `ctfd-mcp` on your PATH:
pipx install "git+https://github.com/sanjarbiy/ctfd-mcp"
# or with pip:
pip install "git+https://github.com/sanjarbiy/ctfd-mcp"

# --- or from a local clone ---
git clone https://github.com/sanjarbiy/ctfd-mcp && cd ctfd-mcp
python3 -m venv .venv && .venv/bin/pip install -e .    # Windows: .venv\Scripts\pip install -e .
```

> **⚠️ Install from the repo URL, not the bare name.** An *unrelated* package called `ctfd-mcp`
> exists on PyPI — a plain `pip install ctfd-mcp` would fetch **that**, not this project.
> Also requires `mcp>=1.2,<2` (FastMCP 1.x runs async tools reliably; `mcp` 2.x is intentionally excluded)
> — the pin is handled for you when you install from the URL above.

## Configure

Get an **Access Token** in CTFd: *Settings → Access Tokens → Generate* (`ctfd_...`).
Then set two env vars (or drop a `.env` next to where you run it — see `.env.example`):

| var | required | meaning |
|---|---|---|
| `CTFD_URL` | ✅ | e.g. `https://demo.ctfd.io` |
| `CTFD_TOKEN` | ✅ | your CTFd access token |
| `CTFD_FILES_DIR` | | download dir (default `ctf_files`) |
| `CTFD_TIMEOUT` | | per-request seconds (default `25`) |
| `CTFD_VERIFY_TLS` | | `0` disables TLS verification for self-signed lab CTFds — **the API token then rides an unverified connection, so use only on a trusted LAN** |
| `CTFD_ALLOW_PRIVATE_FETCH` | | `fetch_url`/downloads may reach private/loopback hosts (default `1`, since CTF instances live on lab LANs/VPNs); set `0` to block RFC1918/loopback. Cloud-metadata / link-local (169.254.x) is **always** blocked |

### Add it to your agent

**opencode** (`~/.config/opencode/opencode.jsonc` → `"mcp"`):
```json
"ctfd": {
  "type": "local",
  "command": ["ctfd-mcp"],
  "environment": { "CTFD_URL": "https://YOUR-CTF", "CTFD_TOKEN": "ctfd_..." },
  "enabled": true
}
```

**Claude Code**:
```bash
claude mcp add ctfd -s user -e CTFD_URL=https://YOUR-CTF -e CTFD_TOKEN=ctfd_... -- ctfd-mcp
```

**Claude Desktop / Cursor** (`mcpServers`):
```json
"ctfd": { "command": "ctfd-mcp", "env": { "CTFD_URL": "https://YOUR-CTF", "CTFD_TOKEN": "ctfd_..." } }
```

(If you didn't `pip install`, use the venv python: `"command": "/path/ctfd-mcp/.venv/bin/python", "args": ["-m","ctfd_mcp"]`.)

## Tools

| tool | what it does |
|---|---|
| `ctf_list(unsolved, category, name, limit)` | list challenges, easy-first (most solves) |
| `ctf_show(challenge)` | full details: description, connection_info, files, hints, tags |
| `ctf_download(challenge)` | download the challenge's CTFd-hosted files |
| `ctf_submit(challenge, flag)` | submit a flag → `{status, message}` |
| `ctf_solved()` | names you/your team already solved |
| `ctf_hints(challenge, unlock)` | list hints (optionally spend points to unlock) |
| `ctf_scoreboard(top)` | live top-N standings |
| `ctf_me()` | your team/user info (auto-detects CTFd mode) |
| `fetch_url(url)` | GET any file off a challenge instance/service |
| `whale_list()` | *[ctfd-whale]* your live dynamic instances |
| `whale_start(challenge)` | *[ctfd-whale]* start an instance → host/URL |
| `whale_stop(challenge)` | *[ctfd-whale]* destroy your instance |

`challenge` is an **id or a name** (exact match wins, else substring). Every tool returns JSON.

## Typical flow

```
ctf_list unsolved=true              # triage, easy-first
ctf_show "Baby RSA"                 # read it
ctf_download "Baby RSA"             # get the files  (or: whale_start + fetch_url for dynamic ones)
# ...you solve it...
ctf_submit "Baby RSA" "flag{...}"   # submit
```

## Troubleshooting

- **`{"error":"HTTP 403"}` on challenge tools** — the CTF hasn't started, has ended, or the
  token lacks access. `ctf_me` still works (it confirms your token is valid).
- **`{"error":"HTTP 401"}`** — bad/expired `CTFD_TOKEN`.
- **ctfd-whale tools return `not available`** — this CTF has no ctfd-whale plugin; use
  `ctf_download` (static files) instead.
- **Self-signed lab CTFd** — set `CTFD_VERIFY_TLS=0` (trusted networks only; see the env table).
- Only `mcp>=1.2,<2` is supported (async tools break under `mcp` 2.x).

## Security notes

This server is driven by an LLM whose context fills with **untrusted** challenge text (descriptions,
hints, files). Keep that in mind:

- **`fetch_url` is a fetch-any-URL tool.** A crafted challenge could try to make the agent fetch an
  internal address (SSRF). It refuses non-`http(s)` schemes and **always** blocks cloud-metadata /
  link-local (`169.254.x`); private/loopback hosts are allowed by default (real CTF instances live
  there) — set `CTFD_ALLOW_PRIVATE_FETCH=0` to lock that down. Only the initial host is checked, not
  redirect targets.
- **File downloads never carry your API token** (they use a token-less client), so a malicious file
  URL can't exfiltrate `CTFD_TOKEN`.
- Your token lives only in the process env / a local `.env` (git-ignored) — never commit it.

## License

MIT — see [LICENSE](LICENSE).

TDQS

A3.7/5.0

Scored across 12 tools

Disambiguation4/5

Most tools target distinct resources/actions (list, show, download, submit, hints, scoreboard, me, whale instances). Minor overlap between ctf_solved and ctf_list (which can hide solved), and ctf_me vs ctf_scoreboard, but descriptions clarify boundaries well.

Naming Consistency4/5

Clean ctf_-prefixed verb/noun pattern across the core eight tools, with whale_* grouping the plugin tools consistently. fetch_url deviates from the prefix scheme but is a single understandable outlier.

Tool Count5/5

12 tools is well-scoped for a CTFd client, covering the full player workflow plus dynamic instances without redundancy. Each tool earns its place.

Completeness4/5

Strong lifecycle coverage: discover, inspect, download, submit, hints, scoreboard, personal info, and whale instance control. Missing team join/create and some auth management, but core competitive workflows are covered.

Maintenance

ActivityMaintained
ResponsivenessNo issues