The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the CTFd listing page.
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:
ctfd_mcp_server.py, used over stdio or sse.server/main.py, a FastAPI mirror for scripting,
debugging, and Docker deployments.Credentials (token / cookie / password) live in memory only and are never
echoed in tool output, written to server_state.json, or logged.
category, search (name),
and solved/unsolved filters, plus per-challenge detail retrieval.confirm=True, returns clear
success/failure, and surfaces rate-limit errors. Flags are never logged.AuthenticationError, CTFdAPIError,
ChallengeNotFoundError, SubmissionError, ValidationError,
ConfigurationError.GETs,
no retries for POSTs (no duplicate submissions), strict JSON/content parsing.BASE_URL is validated and configurable at startup
and at runtime.Requires Python 3.10+.
The fastest way is to install from PyPI:
For MCP clients, point your config at the packaged entry point:
Or run from source:
| Variable | Default | Meaning |
|---|---|---|
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_URLmay include a path prefix (e.g.https://host/ctfd); the client appends/api/v1automatically.
Most MCP clients launch the server themselves via a command/args config.
For that, your client config should reference ctfd_mcp_server.py:
Manual launch:
| Tool | Parameters | Description |
|---|---|---|
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 honoursAuthorization: Token ...on JSON requests). With cookie/credentials auth, state-changing requests echo the session CSRF nonce as theCSRF-Tokenheader, which is re-fetched from the site after login.
Tools return JSON text. Errors are structured, e.g.:
Endpoints (all under /api/v1):
| Method | Path | Description |
|---|---|---|
| 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 requireAuthorization: Bearer <token>. The server binds to loopback by default (MCP_HOST=127.0.0.1).
A ready-made image is published on Docker Hub:
Or build locally (REST mode):
docker compose up --build also works (REST API on http://localhost:8000).
To run the MCP SSE server in a container instead:
See DEMO.md for a complete walkthrough and
examples/ for curl and Python snippets.
The test suite mocks the CTFd API (tests/conftest.py::FakeGateway), so unit
tests run offline.
Run a local CTFd for live tests (recommended over the shared demo instance, which serves HTML on public auth-gated routes):
server_state.json. Enabling CTFD_PERSIST_SECRETS is discouraged.server/utils.py).set_base_url
requires an absolute http(s) URL, submit_flag requires confirm=True,
etc.).GET requests are retried (once).
Flag submissions are never automatically replayed.CTFD_ALLOW_PRIVATE_IPS=1 opts out). A local
container or a tool pointed at a private instance will get a clear error./api/v1 routes used are standard CTFd v3 API
routes.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./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.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)./api/v1/challenges on team-mode instances until it
joins or creates a team; the server surfaces CTFd's permission message.Pull requests are welcome. Please:
tests/ (mocked CTFd API preferred).python -m pytest -q and ruff check server ctfd_mcp_server.py tests.server_state.json /
.env.MIT — repository: https://github.com/MrJamescot/ctfd-mcp-server