Skip to main content
Glama
dtrack-mcp

dtrack-mcp

by drewrukin

dtrack-mcp

PyPI License: MIT Python MCP Dependency-Track

MCP server that connects Claude (or any MCP-compatible LLM) to Dependency-Track.

Instead of clicking through the DT UI to triage hundreds of vulnerability findings, describe what you need in natural language — Claude pulls the data, reasons over it, and writes the verdict back.


Why

Dependency-Track accumulates findings fast. A product with 5 versions and 100+ vulnerabilities each means 500+ rows to triage — each requiring you to open a finding, read the CVE, check CVSS, look at EPSS/KEV signals, see what other projects decided, and pick a state. That's hours of browser clicking per release cycle.

dtrack-mcp exposes DT as an MCP server so Claude can do the data work:

  • Pull findings filtered by severity or state

  • Deduplicate CVE/GHSA/OSV aliases into one group per real issue

  • Check what was decided for the same vulnerability in other projects or versions

  • Write the triage decision back to DT with a comment


Related MCP server: CVE MCP Server

Key scenarios

1. Triage a batch of findings

Show me all CRITICAL and HIGH findings in project "myapp v2.3.0"
that haven't been analysed yet. Group by alias so I don't see
the same CVE twice. For each group tell me the CVSS vector,
EPSS score, and whether it's in CISA KEV.

2. Carry triage forward when upgrading

We just uploaded the SBOM for myapp v2.4.0. Carry over all
triage decisions from v2.3.0 — dry run first, then apply.

3. Propagate a decision across versions

A new CVE is found in v1.0, v1.1, v2.0, and v2.1 simultaneously. Triage it in one version, then carry the decision backward and forward to the others:

Set CVE-2024-12345 in myapp v2.1.0 as NOT_AFFECTED
(justification: CODE_NOT_REACHABLE). Then carry that decision
to v1.0, v1.1, and v2.0.

Tools

Tool

Description

list_projects

List projects with vulnerability counts

resolve_project

Find project by UUID or by exact name + version

list_findings

Findings with severity / state / suppressed filters

group_findings_by_alias

Deduplicate CVE/GHSA/OSV via union-find

find_vulnerability

Full detail by id, optionally specifying source

search_vulnerability

Which projects are affected by a given CVE?

get_analysis

Current triage state + full comment history

find_duplicate_analyses

Same vuln in other components / projects, with prior analyses

set_analysis

⚠ WRITE — set state, justification, response, comment. Accepts raw UUIDs or a finding dict

get_project_versions

All versions of a project, newest first

diff_findings

Carried / updated / new / gone between two versions

carry_over_triage

⚠ WRITE — transfer decisions v1 → v2 (or v2 → v1)

broadcast_triage

⚠ WRITE — fan out one decision to all versions of a project

upload_bom

⚠ WRITE — upload CycloneDX/SPDX SBOM

All tools are read-only except the four marked ⚠ WRITE. The HTTP layer enforces this: any write path other than PUT /api/v1/analysis and POST /api/v1/bom raises before reaching the network.


Requirements

  • Python 3.10+ (tested on 3.10–3.12).

  • Dependency-Track 4.11+. DT 4.14+ is recommended — earlier versions lack EPSS-for-GHSA data and CVSSv4 fields, and purl distro-qualifier matching degrades. The server logs a warning at startup when it detects an older DT.

  • An MCP-capable client — Claude Desktop, Claude Code, or any other stdio MCP runtime.

  • A DT account with VIEW_PORTFOLIO + VULNERABILITY_ANALYSIS permissions (and BOM_UPLOAD if you use upload_bom).

Linux and macOS are the primary targets; Windows works under WSL.

Installation

pip install dtrack-mcp

Or from source (for development):

git clone https://github.com/drewrukin/dtrack-mcp.git
cd dtrack-mcp
pip install -e .

After pip install, the dtrack-mcp command is on $PATH and starts the MCP server on stdio. A quick local smoke test against your DT instance:

DTRACK_URL=https://dt.example.com \
DTRACK_API_KEY=odt_... \
python scripts/smoke.py

