Skip to main content
Glama
ZeeshanSultan

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.