Security Scanning · 2026-07-12 · Security Scanning arc
MCP Tools for Security Scanning: Severity Models, Null-Result Traps, and Subprocess Integration Across Snyk, Trivy, Semgrep, Checkov, and Grype
When you build your first MCP tool that runs a security scanner, three problems surface in a predictable sequence regardless of which scanner you chose. You collect severity labels from two different tools and try to compare them — not realizing that Snyk's "high", Trivy's "HIGH", Semgrep's "ERROR", Checkov's (absence of severity), and Grype's "High" all measure different things using different scales that cannot be normalized into a single ranking without losing meaning. You check that the scan returned no findings and report health — not realizing that a null result, an empty array, an empty results field, a non-zero exit code, and a parsing error are all "no findings" in different scanners but signal completely different conditions about whether the scan ran at all. You invoke your scanner and it works in your local environment, then fails silently in production — not realizing that some scanners require only an API token while others require filesystem access, running Docker, or a pre-cached vulnerability database that is 50–200 MB. By the second scanner you integrate, you recognize all three problems wearing a different CLI's uniform. This synthesis covers five security scanning tools — Snyk, Trivy, Semgrep, Checkov, and Grype — through the three structural patterns they all share, so you can implement them correctly before they surface as production failures in your MCP security tooling.
TL;DR
Five security scanners, three shared patterns. (1) Severity scales are incompatible and cannot be normalized: every scanner uses a different scale measuring a different concept — Snyk uses critical/high/medium/low (CVE database severity, from NVD or GHSA), with actionability driven by isUpgradable and isPatchable flags that matter more than severity alone; Trivy uses all-caps CRITICAL/HIGH/MEDIUM/LOW/UNKNOWN (distro-vendor advisory severity, not always NVD — same CVE can be HIGH in NVD but MEDIUM in Alpine because Alpine's build doesn't include the vulnerable code path); Semgrep uses ERROR/WARNING/INFO (pattern confidence, not CVSS — ERROR means "this pattern is essentially always a security defect", not "this is a CRITICAL severity exploit"); Checkov has no severity output in the open-source CLI — only PASSED/FAILED/SKIPPED — you must maintain a SEVERITY_MAP keyed on check IDs; Grype uses capitalized-first-letter Critical/High/Medium/Low/Negligible/Unknown (same NVD/vendor data as Trivy but with an extra "Negligible" level used by Red Hat that breaks five-level severity comparators). Never translate between these scales in MCP tool output — present each scanner's severity in its own native terminology. (2) Null, empty, and absent are three different things: Trivy's result.Vulnerabilities is explicitly null (not []) when a scan target has zero findings — a deliberate design choice that distinguishes clean from not-applicable; guard with result.Vulnerabilities ?? [] or crash on your first scan of a clean image; Semgrep's results[] is an empty array on no findings with exit code 0, but exit code 1 means findings were found (not an error) and exit code 2 means an actual error — wrapping exit code 1 as a process exception crashes your tool on every scan that finds anything; Checkov's failed_checks[] is always an array, but parsing_errors[] is the hidden trap — a Terraform file that fails to parse silently produces zero check results, so a clean failed_checks: [] may mean "nothing is wrong" or "nothing was evaluated"; Grype's ignoredMatches[] is separate from matches[] and not included in the default summary counts — suppressed CVEs are invisible unless you explicitly inspect this field. (3) Scan target diversity determines infrastructure requirements: Snyk uses a REST API with manifest content in the request body — no filesystem access needed, scans run from any network-connected environment including serverless functions; Trivy, Grype, Semgrep, and Checkov are subprocess-based CLI tools that need to be installed on the host running the MCP server — Trivy and Grype also need a pre-cached vulnerability database (Trivy's is ~200 MB with 24-hour TTL, Grype's is ~50 MB with 5-day warning threshold); container image scanning additionally requires Docker registry access or a local archive. Wire your scanner's health status to AliveMCP by exposing a lightweight endpoint that attempts a minimal probe scan — a one-package Snyk test, a single-file Trivy fs scan — to confirm the scanner is functional, its database is fresh, and its credentials are valid.
Pattern 1: Severity scales are incompatible — never normalize across tools
The most common mistake in MCP security tool aggregation is building a normalized severity ranking across multiple scanners. The instinct is reasonable: you have output from Snyk and Trivy, you want to sort everything from most severe to least severe, so you map ERROR→CRITICAL, HIGH→HIGH, WARNING→MEDIUM, and present a unified list. This mapping is wrong in every direction it is applied, and it misleads the AI client in ways that matter operationally.
Each scanner's severity scale measures a different concept. Understanding what each scale actually measures is the prerequisite to using scanner output correctly.
Snyk: severity plus actionability flags
Snyk's severity field (critical/high/medium/low) comes from the CVE database and measures the same thing as NVD severity: potential impact if the vulnerability is exploited. But Snyk severity alone is a poor MCP tool output because it omits the actionability dimension that determines what a developer should do right now.
A CRITICAL severity Snyk finding with isUpgradable: false and isPatchable: false — meaning the vulnerable package is unmaintained or requires a breaking major version upgrade to fix — cannot be resolved by a developer today regardless of urgency. Meanwhile, a MEDIUM severity finding with isUpgradable: true and a clear upgradePath[] can be eliminated in seconds with npm update. An MCP tool that sorts only by severity presents the CRITICAL unmaintainable finding first and buries the immediately-fixable MEDIUM finding.
The correct prioritization model for Snyk output uses three dimensions: severity (risk if exploited), isUpgradable (can it be fixed by upgrading a dependency?), and isPatchable (can Snyk's backport patch system fix it without upgrading?). Present these as three groups: "fixable now", "patchable", and "no fix available" — within each group sorted by severity.
Deduplication is also mandatory before presenting Snyk results. The same CVE appears once per dependency chain path — if both express and apollo-server depend on a vulnerable version of qs, you get two entries for the same SNYK-JS-QS-* vulnerability. Deduplicate by id before building any fix report.
Trivy: severity from the distro advisory, not NVD
Trivy's Severity field is all-caps (CRITICAL/HIGH/MEDIUM/LOW/UNKNOWN) and comes from the distribution maintainer's security advisory when one exists, not directly from NVD. This is the right severity to use for filtering — it accounts for whether the Alpine or Debian build actually includes the vulnerable code path — but it is not the same number as the NVD CVSS score.
The SeveritySource field tells you which database provided the severity: "nvd", "redhat", "debian", "alpine", "ghsa", and others. The raw CVSS numeric score is in CVSS.nvd.V3Score. A common mistake is filtering by CVSS.nvd.V3Score >= 7.0 while displaying the Trivy Severity label — when these disagree, the filter silently drops HIGH-labeled findings (distro severity HIGH, NVD score 6.8) or surfaces MEDIUM-labeled findings (distro severity MEDIUM, NVD score 7.2) that the developer's understanding of the filter would exclude.
Always use Trivy's categorical Severity field for filtering and labeling in MCP output. Only use the numeric CVSS score when you need to sort within a severity category and you explicitly show users that the sort key is NVD-scored.
Semgrep: confidence, not CVSS
Semgrep's severity scale — ERROR, WARNING, INFO — measures the rule author's confidence that the matched pattern represents a security defect, not the potential impact of exploitation. ERROR means the pattern is essentially always a problem: eval(userInput), string concatenation into a SQL query, hardcoded AWS credentials. WARNING means the pattern is often problematic but depends on context. INFO means the match is worth awareness but is not necessarily a defect.
Never translate Semgrep severity to CVSS language. Do not say "this ERROR is a CRITICAL finding." The scales are incompatible. A Semgrep ERROR finding for a SQL injection pattern in a unit test fixture is not exploitable; a Semgrep INFO finding for a deprecated crypto function in a payment handler may be more consequential than the label suggests. The extra.metadata.confidence field is a better automated-action gate than severity: HIGH confidence rules have near-zero false-positive rates and are safe for CI gating; LOW confidence rules can exceed 50% false-positive rates and should never trigger automated actions.
The correct MCP output pattern for Semgrep is grouping by check_id with a count of occurrences, showing the rule's extra.message explanation (not just the rule ID), and separately flagging HIGH-confidence ERROR findings as the only candidates for immediate developer action.
Checkov: no severity in the open-source CLI
Checkov does not output severity levels in the open-source CLI. The output contains only PASSED, FAILED, and SKIPPED states per check. This is intentional: the same open security group (CKV_AWS_25) is CRITICAL for a production database and perhaps MEDIUM for a developer sandbox environment behind a VPN. Severity is a policy decision for your team, not a universal fact Checkov can determine from the resource alone.
The Prisma Cloud SaaS platform adds severity to its check database, but those values are not available in the CLI JSON output. You must maintain a SEVERITY_MAP keyed on the check IDs your team has opinionated on (CKV_AWS_18: access logging enabled on S3 buckets → MEDIUM; CKV_AWS_57: S3 bucket public access blocked → HIGH). For unmapped check IDs, assign a default of LOW or MEDIUM and flag them for human triage. MCP tools that present Checkov results without a severity annotation give the AI client no way to prioritize remediation.
Grype: "Negligible" breaks five-level severity comparators
Grype's severity format is capitalized-first-letter: Critical, High, Medium, Low, Negligible, Unknown. Two things break utility functions that handle any other scanner's output. First, the capitalization: Trivy uses all-caps HIGH, Grype uses title-case High. Any shared severity-ranking function must normalize to a common case before applying a rank map. Second, Grype includes a Negligible severity level used by Red Hat family distributions (RHEL, CentOS, Fedora) for CVEs they have assessed as having essentially no real-world impact in their build. A severity rank array of ["Unknown", "Low", "Medium", "High", "Critical"] will throw an index error or return undefined when it encounters Negligible. Always explicitly handle Negligible, placing it between Low and Unknown in the risk hierarchy.
Grype also has richer fix state granularity than most scanners. The vulnerability.fix.state field has three values with distinct action profiles: "fixed" (an upgrade exists and resolves the CVE), "wont-fix" (the OS vendor has reviewed the CVE and decided not to patch this release — different risk posture than "unknown"), and "unknown" (no vendor determination yet). The wont-fix state should not be auto-ignored — it represents a vendor's conscious decision, documented with the reasoning in vulnerability.advisories[], and should be re-evaluated if the CVE is later upgraded in severity or your deployment environment changes.
Pattern 2: Null, empty, and absent results are three different conditions
The second shared failure pattern is assuming that "no findings" always looks the same across scanners. In practice, each tool signals "the scan completed clean" differently, and conflating these signals with error conditions — or worse, treating a scan that never ran as a clean scan — produces false health reports that hide real vulnerabilities.
Trivy: Vulnerabilities is null, not []
In Trivy's JSON output, each entry in the Results[] array represents a component class found in the scan target — OS packages, language packages, IaC configs, secrets. When a component class was scanned and found zero vulnerabilities, the Vulnerabilities field on that result entry is explicitly null, not an empty array. This is a deliberate design choice: Trivy uses null to distinguish "this target was scanned clean" from "this field doesn't apply to this result type" (config class results don't have a Vulnerabilities field at all).
The consequence is that result.Vulnerabilities.forEach() throws a TypeError on the first clean image your MCP tool scans. Always use (result.Vulnerabilities ?? []).forEach(). Images with no findings are not edge cases — after a proper base image update cycle, the majority of production images should have no HIGH or CRITICAL findings.
Semgrep: exit code 1 means findings, not failure
Semgrep uses an exit code convention borrowed from diff and grep: exit code 0 means "ran successfully, found nothing"; exit code 1 means "ran successfully, found something"; exit code 2 means "encountered an error and may not have produced valid output." Most CLI tools use non-zero exit codes exclusively for errors, so naive subprocess wrappers check if (status !== 0) throw new Error(...) and crash on every scan that finds issues — which in a typical codebase scan is every scan.
The correct subprocess wrapper for Semgrep checks status !== 2 as the "scan ran successfully" condition (treating both 0 and 1 as success), and separately checks results.length === 0 to determine whether the scan found anything. Handle exit code 2 as a tool failure requiring a different error path than "scan succeeded with findings."
Checkov: the parsing_errors[] silent miss
In Checkov's compact JSON output (--compact is mandatory for predictable parsing), the parsing_errors[] array is the hidden health signal. A Terraform file with invalid HCL syntax, a truncated YAML manifest, or an unsupported Kubernetes API version causes Checkov to generate zero check results for all resources in that file — without any signal in failed_checks[]. A tool that checks only failed_checks.length === 0 and reports "clean" has potentially missed an entire module worth of resources.
Always surface parsing_errors[] before the findings summary in MCP tool output. If parsing errors are present, report them explicitly: "Checkov found 3 failed checks in the resources it evaluated, but could not parse 2 files — review parsing errors before treating this as a complete scan result." The omission of this step is operationally treacherous because the failure is invisible: the LLM sees a clean scan and proceeds with confidence.
Grype: ignoredMatches[] are invisible in the default summary
Grype's JSON output separates active findings from suppressed ones into two distinct arrays: matches[] for findings that pass all filters and ignoredMatches[] for findings that were suppressed via a .grype.yaml ignore configuration. The default summary counts and the --fail-on exit code threshold both operate only on matches[] — suppressed findings are not counted. A Grype scan that reports zero HIGH and CRITICAL findings may have dozens of suppressed findings that an operator decided to ignore months ago and never re-evaluated.
In MCP tool output, include the count of suppressed findings alongside the active findings summary: "0 active HIGH findings, 14 suppressed findings (14 marked ignored in .grype.yaml)." This surfaces the suppression debt without disrupting the normal alert flow.
Pattern 3: Scan target diversity determines infrastructure requirements
The third shared failure pattern is discovered at deployment time rather than during development: your MCP security tool works perfectly locally but silently fails in production because the infrastructure requirements are completely different between scanners. Some scanners need only an API token; others need a binary installed on the host, a pre-cached database, Docker access, or filesystem access to the code being scanned. Understanding these requirements before wiring a scanner into your MCP tool determines what can be deployed where.
Snyk: REST API, no filesystem required
Snyk's scanning model is fundamentally different from the other four tools in this arc. Instead of invoking a local binary, Snyk's MCP integration calls a REST API and sends the contents of the manifest files as request body data. The POST /v1/test/npm endpoint accepts the raw text of package.json and package-lock.json and returns vulnerability results immediately without creating any persistent state in the Snyk dashboard (test mode versus monitor mode). Snyk's scanning engine runs in Snyk's cloud infrastructure.
This means an MCP tool using Snyk can run in any environment that has a network connection and an API token — serverless functions, CI containers without Docker, read-only runtime environments where installing binaries is impossible. The infrastructure requirement is minimal: a valid Snyk API token and network access to api.snyk.io. The trade-off is that the manifest files must be readable by the MCP server process and transmitted to an external service.
The one non-obvious behavior to handle: lock file inclusion significantly improves scan accuracy. Without the lock file, Snyk resolves transitive dependencies from the registry at scan time, which may differ from what is installed. Always include the lock file when available.
Trivy and Grype: subprocess + local database
Both Trivy and Grype are subprocess-based CLI tools that must be installed on the host running the MCP server. They download vulnerability databases to the local filesystem before scanning — Trivy's database is approximately 200 MB and has a 24-hour TTL; Grype's is approximately 50 MB and emits a staleness warning after 5 days. In CI environments that run many parallel jobs, these downloads are a significant source of latency and bandwidth cost if not cached.
For container image scanning, both tools additionally require either Docker daemon access (to pull images for scanning) or pre-downloaded image archives (docker-archive: input for Trivy, docker-archive: input for Grype). In environments where Docker is not available — some serverless or sandbox runtimes — filesystem scanning (fs: input) can still find language package vulnerabilities without OS-level package scanning.
The SBOM-first workflow with Grype is the most portable option for restricted environments. Generate the SBOM using Syft at build time (in an environment that has full Docker and filesystem access), store the SBOM as an artifact, and run Grype against the SBOM file in the MCP server environment. The SBOM file is plain JSON — no binary or daemon required at scan time, just the Grype binary and its database. The same SBOM can be re-scanned against an updated database to find newly disclosed CVEs without rebuilding the image.
Semgrep: subprocess + code filesystem access
Semgrep is a Python-based CLI tool that scans source code files on the local filesystem. It does not scan containers or binary packages — it operates on source code and IaC files. The MCP server running Semgrep must have read access to the source directory being scanned, which is a different requirement than the container-registry access needed for image-based CVE scanners.
For large codebases, the key performance optimization is PR-scoped scanning: compute the list of changed files via git diff --name-only origin/main...HEAD, filter to source code extensions, and pass them explicitly as Semgrep's scan targets rather than scanning the entire codebase. This reduces scan time from minutes to seconds for typical PRs that modify 5–20 files. Semgrep's ci subcommand handles this automatically for supported CI platforms by reading PR context from environment variables.
Checkov: subprocess + IaC directory access
Checkov operates on IaC files — Terraform, Kubernetes YAML, Dockerfiles, CloudFormation, Helm charts — in a local directory. Always invoke Checkov with -d directory/ rather than -f single-file.tf: graph-based CKV2_* checks analyze relationships between multiple resources (e.g., whether a security group is actually attached to any instance) and only run with directory-level context. Passing a single file silently skips all CKV2 checks without error.
For Terraform, the most accurate Checkov scan operates on the resolved plan rather than raw template files. Use terraform show -json tfplan.bin > tfplan.json followed by checkov -f tfplan.json --file-type json to scan the concrete resource values after variable substitution. Raw file scanning misses issues where the problematic value comes from a variable, module output, or data source that is only resolved at plan time.
Cross-tool comparison
| Tool | Input method | Severity scale | Fix state granularity | Coverage | CI exit code for findings |
|---|---|---|---|---|---|
| Snyk | REST API (manifest content in body) | critical / high / medium / low + isUpgradable + isPatchable | upgradePath[] (exact fix version), isPatchable (backport patch) | npm, pip, Go, Maven, Gradle, Docker (via API) | Configurable — default is non-zero on findings; test mode separate from monitor |
| Trivy | Subprocess (image ref, dir:, fs:, sbom:) | CRITICAL / HIGH / MEDIUM / LOW / UNKNOWN (distro advisory, not always NVD) | FixedVersion (non-empty = fix exists); WillNotFix status in advisory data | OS packages, language packages, IaC, secrets, licenses (multi-scanner in one binary) | 0 = clean; 1 = findings (only when --exit-code 1 is passed); default = 0 always |
| Semgrep | Subprocess (source directory or file list) | ERROR / WARNING / INFO (pattern confidence, NOT CVSS — different concept entirely) | extra.fix (autofix string) or extra.fix_regex when deterministic fix exists; no upgrade path | SAST: JS, TS, Python, Go, Java, Ruby, PHP, Scala, Kotlin, IaC (Terraform, Dockerfile) | 0 = clean; 1 = findings found; 2 = scan error — treat 0 and 1 as success |
| Checkov | Subprocess (IaC directory or plan JSON) | No severity output in OSS CLI — PASSED / FAILED / SKIPPED only; must maintain custom SEVERITY_MAP | No fix state — check_result is PASSED or FAILED; suppress via inline comment | Terraform, CloudFormation, Kubernetes, Helm, Dockerfile, Ansible, ARM, Bicep | 0 = all checks passed; 1 = checks failed; 2 = tool error — treat 0 and 1 differently |
| Grype | Subprocess (image ref, dir:, sbom:) + Syft SBOM-first | Critical / High / Medium / Low / Negligible / Unknown (title-case, includes 6th level) | vulnerability.fix.state: "fixed" / "wont-fix" / "unknown" (explicit vendor non-fix decision) | OS packages, language packages (npm, pip, Go, Ruby, Java); no IaC or SAST | 0 = clean; 1 = findings at or above --fail-on threshold; default = 0 always |
AliveMCP health probes for scanner infrastructure
Security scanners have their own health conditions that are independent of the code being scanned. Trivy and Grype need a current vulnerability database — a database older than 24 hours (Trivy) or 5 days (Grype) degrades scan accuracy without failing loudly. Snyk needs a valid API token with sufficient rate limit headroom (Snyk's default limit is 1,620 requests per minute, but a burst from a CI pipeline can exhaust it temporarily). Semgrep's --config auto mode downloads rules on first run and caches them; a rule download failure silently reduces scan coverage.
The probe pattern for each scanner is different, but the goal is the same: a minimal scan that confirms the scanner binary (or API) is reachable, its credentials are valid, and its data sources are fresh.
For Snyk, the probe is a GET /v1/orgs/ call that resolves the org ID and confirms the token is valid. This is fast (<200ms) and has negligible rate limit impact. For Trivy and Grype, the probe is a trivy fs --list-all-pkgs /dev/null or grype dir:/dev/null command — it exercises the binary and database load without actually scanning anything meaningful. For Semgrep, the probe is a scan of a single file with a known-safe content to confirm the rules download and parse correctly. For Checkov, the probe is a scan of a directory containing a single known-safe Terraform file.
Register these probe endpoints with AliveMCP to detect scanner infrastructure failures before they produce silent security coverage gaps. A Trivy scan that fails because the database is 48 hours stale looks identical to a successful clean scan from the outside — you only discover the failure when a critical CVE appears in production that should have been caught at build time. AliveMCP's continuous probing surfaces database staleness, credential rotation failures, and binary removal events as operational alerts rather than post-incident discoveries.
This probe pattern also applies to the broader dependency security toolchain. When your MCP server integrates multiple scanners — Snyk for dependency CVEs, Semgrep for SAST, Checkov for IaC misconfigurations — each scanner's health is an independent failure mode. An MCP tool that calls three scanners and one silently fails returns partial results to the AI client with no indication that coverage is incomplete. Health monitoring closes this gap.
Implementation checklist for every security scanning MCP integration
- Present each scanner's severity in its native terminology. Do not translate Semgrep ERROR → CRITICAL, or Snyk high → Trivy HIGH, or Checkov FAILED → any severity level. Each scale measures a different concept. The AI client receiving your MCP tool output must understand that it is reading a Semgrep confidence rating, not a CVSS score, when it sees "ERROR". Label the source tool and scale explicitly in every finding you return.
- Guard every result field against null, absent, and empty. In Trivy, use
result.Vulnerabilities ?? []before iterating. In Semgrep, checkstatus !== 2as the success condition (notstatus === 0). In Checkov, checkparsing_errors[]before trustingfailed_checks[]. In Grype, includeignoredMatches.lengthin the summary alongsidematches.length. In Snyk, deduplicate byidbefore counting findings. - Validate scanner infrastructure before returning scan results. If the vulnerability database is stale, the API token is expired, or the binary is missing, return an explicit error — do not return empty results that look like a clean scan. Surface the distinction between "scan ran clean" and "scan did not run" as two different MCP tool responses so the AI client can differentiate between health and silence.
- Match access requirements to deployment environment. Snyk can run anywhere with network access. Trivy and Grype need the binary, the database, and Docker or filesystem access for image scanning. Semgrep needs the binary and source code access. Checkov needs the binary and IaC directory access. Wire only the scanners whose access requirements match your MCP server's deployment context — running Trivy in a read-only serverless environment is an infrastructure error, not a Trivy error.
Further reading
- MCP Tools for Snyk — REST API, vulnerability severity, and fix prioritization
- MCP Tools for Trivy — subprocess integration, Results[] structure, and SeveritySource
- MCP Tools for Semgrep — exit codes, confidence filtering, and SARIF output
- MCP Tools for Checkov — --compact flag, parsing_errors[], and severity mapping
- MCP Tools for Grype — matches[], Negligible severity, and SBOM-first workflow
- MCP Server Dependency Security — patterns for scanning package manifests
- MCP Server Security Monitoring — continuous scanning and alert routing
- MCP Server Health Checks — implementing and monitoring protocol probes
- MCP Server Error Handling — classifying failures and implementing retry logic
- MCP Server Security Hardening — audit logging, CORS, SSRF, and security headers
- MCP Tools for Security Platforms — AWS IAM, Okta, Auth0, OPA, and Cloudflare Access
- MCP Tools for DevOps Platforms — patterns across CloudWatch, Jenkins, Vault, and ArgoCD