scripts/smoke.py is read-only and safe to re-run — it exercises every GET-based tool on one real project and prints a compact summary.

Claude Desktop — add to ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows):

{
  "mcpServers": {
    "dtrack": {
      "command": "dtrack-mcp",
      "env": {
        "DTRACK_URL": "https://dt.example.com",
        "DTRACK_API_KEY": "odt_..."
      }
    }
  }
}

Claude Code — add to ~/.claude.json:

{
  "mcpServers": {
    "dtrack": {
      "command": "dtrack-mcp",
      "env": {
        "DTRACK_URL": "https://dt.example.com",
        "DTRACK_API_KEY": "odt_..."
      }
    }
  }
}

Restart Claude after editing the config.

Auth options

Variable

Description

DTRACK_URL

Base URL of your DT instance

DTRACK_API_KEY

Preferred. Requires VIEW_PORTFOLIO + VULNERABILITY_ANALYSIS permissions. Add BOM_UPLOAD for upload_bom.

DTRACK_USER + DTRACK_PASSWORD

Alternative. Exchanges credentials for a JWT; re-fetches on 401.

Optional tuning

Variable

Default

Description

DTRACK_TIMEOUT

30

HTTP timeout in seconds.

DTRACK_VERIFY_TLS

false

Set true when your DT instance has a certificate your system trust store recognises.

DTRACK_RETRY_MAX

3

Retries on HTTP 429/502/503/504 and transport errors (connect refused, read timeout). 0 disables retries.

DTRACK_RETRY_BACKOFF_MS

500

Base for exponential backoff: base · 2^attempt + jitter. Retry-After header is honoured up to a 60 s cap.

DTRACK_WRITE_DELAY_MS

0

Sleep between writes in carry_over_triage / broadcast_triage. Use when DT is under load.

DTRACK_LOG_LEVEL

INFO

Standard logging level (DEBUG, INFO, WARNING, ERROR). Logs go to stderr.

DTRACK_SKIP_VERSION_CHECK

false

Skip the one-shot GET /api/v1/version probe at first request. Useful for offline or heavily-mocked tests.

Proxies are disabled inside the client (trust_env=False) because DT is typically on an internal network and corporate proxies refuse to tunnel it. Equivalent to curl --noproxy '*'.


Safety

  • Read-mostly by design. Writes are gated at the HTTP client level, not in application logic. The guard runs before any network call. The only allowed write paths are PUT /api/v1/analysis (triage) and POST /api/v1/bom (SBOM upload).

  • Input validation. All tool parameters are validated against enum allowlists before hitting DT. Unknown parameters are rejected (additionalProperties: false in JSON schema).

  • carry_over_triage and broadcast_triage default to dry_run. No writes happen unless you explicitly pass mode="exact" after reviewing the plan.

  • max_operations cap. Bulk write operations fail early if the plan exceeds 500 entries by default, preventing accidental mass-triage from a hallucinated call.

  • Transient-failure retry. Connection refuseds, read timeouts, and HTTP 429/502/503/504 are retried with exponential backoff (see DTRACK_RETRY_*); no other status is retried, so a broken caller never loops.

  • No secrets in logs. Credentials come from env only; JWTs and API keys are never logged, and raw DT responses are never echoed verbatim into tool output.


Troubleshooting

  • no credentials: set DTRACK_API_KEY or DTRACK_USER+DTRACK_PASSWORD at startup — the env is not visible to the MCP subprocess. Put the vars in the env block of your client config (examples above), not just in your shell.

  • DTRACK_URL is not set — same cause as above.

  • HTTP 401 after api_key auth — the key was revoked or lacks the required permissions. API-key auth does not re-login; rotate the key in DT and update the client config.

  • HTTP 403 on PUT /api/v1/analysis — your account lacks VULNERABILITY_ANALYSIS. Read-only tools work without it.

  • dtrack-mcp: refused <METHOD> <path> — you hit the read-mostly guard. Either the call is outside the documented write allowlist (which is the whole point of the guard) or DT introduced a new endpoint and the spec needs updating — open an issue, don't relax the guard locally.

  • Every call is slow on login+password auth — the JWT is cached per-process, so this only happens at startup. If it repeats, the client is restarting between calls; prefer DTRACK_API_KEY for long sessions.


Documentation

  • SPEC.md — full protocol specification: normalized schemas, per-tool input/output contracts, hard invariants, per-stage evolution.

  • scripts/smoke.py — end-to-end read-only smoke test; mirrors the shape of a real triage session.

  • scripts/smoke_retry.py — retry-layer integration check; includes a recovery-probe mode that requires a live DT instance you can restart.


License

MIT — see LICENSE.

Available Tools

14 tools
broadcast_triageA

⚠ WRITE (when mode="exact"). Fan out triage decisions to all versions.

A specialised form of carry_over_triage for the case where a new CVE is found simultaneously in multiple versions of the same product. Instead of running carry_over N times, triage the finding once in any version, then call this tool to propagate the decision in all directions (newer AND older versions).

Steps:

  1. Fetches every version of project_name from DT.

  2. Excludes the reference version.

  3. Calls carry_over_triage(reference → target) for each.

  4. Returns per-target results plus an aggregate summary.

ALWAYS run mode="dry_run" first to review the plan.

Args: reference_project_uuid: UUID of the version that already has the triage decision to broadcast. project_name: Exact project name (used to find all other versions). mode: "dry_run" (no writes) or "exact" (performs writes). include_updated_components: Also transfer updated_component matches. Default False — conservative. overwrite_not_set: Transfer over targets in state NOT_SET. Default True. overwrite_any: Transfer over targets in any state. Default False. comment_prefix: Prepended to every carry-over comment. max_operations: Per-target cap. Raise if a single target needs more. active_only: Skip inactive/archived versions. Default True.

Inputs: reference_project_uuid (required), project_name (required), mode, include_updated_components, +5 more. Returns: object

carry_over_triageA

⚠ WRITE (when mode="exact"). Transfer triage decisions v1 → v2.

ALWAYS run with mode="dry_run" first. Only switch to mode="exact" after a human has reviewed the plan. In exact mode each transfer issues PUT /api/v1/analysis and appends a comment noting the source project and match reason. The full history of source comments is preserved in the original project untouched.

Skip rules:

  • source has no actionable analysis (state NOT_SET) → skipped

  • target already triaged (state ≠ NOT_SET) and overwrite_any=False → skipped

  • target NOT_SET and overwrite_not_set=False → skipped

Safety caps:

  • max_operations (default 500) early-fails in exact mode when the plan is larger than the cap. Raise explicitly for huge transfers, or split into batches.

  • DTRACK_WRITE_DELAY_MS env var adds a per-write sleep for rate-limit-sensitive instances.

Args: source_project_uuid: Old version UUID with existing triage. target_project_uuid: New version UUID to populate. mode: "dry_run" (no writes, returns plan) or "exact" (performs writes). include_updated_components: Also transfer updated_component matches (same CVE, different component version). Default False — conservative. overwrite_not_set: Transfer over target entries in state NOT_SET. Default True. overwrite_any: Transfer over target entries in any state. Default False. comment_prefix: Prepended to every carry-over comment. max_operations: Sanity cap against hallucination-driven bulk writes in exact mode. Raise if you genuinely need to transfer more.

Inputs: source_project_uuid (required), target_project_uuid (required), mode, include_updated_components, +4 more. Returns: object

diff_findingsA

Compute carried / updated_component / new / gone between two versions.

Typical use: upgrading a product v1 → v2. source is v1 (where triage decisions already exist), target is v2 (new SBOM just uploaded). Returns four lists:

  • carried — same component + same vulnerability, safe to transfer analyses 1:1.

  • updated_component — same vulnerability, component version changed (patch or major). Decision may or may not still apply.

  • new — appeared in target only.

  • gone — were in source only; reason is vuln_fixed (component still there) or component_removed.

Component matching uses (purl_type, purl_namespace, purl_name) — deliberately drops qualifiers so DT 4.13→4.14 upgrades that add distro=... don't invalidate every match. Ambiguous matches (multi-arch SBOMs with the same component at different qualifiers) emit an entry in warnings. Read-only.

