DefectDojo Intelligence MCP Server
README.md
# DefectDojo Intelligence MCP Server
A permission-preserving [Model Context Protocol](https://modelcontextprotocol.io) server over
**DefectDojo OSS v2.58.4** REST API v2. It reaches **1:1 capability parity** with the official
DefectDojo **Pro** built-in MCP Server (12 read tools + a Security Landscape prompt) — and adds
a deterministic analytics/reporting **superset** — implemented as an **external adapter**,
because the built-in Pro MCP requires Pro edition + instance v2.51.2+ and is unavailable on an
OSS 2.58.4 host.
Every call uses the **caller's own DefectDojo API token**, so DefectDojo decides visibility.
The token is held in memory only, never logged, never sent to the LLM.
- **Status:** Phases 0–4 complete, hardened, and dockerized. **140 tests, ruff-clean.**
- **Deploy:** see [deploy/DEPLOY.md](deploy/DEPLOY.md) for the dockerized
`https://dojo.example.com/mcp/` setup. Note: that deployment **enables the write and
DB-history tool groups** (see [Security model](#security-model) and DEPLOY.md). Spec: [PRD.md](PRD.md).
## Tools
**26 tools** — 20 on by default (14 read + 6 analytics/report), 6 gated off by default. Plus 1 MCP prompt primitive.
| Group | Tools | Default |
|---|---|---|
| **Core read** (Pro parity + source resolver) | `dd_get_findings`, `dd_get_finding_by_id`, `dd_get_products`, `dd_get_product_by_id`, `dd_get_product_types`, `dd_get_engagements`, `dd_get_tests`, `dd_resolve_finding_source`, `dd_get_users`, `dd_get_user_by_id`, `dd_get_groups`, `dd_get_group_by_id`, `dd_get_dojo_group_members`, `dd_get_roles` | ✅ on |
| **Analytics / reports** (superset) | `dd_get_sla_breaches`, `dd_get_top_cwes`, `dd_prioritize_findings`, `dd_generate_product_security_summary`, `dd_generate_executive_report`, `dd_generate_engineering_report` | ✅ on |
| **DB history** (read-only, API-gated) | `dd_get_finding_history`, `dd_get_reopened_findings`, `dd_get_product_risk_trend` | ⛔ off (`enable_db_tools` + reporting DB) |
| **Writes** (confirmation-gated) | `dd_add_finding_note`, `dd_mark_finding_false_positive`, `dd_close_finding` | ⛔ off (`enable_write_tools`) |
| **Prompt** | `security_landscape_report` | ✅ on |
Analytics tools compute **deterministically server-side** (no LLM, no invented data — every
figure traces to a findings-API query) and return typed, structured output. SLA uses
DefectDojo's own `outside_of_sla` filter; prioritization uses real KEV/EPSS/SLA signals.
## Capability parity with Pro
| Pro tool | This server |
|---|---|
| `get_findings`, `get_finding_by_id` | `dd_get_findings`, `dd_get_finding_by_id` |
| `get_products`, `get_product_types` | `dd_get_products`, `dd_get_product_types` (+ `dd_get_product_by_id`) |
| `get_engagements`, `get_tests` | `dd_get_engagements`, `dd_get_tests` |
| `get_users`, `get_user_by_id` | `dd_get_users`, `dd_get_user_by_id` |
| `get_groups`, `get_group_by_id`, `get_dojo_group_members` | `dd_get_groups`, `dd_get_group_by_id`, `dd_get_dojo_group_members` |
| `get_roles` | `dd_get_roles` |
| _(no Pro equivalent)_ | `dd_resolve_finding_source` — finding → repo/branch/commit to clone (superset) |
| 📊 Security Landscape Report (prompt) | `security_landscape_report` (MCP prompt primitive) |
| 🛡️ SAST Review Report (prompt) | intentionally dropped — low value for DAST/pentest data |
## Security model
- **Token:** `Authorization: Token <token>` (not Bearer). HTTP reads it **per-request** from the
incoming header (the caller's own token); stdio reads `DD_API_TOKEN`. On streamable-http the
env fallback is **disabled** — a request with no/garbled Authorization header is rejected, never
silently run as the server's env token.
- **Token isolation under pooling:** the shared connection pool uses a no-store cookie jar, so an
upstream `Set-Cookie` is never replayed across different callers' tokens. Auth is per-request only.
- **Validation:** the token is validated once per TTL (cached by 128-bit fingerprint, never the
token; cache is bounded) before the first tool call.
- **Redaction:** applied **centrally to every tool's output** when
`redact_secrets`/`treat_finding_text_as_untrusted` is set — secrets scrubbed (incl. URL-embedded
credentials), PII stripped when `omit_user_pii`, free-text truncated to `max_evidence_chars`.
- **Errors:** failures (401/403/404/timeout/…) surface as MCP tool errors (`isError`), not as
success payloads, so a client can't mistake a denial for data.
- **Writes:** off by default; when enabled, every write requires a non-empty reason + explicit
`confirm=true` (otherwise a no-op preview), with before/after state-hash audit. REST API only —
never writes to the DB.
- **DB history:** off by default; each tool authorizes the object via REST **first**, then reads an
**allowlisted read-only view** (never arbitrary SQL).
- **Transport:** streamable-http gets explicit `TransportSecuritySettings` (DNS-rebinding/Origin
protection) from `mcp.allowed_hosts`/`allowed_origins`; loopback is always allowed for healthchecks.
- **Deprecated endpoints** (`credentials`, `credential_mappings`, `stub_findings`) are blocked; a
disallowed-tool allowlist is asserted at registration.
- **Audit:** one structured JSON event per tool call, to **stderr** (stdout is the MCP stream).
## Hardening
Bounded retry/backoff for transient GET failures (429/502/503/504/timeout; honors Retry-After
incl. HTTP-date form; never retries writes or 4xx) · process-wide concurrency `Semaphore` ·
shared pooled httpx client (per-request auth, closed on lifespan shutdown) · per-token roles TTL
cache. See `tests/test_hardening.py`.
## Production deployment (dockerized, `/mcp/`)
Full guide: **[deploy/DEPLOY.md](deploy/DEPLOY.md)**. In short:
```bash
cd deploy
# Optional: set DOJO_NETWORK / DD_URL / DD_MCP_REPORTING_DB_DSN in a deploy/.env
# (docker compose reads ${...} env defaults; there is no committed .env template —
# config lives in deploy/config.prod.yaml, bind-mounted at /app/config.yaml).
docker compose up -d --build
curl -s http://127.0.0.1:9900/healthz # -> {"status":"ok","transport":"streamable-http"}
```
Then add the `location ~ ^/(mcp|sse|messages)` block from
[deploy/nginx-mcp.conf](deploy/nginx-mcp.conf) to the host nginx and reload. Clients connect to
`https://dojo.example.com/mcp/` with their own `Authorization: Token <token>` (via `mcp-remote`).
The container binds `0.0.0.0:9000` inside the container, published on the host as
`127.0.0.1:9900` only, and reaches DefectDojo internally at `http://nginx:8080`. The prod config
**enables the write and DB-history tool groups** (`enable_write_tools` / `enable_db_tools`);
writes remain confirmation-gated and DB tools API-authorize first — see DEPLOY.md.
## Local development
### Phase 0 — verify the live schema first
```bash
DD_URL=https://dojo.internal DD_API_TOKEN=xxxx ./scripts/check_schema.sh
```
Confirms the required collection endpoints exist and flags deprecated ones. The DRF filter
lookups in `src/defectdojo_mcp/tools/*` are verified against DefectDojo 2.58.4 source — re-confirm
against your live schema here.
### Install & run
```bash
pip install -e ".[dev]"
cp config.example.yaml config.yaml # edit base_url
# stdio reads the token from the DD_API_TOKEN env var (and DD_URL) directly — no .env file.
# stdio (single user / desktop client)
DD_URL=https://dojo.internal DD_API_TOKEN=xxxx DD_MCP_CONFIG=config.yaml defectdojo-mcp
# streamable-http (multi-user; token per-request). Set mcp.allowed_hosts for a 0.0.0.0 bind.
DD_MCP_TRANSPORT=streamable-http DD_MCP_HOST=0.0.0.0 DD_MCP_PORT=9000 defectdojo-mcp
# endpoint: http://<host>:9000/mcp health: http://<host>:9000/healthz
```
### Claude Desktop (stdio)
```json
{
"mcpServers": {
"defectdojo": {
"command": "defectdojo-mcp",
"env": {
"DD_URL": "https://dojo.internal",
"DD_API_TOKEN": "your-token",
"DD_MCP_CONFIG": "/path/to/config.yaml"
}
}
}
}
```
### Test
```bash
pytest # 140 tests: redaction/sanitize, params, paging, auth+validator, context,
# server gating, intelligence scoring, source resolution, writes, history,
# hardening (retry/pool/semaphore/cache)
ruff check .
```
## Configuration
Key `config.yaml` knobs (see `config.example.yaml` / `deploy/config.prod.yaml`):
- `mcp.transport` (`stdio` | `streamable-http`), `mcp.host`/`port`
- `mcp.enable_write_tools`, `mcp.enable_db_tools` — gate the off-by-default groups
- `mcp.enable_dns_rebinding_protection`, `mcp.allowed_hosts`, `mcp.allowed_origins`
- `security.redact_secrets`, `omit_user_pii`, `require_confirmation_for_writes`
- `limits.max_concurrent_api_calls`, `max_retries`, `max_evidence_chars`, `roles_cache_ttl_seconds`
- `database.enabled`, `dsn_env`, `allowed_views` (Phase 3 reporting DB)
## Discoverability & extensibility
Every tool carries `ToolAnnotations` (`readOnlyHint`/`idempotentHint`/`title`; writes are
non-read-only); enum params publish enums; list tools declare a typed `Page` output schema; the
server advertises `instructions`; the prompt has a title + documented args. Tools register through
a `(module, predicate)` registry in `server.py` (single default-off gate per optional group);
`ServerContext.execute()` / `execute_db_gated()` / `execute_write()` are the chokepoints for token
validation, the central egress transform, audit, and error mapping.
## License
GNU Affero General Public License v3.0 only (AGPL-3.0-only). Full text in
[LICENSE](LICENSE).
Copyright (C) 2026 Zeeshan Sultan.
## Security
Please report vulnerabilities privately — see [SECURITY.md](SECURITY.md). Do not
open a public issue for a security problem.
## Related
Built alongside [ShadowDSO](https://github.com/ZeeshanSultan/ShadowDSO), an
out-of-band security scanning platform that uses DefectDojo as its system of
record. This server stands alone and does not depend on it.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues