Skip to main content

CTFd MCP Server

PyPI - Version PyPI - Python Versions Docker Pulls License: MIT GitHub Stars

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 errorsAuthenticationError, 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 instanceBASE_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

# MCP stdio server with env config:
CTFD_BASE_URL=https://ctf.example.com CTFD_ADMIN_TOKEN=ctfd_... ctfd-mcp

# optional REST interface:
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     # then edit .env

Configure

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_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:

# stdio (default) — used by MCP clients
python ctfd_mcp_server.py

# SSE — expose over HTTP for remote/Docker use
CTFD_MCP_TRANSPORT=sse python ctfd_mcp_server.py   # http://127.0.0.1:8000/sse

MCP tools

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 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           # reads .env, default http://127.0.0.1:8000
# or
uvicorn server.main:app --host 0.0.0.0 --port 8000

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 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
# stdio on the attached terminal

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          # 76 unit tests, mocked CTFd API (no network)
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
# create a user/challenge, then:
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:

  1. Open an issue describing the change.
  2. Add tests in tests/ (mocked CTFd API preferred).
  3. Run python -m pytest -q and ruff check server ctfd_mcp_server.py tests.
  4. Do not ship credentials in code, tests, or commit server_state.json / .env.

License

MIT — repository: https://github.com/MrJamescot/ctfd-mcp-server

Release files for ctfd-mcp-server 1.0.4

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for ctfd-mcp-server 1.0.4
File Size Uploaded
ctfd_mcp_server-1.0.4.tar.gz 39.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for ctfd-mcp-server 1.0.4
File Interpreter ABI Platform
ctfd_mcp_server-1.0.4-py3-none-any.whl Python 3 none any Details

Total release size: 73.6 kB

Release files / ctfd_mcp_server-1.0.4.tar.gz

Download URL ctfd_mcp_server-1.0.4.tar.gz
Size 39.2 kB
Tags Source
SHA-256 checksum
How to use checksums
3ecd102e812c5855673a09f60fd1b09e6aac1ab4217339e97cdfe24cea1726fe
BLAKE2b-256 checksum
How to use checksums
ce86f6764cb1aef095b5abfc44aecbb44cf26fd781fcb0133fc54e26f0af9e5d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 21, 2026.

Transparency log

Release files / ctfd_mcp_server-1.0.4-py3-none-any.whl

Download URL ctfd_mcp_server-1.0.4-py3-none-any.whl
Size 34.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
06e131f7f80b363b9b57bda948c6bf1bef89e66267f5621df16b5e8e2ae0d9af
BLAKE2b-256 checksum
How to use checksums
d4044d965cf5460eb97218842c41a494f687614952f536b36f0392131eb16181
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 21, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.0.4 This release

2 release files

1.0.3

2 release files

1.0.2

2 release files

1.0.1

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page