Args: source_project_uuid: Old version UUID (usually with existing triage). target_project_uuid: New version UUID. include_analysis: Load current analysis for each source finding (needed for carry_over; adds one HTTP call per finding).

Inputs: source_project_uuid (required), target_project_uuid (required), include_analysis. Returns: object

find_duplicate_analysesA

Find analyses of duplicates of a finding across DT.

Given one finding, returns three parallel lists of duplicates with their current analysis (state + comment history), intended for a triage loop that wants to reuse prior decisions:

  • aliases_in_project — other findings in the same project in the same alias cluster (CVE ↔ GHSA ↔ OSV of the same issue).

  • same_vuln_other_components — same vulnerability uuid on other components/versions in the same project.

  • other_projects — findings in other DT projects that share any id in the target's alias cluster; each entry carries its project uuid/name/version.

Each entry bundles {component, vulnerability, analysis}; entries in other_projects also carry project. Read-only.

Filters (v0.4):

  • states — whitelist of analysis states (e.g. ["NOT_AFFECTED","EXPLOITABLE"]) applied to all three output buckets. target is never filtered.

  • only_analyzed — shorthand for every state except NOT_SET. Ignored when states is non-empty (states wins).

  • active_only (default True) — skip archived/inactive DT projects in other_projects. v0.4 default flip — existing callers that don't pass the flag stop seeing archived hits.

  • project_tag — in other_projects only, keep projects carrying this tag (case-insensitive name equality).

  • compact — strip bulky fields (description, CVSS vectors, analysis details, long comment bodies truncated to 200 chars). See SPEC §13.4.1 for the exact field list.

Args: project_uuid: DT project UUID of the target finding. component_uuid: DT component UUID of the target finding. vulnerability_uuid: DT vulnerability UUID of the target finding. states: Whitelist of analysis state strings, e.g. ["NOT_AFFECTED","EXPLOITABLE"]. only_analyzed: If true, keep only entries with a non-NOT_SET analysis. Ignored when states is non-empty. active_only: If true (default), skip archived projects in other_projects. project_tag: Optional DT tag name; restricts other_projects to projects carrying this tag (case-insensitive). compact: If true, strip bulky fields from the payload.

Inputs: project_uuid (required), component_uuid (required), vulnerability_uuid (required), states, +4 more. Returns: object

find_vulnerabilityA

Fetch the full detail record of a vulnerability.

When source is given (e.g. "NVD", "GITHUB"), fetches directly. When omitted, probes candidate sources based on the id prefix (CVE-* → NVD, GHSA-* → GITHUB, etc.) and returns the first hit, or null if nothing matches. Read-only.

Returns title, description, CVSS v2/v3/v4 scores and vectors, CWEs, EPSS score and percentile, KEV flag, references, and alias list.

Args: vuln_id: Vulnerability id, e.g. "CVE-2024-1234", "GHSA-xxxx-yyyy-zzzz". source: Optional DT source namespace — "NVD", "GITHUB", "OSV", "SNYK", "SONATYPE", "VULNDB", "INTERNAL", etc. When omitted the source is inferred from the id prefix.

Inputs: vuln_id (required), source. Returns: result

get_analysisA

Fetch the analysis record for one finding.

Returns the analysis state, justification, response, details, suppressed flag, and the full comment history. If DT has no analysis row yet, returns an empty-analysis default (state NOT_SET, no comments) — callers never get null. Read-only.

Args: project_uuid: DT project UUID. component_uuid: DT component UUID inside that project. vulnerability_uuid: DT vulnerability UUID.

Inputs: project_uuid (required), component_uuid (required), vulnerability_uuid (required). Returns: object

get_project_versionsA

List all versions of a project by exact name.

Returns {name, total, versions} where versions are sorted newest first (semver-aware, lexicographic fallback). Used to pick source / target UUIDs for diff_findings and carry_over_triage. Read-only.

Args: name: Exact project name. active_only: Exclude projects marked inactive in DT.

Inputs: name (required), active_only. Returns: object

group_findings_by_aliasA

Group findings by alias (transitive closure) — dedup CVE/GHSA/OSV.

