CTFd MCP Server

A Model Context Protocol (MCP) server for interacting with any CTFd v3 instance.
It lets AI tools (Claude Desktop, Cursor, custom agents, ...) authenticate, list
and inspect challenges, submit flags, and query instance state through a stable,
type-safe interface.
The project ships two interfaces built on the same client library:
- MCP tools (primary) —
ctfd_mcp_server.py, used over stdio or sse.
- REST API (optional) —
server/main.py, a FastAPI mirror for scripting,
debugging, and Docker deployments.
┌──────────────────────────────────────────────┐
AI agent / MCP │ FastMCP (MCP tools) │
client ────────►│ set_token · login · challenges · submit_flag │
└──────────────────┬───────────────────────────┘
│ shared client
┌──────────────────▼───────────────────────────┐
curl / scripts ─►│ FastAPI REST (/api/v1/...) (optional) │
└──────────────────┬───────────────────────────┘
│
┌──────────────────▼───────────────────────────┐
│ server.ctfd_client.CTFdClient │
│ └─ gateway.py (HTTP, auth, timeouts) │
└──────────────────┬───────────────────────────┘
│ HTTPS / HTTP
┌─────▼─────┐
│ CTFd │
└───────────┘
Credentials (token / cookie / password) live in memory only and are never
echoed in tool output, written to server_state.json, or logged.
Features
- Multiple authentication modes — API token, session cookie, or username/password
form login (with CSRF handling).
- Rich challenge queries — paginated listing with
category, search (name),
and solved/unsolved filters, plus per-challenge detail retrieval.
- Safe flag submission — requires an explicit
confirm=True, returns clear
success/failure, and surfaces rate-limit errors. Flags are never logged.
- Instance introspection — public instance info, health check, and an
authentication-status tool that reveal no secrets.
- Consistent structured errors —
AuthenticationError, CTFdAPIError,
ChallengeNotFoundError, SubmissionError, ValidationError,
ConfigurationError.
- Pagination by default — one page of challenges per call; no accidental
full-dump downloads.
- Hardened HTTP — configurable timeouts, one safe retry for idempotent
GETs,
no retries for POSTs (no duplicate submissions), strict JSON/content parsing.
- REST + MCP from one codebase — identical behaviour on both interfaces.
- No hardcoded instance —
BASE_URL is validated and configurable at startup
and at runtime.
Installation
Requires Python 3.10+.
The fastest way is to install from PyPI:
pip install ctfd-mcp-server
CTFD_BASE_URL=https://ctf.example.com CTFD_ADMIN_TOKEN=ctfd_... ctfd-mcp
ctfd-rest
For MCP clients, point your config at the packaged entry point:
{
"mcpServers": {
"ctfd-mcp": {
"command": "ctfd-mcp",
"env": {
"CTFD_BASE_URL": "https://demo.ctfd.io",
"CTFD_ADMIN_TOKEN": "ctfd_..."
}
}
}
}
Or run from source:
git clone https://github.com/MrJamescot/ctfd-mcp-server.git
cd ctfd-mcp-server
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
Configure
CTFD_BASE_URL | (empty) | CTFd instance root, e.g. https://ctf.example.com (without /api/v1) |
CTFD_ADMIN_TOKEN | (empty) | API token (preferred auth) |
CTFD_SESSION_COOKIE | (empty) | Session cookie, e.g. session=abc... |
CTFD_USERNAME | (empty) | Username for form login |
CTFD_PASSWORD | (empty) | Password for form login |
CTFD_HTTP_TIMEOUT | 15 | Per-request HTTP timeout (seconds) |
CTFD_HTTP_MAX_REDIRECTS | 5 | Max redirects followed per request |
CTFD_STATE_FILE | ~/.local/state/ctfd-mcp/server_state.json | File used to cache auth state |
CTFD_MCP_TRANSPORT | stdio | MCP transport: stdio or sse |
MCP_HOST / MCP_PORT | 127.0.0.1 / 8000 | REST server bind settings (loopback by default) |
CTFD_API_TOKEN | (empty) | Optional bearer token protecting the optional REST API (/api/v1/*) |
CTFD_ALLOW_PRIVATE_IPS | false | Allow connections to private/loopback/metadata addresses (e.g. local CTFd test instances) |
CTFD_DOWNLOAD_DIR | ./downloads | Directory where challenge attachments are saved by download_file |
CTFD_PERSIST_SECRETS | false | ⚠ Strongly discouraged: write secrets to disk |
CTFD_BASE_URL may include a path prefix (e.g. https://host/ctfd); the client
appends /api/v1 automatically.
Running the MCP Server
Most MCP clients launch the server themselves via a command/args config.
For that, your client config should reference ctfd_mcp_server.py:
// e.g. Claude Desktop / mcp.json
{
"mcpServers": {
"ctfd-mcp": {
"command": "python",
"args": ["/path/to/ctfd-mcp-server/ctfd_mcp_server.py"],
"env": {
"CTFD_BASE_URL": "https://demo.ctfd.io",
"CTFD_ADMIN_TOKEN": "ctfd_..."
}
}
}
}
Manual launch:
python ctfd_mcp_server.py
CTFD_MCP_TRANSPORT=sse python ctfd_mcp_server.py
MCP tools
set_base_url | url | Point the server at a CTFd instance |
set_token | token | Adopt an API token (memory only) |
set_cookie | cookie | Adopt a session cookie (memory only) |
login | username, password | Form login; keeps the session cookie |
challenges | category, search, solved, page, per_page | Paginated challenge list with filters |
challenge | identifier (id or name) | Full detail of one challenge |
submit_flag | flag, challenge_name/challenge_id, confirm | Submit a flag (requires confirm=True) |
download_file | file_url, dest_dir | Download a challenge attachment over the CTFd /files/… route |
unlock_hint | hint_id | Unlock and read a hint (paid hints cost points) |
scoreboard | — | Public scoreboard standings |
progress | — | Your score + solved challenges |
instance_info | — | Safe public instance metadata |
auth_status | — | Auth mode + validity (no secrets) |
health | — | Reachability, API and auth checks |
With token auth, requests are sent with Content-Type: application/json
(CTFd only honours Authorization: Token ... on JSON requests). With
cookie/credentials auth, state-changing requests echo the session CSRF nonce
as the CSRF-Token header, which is re-fetched from the site after login.
Tools return JSON text. Errors are structured, e.g.:
{ "error": { "type": "ChallengeNotFoundError", "message": "Challenge '99' not found (or not visible)." } }
Running the REST API (optional)
python scripts/run_local.sh
uvicorn server.main:app --host 0.0.0.0 --port 8000
Endpoints (all under /api/v1):
| POST | /set_base_url | Validate & set the CTFd base URL |
| POST | /set_token | Set API token |
| POST | /set_cookie | Set session cookie |
| POST | /set_creds | Store username/password for later login |
| POST | /login | Form login (session cookie) |
| GET | /challenges | Paginated + filtered challenge list |
| GET | /challenges/{id-or-name} | Challenge detail |
| POST | /submit | Submit a flag (confirm: true required) |
| POST | /download | Download a challenge attachment (body: file_url) |
| POST | /unlock_hint | Unlock and read a hint (body: hint_id) |
| GET | /scoreboard | Public standings |
| GET | /progress | Your score and solves |
| GET | /instance_info | Public instance metadata |
| GET | /auth_status | Auth mode + validity |
| GET | /health | Health check |
The optional REST API can be protected with an extra bearer token: set
CTFD_API_TOKEN, and requests to /api/v1/* will require
Authorization: Bearer <token>. The server binds to loopback by default
(MCP_HOST=127.0.0.1).
Docker
A ready-made image is published on Docker Hub:
docker run --rm -p 8000:8000 \
-e CTFD_BASE_URL=https://ctf.example.com \
-e CTFD_ADMIN_TOKEN=ctfd_... \
jamescot/ctfd-mcp-server
Or build locally (REST mode):
docker build -t ctfd-mcp .
docker run --rm -p 8000:8000 \
-e CTFD_BASE_URL=https://ctf.example.com \
-e CTFD_ADMIN_TOKEN=ctfd_... \
ctfd-mcp
docker compose up --build also works (REST API on http://localhost:8000).
To run the MCP SSE server in a container instead:
docker run --rm -it -e CTFD_BASE_URL=https://ctf.example.com ctfd-mcp python ctfd_mcp_server.py
Usage examples
MCP (agent)
1. set_base_url url="https://ctf.example.com"
2. set_token token="ctfd_..."
3. challenges category="web", solved=false, page=1, per_page=25
4. challenge identifier="3"
5. submit_flag flag="flag{...}", challenge_id=3, confirm=true
REST
curl -X POST http://localhost:8000/api/v1/set_base_url \
-H 'Content-Type: application/json' -d '{"url":"https://ctf.example.com"}'
curl -X POST http://localhost:8000/api/v1/set_token \
-H 'Content-Type: application/json' -d '{"token":"ctfd_..."}'
curl 'http://localhost:8000/api/v1/challenges?search=web&solved=false&per_page=10'
curl -X POST http://localhost:8000/api/v1/submit \
-H 'Content-Type: application/json' \
-d '{"challenge_id":3,"flag":"flag{...}","confirm":true}'
curl http://localhost:8000/api/v1/health
See DEMO.md for a complete walkthrough and
examples/ for curl and Python snippets.
Development & testing
pip install -r requirements-dev.txt
python -m pytest -q
ruff check server ctfd_mcp_server.py tests
The test suite mocks the CTFd API (tests/conftest.py::FakeGateway), so unit
tests run offline.
Integration testing against a real CTFd
Run a local CTFd for live tests (recommended over the shared demo instance,
which serves HTML on public auth-gated routes):
git clone https://github.com/CTFd/CTFd.git /tmp/CTFd
docker compose -f /tmp/CTFd/docker-compose.yml up
CTFD_BASE_URL=http://localhost:8000 python ctfd_mcp_server.py
CTFD_BASE_URL=http://localhost:8000 uvicorn server.main:app --port 8001
curl http://localhost:8001/api/v1/health
Security considerations
- Credentials are memory-only. By default nothing is written to
server_state.json. Enabling CTFD_PERSIST_SECRETS is discouraged.
- Secrets are never echoed. Tool and API responses, error messages and logs
redact tokens, cookies, passwords and flags (
server/utils.py).
- Every tool validates its input before touching the network (
set_base_url
requires an absolute http(s) URL, submit_flag requires confirm=True,
etc.).
- Controlled retries. Only idempotent
GET requests are retried (once).
Flag submissions are never automatically replayed.
- Trust model. The server is a local/dev tool: whoever can call its tools can
point it at any CTFd instance and (with a valid credential) read data or submit
flags. Do not expose the REST/SSE endpoints on an untrusted network.
- SSRF guard. By default connections to private / loopback / link-local /
metadata addresses are refused (
CTFD_ALLOW_PRIVATE_IPS=1 opts out). A local
container or a tool pointed at a private instance will get a clear error.
Limitations
- Requires CTFd v3+. The
/api/v1 routes used are standard CTFd v3 API
routes.
- Form login depends on CTFd's web session flow (CSRF nonce extraction is
best-effort). API tokens are the recommended authentication method.
difficulty is not a standard CTFd field; challenge value (points) is
returned instead.
solved filtering uses CTFd's solved_by_me flag, which is only meaningful
when authenticated.
- Instance "version" is reported only when it appears in the rendered page; CTFd
has no public version API endpoint.
- Tokens are per-instance. CTFd redirects
/api/v1 calls to its login page
when a credential is invalid. The server detects this and reports: "the
credential is not valid for THIS instance" — a token from one CTFd instance
never works on another.
- When
CTFD_USERNAME/CTFD_PASSWORD are configured, the server auto-logs-in
on demand (rotating the session cookie) whenever a call returns
unauthenticated, so expired sessions self-heal. After each login the CSRF
nonce is re-fetched from the site (a fresh nonce is required for flag
submissions over a web session).
- A teamless account sees no
/api/v1/challenges on team-mode instances until it
joins or creates a team; the server surfaces CTFd's permission message.
Contributing
Pull requests are welcome. Please:
- Open an issue describing the change.
- Add tests in
tests/ (mocked CTFd API preferred).
- Run
python -m pytest -q and ruff check server ctfd_mcp_server.py tests.
- Do not ship credentials in code, tests, or commit
server_state.json /
.env.
License
MIT — repository: https://github.com/MrJamescot/ctfd-mcp-server