Deploy — DefectDojo MCP at https://dojo.example.com/mcp/
Architecture:
MCP client (Claude Desktop / mcp-remote)
-> https://dojo.example.com/mcp/ (Cloudflare TLS)
-> host nginx (location /mcp/) (deploy/nginx-mcp.conf)
-> 127.0.0.1:9900/mcp (dd-mcp container host port 9900 -> 9000)
-> http://nginx:8080 (DefectDojo API, internal docker network)
The caller's DefectDojo API token rides in the Authorization: Token <token> header the
whole way; the MCP server uses it per-request and preserves DefectDojo's permissions. The
token is never stored, logged, or sent to the LLM.
docker network ls | grep defectdojo # usually django-defectdojo_defaultcd deploy
# Runtime config is deploy/config.prod.yaml (bind-mounted at /app/config.yaml). There is no
# committed .env template — docker-compose.yml reads ${...} env defaults, so only override
# what differs from the defaults by exporting (or writing a deploy/.env with) any of:
# DOJO_NETWORK (default django-defectdojo_default)
# DD_URL (default http://nginx:8080 — internal DefectDojo nginx)
# DD_MCP_REPORTING_DB_DSN (read-only reporting DB DSN; required for DB-history tools)
# config.prod.yaml already pins allowed_hosts/origins to dojo.example.com.docker compose up -d --build
docker compose logs -f dd-mcp # expect: tool_groups_registered ... transport=streamable-http
curl -s http://127.0.0.1:9900/healthz # -> {"status":"ok","transport":"streamable-http"}The container publishes on 127.0.0.1:9900 only — not world-exposed. The host nginx is the front door.
Append the location /mcp/ block from deploy/nginx-mcp.conf into the existing
server { ... } for dojo.example.com (e.g. /etc/nginx/conf.d/dojo-proxy.conf), then:
sudo nginx -t && sudo systemctl reload nginx# MCP endpoints reject plain GET (protocol needs POST + headers) — a 4xx/406 means it's reachable.
curl -s -o /dev/null -w "%{http_code}\n" -X POST https://dojo.example.com/mcp \
-H "Accept: application/json, text/event-stream" -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"x","version":"0"}}}'
# Full check: connect a client (below) and list tools.Claude Desktop (via mcp-remote), ~/.../claude_desktop_config.json:
{
"mcpServers": {
"defectdojo": {
"command": "npx",
"args": [
"mcp-remote", "https://dojo.example.com/mcp",
"--header", "Authorization: Token ${DD_TOKEN}"
],
"env": { "DD_TOKEN": "your-defectdojo-api-token" }
}
}
}Each user supplies their own DefectDojo API token (Settings → API v2 Key in DefectDojo).
- DNS-rebinding protection is ON; the host nginx forwards
Host: dojo.example.com, whichconfig.prod.yamlallows. If you change the public host, updatemcp.allowed_hosts/mcp.allowed_origins. Loopback (127.0.0.1:9000,localhost:9000) is always allowed so the container healthcheck works. - Cloudflare: streamable-http uses SSE. Ensure Cloudflare does not buffer/cache
/mcp/(the origin already sends no-buffer headers). If streaming misbehaves, try a grey-clouded hostname or setmcp.stateless_httplater. - Internal API URL + Host header:
http://nginx:8080resolves on the shared docker network, but DefectDojo's DjangoALLOWED_HOSTSrejectsHost: nginx:8080with 400 DisallowedHost.config.prod.yamlsetsdefectdojo.host_header: dojo.example.com(or envDD_HOST_HEADER) so the client routes internally but presents the accepted Host. Do NOT pointDD_URLathttps://dojo.example.com— the host can't hairpin its own Cloudflare domain. - Endpoint URLs / two transports: Streamable HTTP at
https://dojo.example.com/mcp(no trailing slash) AND legacy HTTP+SSE athttps://dojo.example.com/sse(+/messages/), sincemcp.enable_sse: true. The nginxlocation ~ ^/(mcp|sse|messages)passes the URI through so both work. Use/mcpfor modern clients; use/sseonly for clients that can't speak Streamable HTTP. Legacy SSE client (mcp-remote auto-detects, or force it):npx mcp-remote https://dojo.example.com/sse --header "Authorization: Token <token>". - Writes / DB tools are ON in this deployment:
config.prod.yamlsetsenable_write_tools: true,enable_db_tools: true, anddatabase.enabled: true. Writes (note / false-positive / close) stay confirmation-gated (a reason + a two-step confirmation-token handshake), and each DB-history tool API-authorizes the object first, then reads an allowlisted read-only view. DB tools require themcp_*views +mcp_reporting_rorole in the DefectDojo Postgres (seesql/) and theDD_MCP_REPORTING_DB_DSNenv. To turn a group back off, flip the flag inconfig.prod.yamlanddocker compose up -dto apply. - Update:
git pull && cd deploy && docker compose up -d --build. - Logs/audit go to the container's stderr (
docker compose logs dd-mcp) as JSON.