Vulnerabilities reported under different ids (e.g. CVE-2024-X and GHSA-Y-Z) often refer to the same issue and are linked via DT's aliases. This tool runs union-find over that alias graph and returns one cluster per real issue. Each cluster carries a canonical id (CVE first, then GHSA, then OSV, then SNYK, then INTERNAL, then alphabetical), the full alias list, a merge_reason trace of the edges that joined the cluster, and every finding in the project belonging to it.

Same filters as list_findings. Pagination applies to groups, not to the findings inside them — a group always ships with all its findings intact. Sorted by highest CVSS score (v3 or v4) descending. Read-only.

include_details=True (v0.3) embeds title/description/references in every finding's vulnerability summary. The same description text repeats on each finding inside a group — acceptable tradeoff for a single-call triage flow.

Args: project_uuid: DT project UUID. suppressed: Include suppressed findings. analysis_states: Whitelist of analysis state strings. severities: Whitelist of severity strings. page: 1-based page of groups (not findings). page_size: Groups per page (max 500). include_details: If true, embed title/description/references in each finding's vulnerability summary (v0.3). Default false.

Inputs: project_uuid (required), suppressed, analysis_states, severities, +3 more. Returns: object

list_findingsA

List vulnerability findings for a project with optional filters.

Returns normalized findings — each one bundles the vulnerability (severity, CVSS v3/v4, CWE, EPSS, aliases), the affected component (name, version, purl, latest known version), and the analysis state. All filters are applied client-side before pagination, so total reflects the post-filter count. Read-only.

When include_details=True, every finding's vulnerability summary also carries title, description, and references so an LLM can draft a verdict without a separate get_vulnerability call. Off by default because descriptions can be 2–4 KB each — set it to true only for focused batches (20–30 findings), not project-wide scans.

Args: project_uuid: DT project UUID (get it from list_projects or lookup_project). suppressed: Include findings suppressed by an analyst. analysis_states: Whitelist, e.g. ["NOT_SET", "IN_TRIAGE", "EXPLOITABLE", "FALSE_POSITIVE", "NOT_AFFECTED", "RESOLVED"]. severities: Whitelist, e.g. ["CRITICAL", "HIGH", "MEDIUM", "LOW", "UNASSIGNED"]. page: 1-based page number (applied after filtering). page_size: Items per page (max 500). include_details: If true, embed title/description/references in each finding's vulnerability summary (v0.3). Default false.

Inputs: project_uuid (required), suppressed, analysis_states, severities, +3 more. Returns: object

list_projectsA

List Dependency-Track projects.

List projects in the Dependency-Track instance, optionally filtered by a substring of the project name. Returns normalized projects with per-severity vulnerability counts. Read-only.

Args: name_filter: Case-insensitive substring on project name. active_only: If true, exclude projects marked inactive in DT. page: 1-based page number. page_size: Items per page (max 500).

Inputs: name_filter, active_only, page, page_size. Returns: object

resolve_projectA

Resolve a project by UUID or by exact (name, version).

Two lookup paths — use whichever you have:

  • project_uuid — direct UUID lookup (e.g. copied from the DT UI URL or returned by another tool).

  • name + version — exact-match lookup by project name and version string.

When project_uuid is provided it takes precedence; name and version are ignored. Returns a normalized project, or null if nothing matches. Read-only.

Args: project_uuid: DT project UUID. Takes precedence when provided. name: Exact project name (requires version). version: Exact project version (requires name).

Inputs: project_uuid, name, version. Returns: result

search_vulnerabilityA

Search which projects are affected by a vulnerability.

Given a vulnerability id (e.g. "CVE-2024-1234"), resolves it, then finds every DT project that contains a finding for this vulnerability (or any of its aliases). For each project returns the analysis state per affected component.

Use this to answer "which products are affected by CVE-X and what's been decided?" without manually iterating over projects. Read-only.

Args: vuln_id: Vulnerability id (e.g. "CVE-2024-1234", "GHSA-xxxx"). active_only: Skip inactive/archived projects. Default True. only_analyzed: Only include projects/findings with a non-NOT_SET analysis state. Default False.

