Skip to content

Latest commit

 

History

History
114 lines (93 loc) · 5.26 KB

File metadata and controls

114 lines (93 loc) · 5.26 KB

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.

1. Find the DefectDojo docker network

docker network ls | grep defectdojo      # usually django-defectdojo_default

2. Configure

cd 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.

3. Build + run the container

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.

4. Add the nginx route

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

5. Verify end to end

# 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.

6. Connect a client

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).

Notes / gotchas

  • DNS-rebinding protection is ON; the host nginx forwards Host: dojo.example.com, which config.prod.yaml allows. If you change the public host, update mcp.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 set mcp.stateless_http later.
  • Internal API URL + Host header: http://nginx:8080 resolves on the shared docker network, but DefectDojo's Django ALLOWED_HOSTS rejects Host: nginx:8080 with 400 DisallowedHost. config.prod.yaml sets defectdojo.host_header: dojo.example.com (or env DD_HOST_HEADER) so the client routes internally but presents the accepted Host. Do NOT point DD_URL at https://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 at https://dojo.example.com/sse (+ /messages/), since mcp.enable_sse: true. The nginx location ~ ^/(mcp|sse|messages) passes the URI through so both work. Use /mcp for modern clients; use /sse only 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.yaml sets enable_write_tools: true, enable_db_tools: true, and database.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 the mcp_* views + mcp_reporting_ro role in the DefectDojo Postgres (see sql/) and the DD_MCP_REPORTING_DB_DSN env. To turn a group back off, flip the flag in config.prod.yaml and docker compose up -d to 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.