Inputs: vuln_id (required), active_only, only_analyzed. Returns: result

set_analysisA

⚠ WRITE. Update the analysis record for one finding.

Two ways to identify the finding:

  • Pass component_uuid and vulnerability_uuid directly.

  • Pass finding — a NormalizedFinding dict as returned by list_findings, group_findings_by_alias, or entries inside find_duplicate_analyses. The UUIDs are extracted automatically, avoiding copy-paste errors in the triage loop.

When finding is provided, its UUIDs take precedence. project_uuid is always required because findings from find_duplicate_analyses → other_projects may belong to a different project.

Issues PUT /api/v1/analysis; the connection-layer guard refuses any other write path. Fields left as None are omitted from the body, so DT keeps its current value. comment appends to the history, it does not replace existing comments. Returns the full normalized analysis after the write.

Args: project_uuid: DT project UUID. state: One of NOT_SET, IN_TRIAGE, EXPLOITABLE, FALSE_POSITIVE, NOT_AFFECTED, RESOLVED. component_uuid: DT component UUID (required unless finding is provided). vulnerability_uuid: DT vulnerability UUID (required unless finding is provided). finding: A NormalizedFinding dict. When provided, component_uuid and vulnerability_uuid are extracted from it. justification: Optional CycloneDX justification enum (e.g. CODE_NOT_REACHABLE, REQUIRES_CONFIGURATION). response: Optional response enum (e.g. CAN_NOT_FIX, WILL_NOT_FIX, UPDATE, ROLLBACK, WORKAROUND_AVAILABLE). details: Optional free-text analysis details. comment: Optional free-text comment appended to the history. suppressed: Optional bool to suppress/unsuppress the finding.

Inputs: project_uuid (required), state (required), component_uuid, vulnerability_uuid, +6 more. Returns: object

upload_bomA

⚠ WRITE. Upload a CycloneDX/SPDX SBOM to Dependency-Track.

Issues POST /api/v1/bom with the SBOM as a base64-encoded string. Returns an upload token — the caller should poll GET /api/v1/bom/token/{token} (not an MCP tool in v0.2) to detect when processing finishes and findings become visible. When auto_create=True, the project is created if missing; this requires the PROJECT_CREATION_UPLOAD permission in DT.

Args: project_name: Target project name (must exist unless auto_create=True). project_version: Target project version. bom: Base64-encoded SBOM document (CycloneDX or SPDX). auto_create: Create project/version if missing. Requires extra permission. parent_name: Optional parent project name for hierarchy. parent_version: Optional parent project version.

Inputs: project_name (required), project_version (required), bom (required), auto_create, +2 more. Returns: object

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A4.6/5.0

    Scored across 14 tools

    Disambiguation5/5

    Each tool has a distinct purpose in the triage workflow. For example, broadcast_triage and carry_over_triage are clearly differentiated, and finding deduplication tools like find_duplicate_analyses and group_findings_by_alias serve different scopes.

    Naming Consistency5/5

    All tools use consistent snake_case naming with a verb_noun pattern (e.g., list_findings, upload_bom, set_analysis), making them predictable and easy to navigate.

    Tool Count5/5

    With 14 tools, the server is well-scoped for a Dependency-Track integration, covering project management, finding analysis, vulnerability search, and SBOM upload without excess.

    Completeness4/5

    The tool set covers the core triage and analysis lifecycle comprehensively. Minor gaps exist, such as explicit project creation or deletion tools, but these are partially addressed by upload_bom with auto_create.

    Maintenance

    ActivityInactive
    ResponsivenessOutdated data

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      B
      quality
      D
      maintenance
      This MCP server transforms Claude into a comprehensive security analyst by providing access to 27 security tools across 21 APIs for vulnerability intelligence. It enables users to query multiple sources like NVD, EPSS, CISA KEV, and threat intelligence platforms in parallel to get correlated security insights and risk assessments for CVEs.
      28
      1,766
      Apache 2.0
    • A
      license
      Not graded
      quality
      D
      maintenance
      Security scanning MCP server that connects Claude to RepoMend findings, enabling vulnerability management and automated fix drafting.
      MIT