OSV-Scanner-MCP
Summary: This MCP server wraps Google's OSV-Scanner so an LLM client can detect known vulnerabilities (CVE/GHSA) in a project's dependencies, explain them, and recommend upgrade versions—without running package managers or builds.
scan_project— Detects and scans Java (Maven/Gradle), JavaScript (npm/yarn/pnpm/bun), Python (Poetry/uv/Pipenv/PDM/requirements.txt), Go, and Rust (Cargo.lock) lockfiles/manifests in one pass, returning severity-sorted reports (critical→unknown), fixed versions, and direct/transitive dependency info.scan_java_project— Scans only Java (Mavenpom.xml/gradle.lockfile) manifests and returns a formatted vulnerability report.suggest_fix— Recommends the upgrade version closest to the current release line (three-tier:same_minor→major_internal→cross_major), with per-CVE fix targets, tailored update hints, and verification of the candidate against api.osv.dev; also reports unfixed CVEs and pre-release caveats.explain_vulnerability— Fetches details of a GHSA/CVE ID straight from the OSV API (description, CVSS vectors, affected ranges, references) without running a scan.scan_java_artifact— Best-effort vulnerability scan of JAR/WAR archives (including nested JARs) for projects without lockfiles or with shaded/fat JARs; flags inferred/unknown coordinates.scan_sbom— Looks up dependencies recorded in a CycloneDX 1.4–1.6 or SPDX 2.2/2.3 JSON SBOM (≤16 MiB), without generating SBOMs or building anything.Coverage reporting — Every scan surfaces what was not checked (missing lockfiles, unpinned requirements, skipped/unscannable files, local crates), so zero findings can be distinguished from incomplete coverage.
Security posture — No shell execution, filesystem/path boundary checks, safe snapshots instead of original files, optional fail-closed allowed-root mode, and Node
--permissionsupport.
Provides vulnerability scanning for Java projects using Google's OSV database and OSV-Scanner, returning severity-ordered reports with CVE/GHSA details and fix suggestions.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@OSV-Scanner-MCPscan my Java project at /home/user/project for vulnerabilities"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
OSV-Scanner-MCP
An MCP server that wraps Google's OSV-Scanner. Ask an MCP client such as Claude to "check this project for vulnerabilities", and it returns the known vulnerabilities (CVE / GHSA) in your dependencies as a report sorted by severity, together with recommended upgrades.
Status: Published on npm (
npx -y osv-scanner-mcp). Supports lockfiles and manifests for Java (Maven / Gradle), JavaScript (npm / yarn / pnpm / bun), Python (Poetry / uv / Pipenv / PDM / requirements.txt), Go, and Rust (Cargo.lock), with upgrade recommendations for Maven, npm, PyPI, Go, and crates.io. Setup instructions are provided for Claude Code, Claude Desktop, Codex CLI, Antigravity, and VS Code (GitHub Copilot).
Features
One-shot multi-language scan: Pass a project path to
scan_project, and it detects and scans the Java, JavaScript, Python, Go, and Rust lockfiles in the project together. Dependencies that could not be scanned (for example, a manifest without a lockfile or an unpinned requirement) are reported up front incoverageUpgrade recommendations:
suggest_fixrecommends, for each vulnerable package, the upgrade closest to the current release line that addresses its fix targets, for Maven, npm, PyPI, Go, and Rust (crates.io). Known unfixed and informational advisories remain visible. The recommended version is also checked against the OSV database for additional fix-target vulnerabilitiesRust without false positives: For
Cargo.lock, only crates from crates.io are scanned. Your own workspace crates, git dependencies, and crates from private registries are not looked up as crates.io crates (OSV-Scanner alone would match them by name, and send their names to the OSV API). RustSec notices about unmaintained or unsound crates are labeledinformationaland counted apart from vulnerabilitiesDirect and transitive dependencies: Packages are marked as direct or transitive dependencies (from
package-lock.json,go.mod,requirements.txt, andpom.xml), and update hints are tailored accordingly, for example by naming the direct dependency that pulls in a transitive oneJava project scan:
scan_java_projectscans only the Java (Maven / Gradle) manifests and returns a formatted reportJAR/WAR scan:
scan_java_artifactdetects known vulnerabilities from the metadata inside JAR/WAR archives, for projects without a lockfile or with only shaded/fat JARs (best-effort identification, stated explicitly incoverage)SBOM scan:
scan_sbomchecks the dependencies recorded in a CycloneDX/SPDX JSON SBOM. It states explicitly that the SBOM's completeness and its match with the actual artifacts are not verifiedSeverity-sorted reports: Vulnerabilities are grouped by package and sorted by CVSS score, with five severity labels (critical / high / medium / low / unknown) and summary counts
Fixed versions: Each vulnerability includes
fixed_versions, sorted correctly by Maven version precedence (including non-semver forms such as2.17.1-RELEASE), Semantic Versioning precedence for npm, Go, and Rust, and PEP 440 for PyPISecurity first: No shell execution, a fixed argument allowlist, path normalization and boundary checks, scanning private copies instead of the original files, and timeouts and output size limits are built in
Related MCP server: maven-mcp
Requirements
Node.js >= 20.19
The OSV-Scanner binary — no manual installation needed. If it is not found, a pinned version is downloaded from the official GitHub Releases and verified against a SHA256 checksum embedded in the package before use (cached in
~/.cache/osv-scanner-mcp/)A manually installed binary (on
PATHor set withOSV_SCANNER_PATH) is used firstSet
OSV_MCP_AUTO_DOWNLOAD=0to disable the automatic downloadSet
OSV_MCP_PREFER_DOWNLOAD=1to always use the verified downloaded binary instead of one onPATH(recommended for production)The pinned version is osv-scanner 2.7.0. Its binary is cached in
~/.cache/osv-scanner-mcp/v2.7.0/; directories of earlier versions (such asv2.4.0/) are no longer used and can be deleted
Scans,
suggest_fix, andexplain_vulnerabilityaccess the network. Vulnerabilities are looked up in the OSV database (api.osv.dev), and scanningpom.xmlorrequirements.txtalso connects to deps.dev (api.deps.dev) to resolve transitive dependencies. See Network destinations and privacy for details and how to turn this off
Setup
Claude Code
claude mcp add osv-scanner -- npx -y osv-scanner-mcpClaude Desktop
Add to claude_desktop_config.json:
{
"mcpServers": {
"osv-scanner": {
"command": "npx",
"args": ["-y", "osv-scanner-mcp"]
}
}
}Codex CLI
codex mcp add osv-scanner -- npx -y osv-scanner-mcpOr add to ~/.codex/config.toml:
[mcp_servers.osv-scanner]
command = "npx"
args = ["-y", "osv-scanner-mcp"]
startup_timeout_sec = 60 # allow time for the first npx package download
tool_timeout_sec = 300 # default is 60 seconds; allow for the binary download plus a scan (120 seconds by default)Note: Codex's MCP tool timeout defaults to 60 seconds, while this server's scan timeout defaults to 120 seconds. With the default, Codex may time out first on the first run (which downloads OSV-Scanner) or on larger projects. Raising
tool_timeout_secas above is recommended.
Antigravity
Open MCP Servers → Manage MCP Servers → View raw config in the agent panel and add to mcp_config.json (same format as Claude Desktop):
{
"mcpServers": {
"osv-scanner": {
"command": "npx",
"args": ["-y", "osv-scanner-mcp"]
}
}
}VS Code (GitHub Copilot)
code --add-mcp '{"name":"osv-scanner","command":"npx","args":["-y","osv-scanner-mcp"]}'Or add to the workspace's .vscode/mcp.json (also available from MCP: Add Server in the Command Palette):
{
"servers": {
"osv-scanner": {
"type": "stdio",
"command": "npx",
"args": ["-y", "osv-scanner-mcp"]
}
}
}From source
git clone https://github.com/tedorigawa001/OSV-Scanner-MCP.git
cd OSV-Scanner-MCP
npm install
npm run build
# When registering, use `node /path/to/OSV-Scanner-MCP/dist/index.js` instead of `npx -y osv-scanner-mcp`Environment variables
Variable | Description |
| Explicit path to the osv-scanner binary. If unset, |
| If set, scans outside this directory are refused (the boundary against path traversal). Recommended. An empty or whitespace-only value is treated as unset |
| If |
| Maximum number of concurrent scans (default |
|
|
| If |
| If |
| If |
Recommendation: The server works without
OSV_MCP_ALLOWED_ROOT, but then any absolute path can be scanned. To prevent malicious instructions (prompt injection) from scanning unintended directories, set it to the root of your projects (for example~/projects). Pass it in the client configuration, for example"env": {"OSV_MCP_ALLOWED_ROOT": "/Users/you/projects"}(in Codex CLI's TOML, use a[mcp_servers.osv-scanner.env]section).
Recommended production configuration: On shared servers, CI, and other production environments, set all three of the following:
OSV_MCP_ALLOWED_ROOT=/root/of/scan/targets— fixes the scan boundary
OSV_MCP_REQUIRE_ALLOWED_ROOT=1— refuses to start without a boundary (fail-closed)
OSV_SCANNER_PATH=/absolute/path/owned/by/an/administratororOSV_MCP_PREFER_DOWNLOAD=1— fixes which binary is run, independent ofPATHCheck Network destinations and privacy for where dependency information is sent. In every configuration, package names and versions are sent to
api.osv.devfor the vulnerability lookup.
Running with restricted permissions
As an additional layer of defense, you can limit what the server can read and write with Node's permission model (--permission). This is optional and does not change the default way of starting the server. --permission is available in Node 22.13, 23.5, and later (Node 20's experimental --experimental-permission has not been tested). Since npx cannot pass Node flags, start the server with node directly:
{
"mcpServers": {
"osv-scanner": {
"command": "node",
"args": [
"--permission",
"--allow-fs-read=/path/to/OSV-Scanner-MCP",
"--allow-fs-read=/Users/you/projects",
"--allow-fs-read=/var/folders/xx/yyyy/T",
"--allow-fs-read=/private/var/folders/xx/yyyy/T",
"--allow-fs-write=/private/var/folders/xx/yyyy/T",
"--allow-fs-read=/opt/homebrew/bin/osv-scanner",
"--allow-child-process",
"/path/to/OSV-Scanner-MCP/dist/index.js"
],
"env": {
"OSV_MCP_ALLOWED_ROOT": "/Users/you/projects",
"OSV_SCANNER_PATH": "/opt/homebrew/bin/osv-scanner"
}
}
}
}Read access: the server itself (the directory containing
distandnode_modules), the scan targets (OSV_MCP_ALLOWED_ROOT), the temporary directory, and the osv-scanner binaryThe temporary directory (
os.tmpdir(),$TMPDIRon macOS) needs read access under both the symlinked and the resolved path (on macOS,/var/folders/...is a symbolic link to/private/var/folders/...). Write access is needed on the resolved pathIf you use the automatic download, also allow reading and writing the cache (
$XDG_CACHE_HOME/osv-scanner-mcp, by default~/.cache/osv-scanner-mcp). Otherwise, setOSV_SCANNER_PATH--allow-child-processis required to run osv-scanner. osv-scanner runs as a child process and is not restricted by the permission model (Node itself warns that this flag weakens the permission model). The impact is limited because osv-scanner only receives verified private copies, but use an OS-level mechanism such as a container if you need stronger isolationIf a required permission is missing, the server prints a warning to stderr at startup, and scans return
permission_denied(naming the missing permission and the path)
Network destinations and privacy
Destinations confirmed with osv-scanner v2.4.0 (2026-10-07) and v2.7.0, the pinned version (2026-10-15):
Operation | Destinations | Information sent |
Scanning |
| Package names and versions. To resolve transitive dependencies, the names and versions of the dependencies declared in |
Scanning |
| As with |
Scanning lockfiles ( |
| Package names and versions (lockfiles already list every dependency, so nothing is resolved remotely). For |
|
| Names and versions of the identified packages |
|
| The names of packages with a recommendation (already sent during the scan) and the candidate versions (published fixed versions). Disable with |
|
| The vulnerability ID |
Automatic binary download (first run only) | GitHub (official Releases) | Nothing (downloads the pinned binary) |
Both api.osv.dev and deps.dev are operated by Google. In every configuration, the names and versions of scanned packages are sent to api.osv.dev for the vulnerability lookup (offline lookup is not supported).
To stop sending data to deps.dev: Set
OSV_MCP_NO_REMOTE_RESOLUTION=1. Transitive dependencies ofpom.xmlandrequirements.txtare then not resolved, and onlyapi.osv.devis contacted. This stops only the requests to deps.dev; requests to OSV continue. Only the dependencies written in those files are scanned, so vulnerabilities in transitive dependencies are missed. Responses ofscan_project/scan_java_project/suggest_fixshow this astransitive_resolution: "disabled"independency_resolutionwith a warning, so it can be told apart from zero findings. To scan transitive dependencies without deps.dev, use lockfiles (gradle.lockfile,poetry.lock, and so on)Arbitrary repositories are never contacted: osv-scanner's
--data-source nativemode connects to any URL listed in<repositories>of the scannedpom.xml(a maliciouspom.xmlcould make it contact an attacker's server). This server never uses that mode and setsdeps.devexplicitly
Tools
scan_project
Detects the lockfiles and manifests in a project and scans the Java / JavaScript / Python / Go / Rust dependencies together. Package managers and builds are never run.
Input
Parameter | Type | Description |
| string | Absolute path to the project directory, or to a supported lockfile or manifest (a file given directly is the only file scanned) |
Supported files
Ecosystem | Files |
Java (Maven) |
|
JavaScript (npm) |
|
Python (PyPI) |
|
Go |
|
Rust (crates.io) |
|
.git, node_modules, target, build, .idea, .vscode, .venv, venv, site-packages, __pycache__, .tox, vendor, .cargo, and symbolic links are not searched. Only the detected files are passed to OSV-Scanner, each with its format given explicitly (coverage.manifests in the response is exactly the scan scope). The search limits are the same as for scan_java_project.
requirements.txt files are not passed to OSV-Scanner as they are. The server parses them, rewrites only the dependency lines it can interpret into simple forms such as name==version, and scans a private temporary copy (deleted after the scan). OSV-Scanner interprets include directives on its own (it even treats - r ../x.txt, with a space, as an include and reads files outside the scan scope), so the copy never contains include directives or options. Includes (-r / --requirement) are expanded by the server, and only when the included file is inside the project directory.
Cargo.lock files are not passed to OSV-Scanner as they are either. OSV-Scanner (v2.4.0 and v2.7.0) ignores where each package comes from and looks every package up as a crates.io crate, so a workspace crate, a git fork, or a private-registry crate with the same name and version as a vulnerable crates.io crate is reported as vulnerable, and its name is sent to the OSV API. The server parses Cargo.lock and scans a private copy that lists only the packages whose source is crates.io (registry+https://github.com/rust-lang/crates.io-index or sparse+https://index.crates.io/), like cargo audit. Packages replaced with [replace] are not scanned, because they are not used (the replacement is classified by its own source). The parser accepts only what Cargo writes; if a Cargo.lock contains anything else, the whole file is excluded and listed in skipped_files instead of being scanned partially. Cargo is never run.
package-lock.json copies get a resolved URL where it is missing. osv-scanner 2.7.0 treats entries without resolved as local packages and does not look them up, which would silently skip bundled dependencies (inBundle) and registry packages in lockfiles generated with npm's omit-lockfile-registry-resolved=true. The private copy that is scanned gets a public-registry URL for those entries (entries under node_modules/ that are not links and have a plain version, and aliases such as npm:lodash@4.17.20 in lockfile version 1), so they are looked up as before; the original file is not changed.
Reading the output
{
"project_dir": "/path/to/project",
"dependency_resolution": { "transitive_resolution": "enabled" },
"coverage": {
"complete": false,
"warning": "…",
"manifests": [{ "path": "web/package-lock.json", "ecosystem": "npm", "format": "package-lock.json" }],
"lockfile_missing": [{ "path": "svc/package.json", "ecosystem": "npm", "status": "missing", "hint": "…" }],
"unpinned_requirements": [{ "file": "py/requirements.txt", "line": 2, "name": "Jinja2", "specifier": ">=2.0", "kind": "lower_bound" }],
"unscannable_requirements": [
{ "file": "py/requirements.txt", "line": 4, "text": "-e git+https://…", "reason": "…" },
{ "file": "py/requirements.txt", "line": 5, "text": "-r ../shared/base.txt", "reason": "…" }
],
"local_crates": 2,
"unscannable_crates": [{ "file": "Cargo.lock", "name": "my-fork", "version": "1.0.0", "reason": "Git dependency: …" }],
"skipped_files": []
},
"ecosystem_breakdown": { "npm": { "manifests": 1, "vulnerable_package_count": 2, "vulnerability_count": 4 } },
"vulnerable_package_count": 2,
"vulnerability_count": 4,
"severity_breakdown": { "critical": 0, "high": 2, "medium": 2, "low": 0, "unknown": 0 },
"packages": [
{ "name": "minimist", "version": "1.2.5", "ecosystem": "npm", "dependency_groups": ["dev"], "dependency_relation": "direct", "declared_in": ["package.json"], "vulnerabilities": [] },
{ "name": "qs", "version": "6.7.0", "ecosystem": "npm", "dependency_relation": "transitive", "introduced_by": ["express"], "vulnerabilities": [] }
]
}Always check
coverage. Ifcompleteisfalse, some dependencies were not scanned, so zero findings does not mean the project is safelockfile_missing: manifests without a lockfile (package.json,pyproject.toml,Pipfile,setup.py,build.gradle,Cargo.toml, and so on). Not recorded if a lockfile of the same ecosystem is in the same directory. If there is only a lockfile in a parent directory, the manifest is not recorded only whenpackage-lock.json(v2 or later) is confirmed to contain that directory (npm workspaces), or whenCargo.lockcontains a local crate with the[package] nameof theCargo.tomland the directory is a member of the workspace according tomembers/excludeof theCargo.tomlnext toCargo.lock(Cargo workspaces;membersglobs with*and?are supported). A crate with the same name that cannot be confirmed as a member, such as a path dependency outside the workspace, is recorded asunconfirmed. Library crates often do not commitCargo.lock, so theirCargo.tomlis recorded here; generate one withcargo generate-lockfile. Otherwise it is recorded withstatus: "missing", orstatus: "unconfirmed"for formats that cannot be checked (yarn.lock, Python lockfiles, and so on). Generate the lockfile as described inhint(in a trusted environment) and scan againunpinned_requirements: lines inrequirements.txtthat do not pin a version.kindisunpinned(no version),range(>,<,!=,==1.*, combined ranges, and so on), orlower_bound(>=,~=). OSV-Scanner does not scanunpinnedandrangelines, and scanslower_boundlines at the lower bound (those packages are markedversion_is_lower_bound: trueand may differ from the installed version)unscannable_requirements: lines that are not scanned, with the reason:-e,name @ URL, paths, includes that were not expanded (outside the project directory, missing, URLs, or over the limits), constraint files (-c, not applied), and options or version specifiers that cannot be interpreted. Lines that cannot be interpreted are recorded here instead of being ignoredlocal_crates(only when aCargo.lockwas scanned): the number of crates without a source inCargo.lock(workspace members and path dependencies). They are your own code and are not scanned; they do not makecompletefalse. A third-party crate vendored as a path dependency is not scanned either (as withcargo audit)unscannable_crates(only when aCargo.lockwas scanned): git dependencies (their versions do not correspond to crates.io releases) and crates from registries other than crates.io (a crate with the same name on crates.io may be a different crate). They are not scanned, socompleteis false. Source URLs are not included in the responseskipped_files: files excluded from the scan, with the reason (aCargo.lockthat cannot be interpreted with certainty, arequirements.txtthat cannot be read or is larger than 1 MiB, or apom.xmlwhose parent POM chain references a file outside the allowed root). Apom.xmlscanned without its parent POM, because the parent could not be read, is also listed here with the reason (see Parent POMs)Each list holds up to 200 entries; the number of omitted entries is returned in
omitted_items
ecosystem_breakdownincludes ecosystems with zero findings, to show that they were scannedInformational advisories (Rust): RustSec also publishes notices that are not vulnerabilities:
unmaintained(the crate is no longer maintained),unsound(a soundness bug), andnotice. Most have no fixed version. When every advisory in a group is such a notice (every related ID, including aliases, must have its details available and be a notice of a known kind; otherwise it is treated as a vulnerability), the vulnerability hasinformationalset to its kind, is listed after the vulnerabilities of the package, and is counted ininformational_count(and inecosystem_breakdown) instead ofvulnerability_count,severity_breakdown, andvulnerable_package_count. If another database (such as GitHub's) lists the same issue as a vulnerability, it stays a vulnerability.informational_countappears only when there is at least onedependency_relationshows whether a package is a direct (direct) or transitive (transitive) dependency. OSV-Scanner does not report this, so the server determines it from validated manifest data and scanned copies:package-lock.json(v2 or later): dependencies of the root and workspacepackage.jsonfiles, resolved with Node's lookup rules (nestednode_modulesfirst, then parent directories), are direct; everything reachable from them is transitive. Direct dependencies list thepackage.jsonfiles that declare them indeclared_in; transitive dependencies list the direct dependencies that require them inintroduced_by(up to 10, with the rest counted inintroduced_by_omitted). A version that is both a direct dependency and required by another dependency isdirectand also hasintroduced_bygo.mod:requirelines without// indirectare direct. Modules affected by areplacedirective are markedreplaced_in_go_mod: true(OSV-Scanner reports the replacement module and version)requirements.txt: dependencies written in the file are direct; dependencies resolved through deps.dev are transitivepom.xml: OSV-Scanner reports the dependencies declared inpom.xml(and its parent POMs) and the transitive dependencies resolved through deps.dev as separate results (source.typelockfile/unknown), and this split is used (so parent POMs, profiles, properties, and dependency management are interpreted exactly as OSV-Scanner does).introduced_by/declared_inare not available. This relies on undocumented OSV-Scanner output, so anything unexpected is reported asunknownCargo.lock: crates.io dependencies immediately required by a local/workspace/path crate are direct; crates.io dependencies reachable through them (including git and alternate-registry origins) are transitive.introduced_bynames the first direct dependency on each path. References are resolved by exact name, version, and source where provided; ambiguous references are not guessed. Replacements are followed.declared_inlists project-relative Cargo.toml paths only when every declaring crate has a unique matching manifest. Otherwisedeclaring_cratesnames the unmapped declaring crates (up to 10, with the rest indeclaring_crates_omitted). Dev/build/target-specific distinctions are unavailable. Traversal over 2,000,000 edges returnsunknownOther formats (
gradle.lockfile,yarn.lock,pnpm-lock.yaml,bun.lock,poetry.lock,uv.lock,Pipfile.lock, andpdm.lock), npm lockfile version 1, and entries not reachable from the project areunknown. If lockfiles disagree, the value ismixed
dependency_groupsis the raw dependency group reported by OSV-Scanner (for exampledev). It is missing or inaccurate for some lockfile formats (absent for pnpm,optionalfor pdm, and so on), so treat it as informationalUpgrade recommendations (
suggest_fix) are available for Maven, npm, PyPI, Go, and Rust (crates.io)
scan_java_project
Scans a Java (Maven) project and returns a known-vulnerability report.
Input
Parameter | Type | Description |
| string | Absolute path to the project directory, or to a |
Gradle projects: Only the lockfile approach is supported (running the build would execute arbitrary code in
build.gradle, so it is not used for security reasons). If there is nogradle.lockfile, generate one with./gradlew dependencies --write-locks(if dependency locking is not configured, adddependencyLocking { lockAllConfigurations() }tobuild.gradle).
Scan scope: For a directory, every
pom.xml/gradle.lockfile/buildscript-gradle.lockfilebelow it is detected regardless of depth, and only the detected files are scanned (manifestsin the response is exactly the scan scope). Non-Java files in the same directories, such aspackage-lock.jsonandrequirements.txt, are not scanned..git,node_modules,target,build,.idea,.vscode, and symbolic links are not searched. If the search visits more than 200,000 entries or finds more than 1,000 manifests,manifest_search_limit_exceededis returned instead of silently truncating the results. When a manifest such aspom.xmlis given directly, the directory is not searched and only that file is scanned (also useful as a workaround when the limits are reached).
Parent POMs
OSV-Scanner reads the parent POM referenced by <parent> in pom.xml (the file at <relativePath>, or ../pom.xml by default as in Maven), follows the chain to its own parents, and includes the dependencies declared there. This is why scanning a submodule alone still detects the dependencies it inherits.
When OSV_MCP_ALLOWED_ROOT is set, a pom.xml whose parent POM chain references a file outside the allowed root at any point is excluded from the scan (so that the contents of files outside the allowed root never reach the results or the lookup services). Excluded files are listed with the reason in skipped_manifests (coverage.skipped_files in scan_project), and scope_warning says that zero findings should not be taken as safe. If every manifest is excluded, or such a pom.xml is given directly, path_outside_allowed_root is returned.
If an existing parent POM cannot be read (larger than 10 MiB, not a regular file such as a named pipe, a symbolic link at the final path component, and so on), the pom.xml is scanned without it, and the possibly missing inherited dependencies are reported with the reason in incomplete_manifests (coverage.skipped_files in scan_project, with coverage.complete set to false). If the child pom.xml itself declares no dependencies and the scan ends with no_packages_found, the error message includes the same reason. A parent POM that does not exist (for example the default ../pom.xml of a root pom.xml) is not reported, because OSV-Scanner would not read it from the original location either.
Parent POMs inside the allowed root are still read (scanning submodules inside the allowed root keeps working)
<relativePath/>(empty) does not reference a local parent POM and is ignoredThe parent reference is read the way OSV-Scanner's (Go) XML decoder reads it: elements are matched by local name regardless of namespace prefix (
<m:parent>counts as a parent), only aparentdirectly under the root element is used, and character references are expandedAs the XML specification requires, line endings (CRLF and CR) are normalized to LF before parsing
A
pom.xmlis excluded when the same interpretation cannot be guaranteed: multipleparentorrelativePathelements directly under the root, CDATA, a DOCTYPE, unknown entity references, property references (${...}), unclosed tags, content that is not valid UTF-8, or arelativePathcontaining control characters (newlines, tabs, and so on), whitespace other than a regular space, or format characters (regular spaces and non-ASCII directory names are allowed)OSV-Scanner does not read a parent whose GAV does not match, but this server does not check the GAV and excludes any existing reference outside the allowed root, to stay on the safe side
When
OSV_MCP_ALLOWED_ROOTis not set, any absolute path can be scanned anyway, so this check is not performed
Output (success)
{
"project_dir": "/path/to/project",
"manifests": ["pom.xml"],
"dependency_resolution": { "transitive_resolution": "enabled" },
"source_files": ["/path/to/project/pom.xml"],
"vulnerable_package_count": 4,
"vulnerability_count": 14,
"severity_breakdown": { "critical": 3, "high": 3, "medium": 7, "low": 0, "unknown": 1 },
"packages": [
{
"name": "org.apache.logging.log4j:log4j-core",
"version": "2.14.1",
"ecosystem": "Maven",
"vulnerabilities": [
{
"id": "GHSA-jfh8-c2jp-5v3q",
"cve": "CVE-2021-44228",
"aliases": ["CVE-2021-44228"],
"severity_score": 10,
"severity": "critical",
"summary": "Remote code injection in Log4j",
"fixed_versions": ["2.3.1", "2.12.2", "2.15.0"]
}
]
}
]
}packagesis sorted by the most severe vulnerability, and eachvulnerabilitieslist by severity (unknown last)fixed_versionslists the fixed versions recorded in OSV, in ascending order: by Maven precedence for Maven, Semantic Versioning precedence for npm, Go, and Rust (values that are not valid SemVer come last), and PEP 440 for PyPI (in the order listed by OSV up to v0.5.0); other ecosystems keep the order listed by OSV (the order is not guaranteed). Several release lines may be mixed (for example a 2.12 backport and the 2.15 line). Pre-releases (5.0.0-beta.3) and Go pseudo-versions (0.0.0-20180925071336-cf3bd585ca2a) may be included. An empty array means that OSV lists no fixed version. Up to v0.4.1, this was always empty for packages outside MavenVulnerabilities without a severity score are reported as
null/"unknown"
scan_java_artifact
Scans JAR/WAR archives themselves. This is a separate tool from the manifest-based scans.
{ "artifact_path": "/absolute/path/to/application.war" }artifact_path is the absolute path to a JAR/WAR file, or to a directory to search.
A directory search includes target and build, and excludes .git, node_modules, .idea, .vscode, and symbolic links.
The search is limited to a depth of 8, 100 archives, and 10,000 entries. If the search cannot complete within the limits, artifact_search_limit_exceeded is returned instead of silently truncating the results; narrow the target and try again.
OSV_MCP_ALLOWED_ROOT applies as well.
OSV-Scanner's java/archive plugin is used, and nested JARs are also analyzed by the scanner. No Java code is executed and no build is run.
Identification relies on metadata inside the archives, so dependencies whose metadata was removed, and dependencies inside shaded/minimized JARs, may be missed.
Reading the output:
coveragecomes first, withjars_found,jars_identified, andunidentified_jars. The counts are per outer archive found on the file system (including WARs), not the total number of nested JARs.artifacts[].statusis one ofidentified_with_vulnerabilities/identified_without_known_vulnerabilities/inferred_only/unidentified. "Identified" means that at least one Maven coordinate was found, not that every dependency was identified.inferred_onlyis an archive identified only through inferred coordinates (see below). Even when vulnerabilities are found for it, it is not counted injars_identifiedand is listed inunidentified_jarswith a hint (its findings are shown inidentified_vulnerability_count), because wrong inferred groupIds may hide other vulnerabilities.Inferred coordinates: For JARs without
pom.properties(such as the main Spring Framework JARs), OSV-Scanner infers the Maven coordinates from file names and similar clues, and often gets the groupId wrong (for examplespring-beans:spring-beansinstead oforg.springframework:spring-beans). Vulnerabilities are not matched for wrong coordinates, so known vulnerabilities are missed (for example, Spring4Shell (CVE-2022-22965) in spring-beans 5.3.2 inside the zipkin-server 2.23.2 fat JAR is not detected). Coordinates whose groupId contains no.are treated as inferred and reported incoverage.inferred_coordinates(count, items, and a warning), and the affected packages are markedcoordinates_inferred: true. Correct old-style coordinates such ascommons-io:commons-ioare included as well (erring on the safe side). Wrong inferences that contain a.(such ascom.sun.jna:jna) cannot be detected. For accurate results, scan the build's lockfile orpom.xmlwithscan_projectcoverage.completenessis alwaysincomplete.identified_vulnerability_count: 0does not mean the archives are safe.packageslists the vulnerable packages that were identified. The same package and vulnerability found in several archives are counted once in the totals.If no JAR/WAR is found,
no_scannable_artifactsis returned. If no archive can be identified, a successful report with a warning is returned.
suggest_fix remains manifest-based only. Because this tool uses an experimental plugin, the flags and the JAR/WAR output format must be re-verified whenever the pinned OSV-Scanner version is updated.
Extracting untrusted archives relies on OSV-Scanner's native code. There are timeouts and output limits, but no OS-level memory limit or sandbox is provided.
scan_sbom
Looks up the dependencies recorded in an existing SBOM with OSV-Scanner. It does not generate SBOMs, run builds, or execute JARs.
{ "sbom_path": "/absolute/path/to/release-sbom.json" }Supported formats: UTF-8 JSON CycloneDX 1.4 / 1.5 / 1.6 and SPDX 2.2 / 2.3. XML, SPDX tag-value, and SPDX 3 are not supported.
Input: the absolute path to a local regular file of 16 MiB or less. Any file name is accepted; the format is detected from the content. CycloneDX requires a
componentsarray and SPDX apackagesarray. Only the format and main structure are checked, not the full JSON Schema.Identification: Include versioned Package URLs in CycloneDX
components[].purlor SPDXpackages[].externalRefs, for examplepkg:maven/org.apache.logging.log4j/log4j-core@2.14.1. See the OSV-Scanner documentation for details.Safe reading: The allowed root and the size limit (enforced while reading) are checked, and only a private temporary copy is scanned. The original file is never modified, and the copy is deleted on success and failure. If the server is terminated during a scan (SIGTERM/SIGINT/SIGHUP, or the MCP client closing stdin), the copy is deleted and the running OSV-Scanner is stopped before exiting.
coverage comes first in the output.
identified_package_count: the number of distinct packages (by name, version, and ecosystem) identified by the scanner, including those without known vulnerabilities.unidentified_packages: packages present in the scanner output but missing a version or other information. Items the scanner itself skipped cannot be listed, so an empty array does not mean everything was checked.status:packages_identifiedif anything was identified, otherwiseno_packages_identified.completeness/artifact_match: bothnot_verified. Whether the SBOM covers every dependency, and whether it matches the actual build, are not verified.
sbom returns the original file path, the format, the specification version, and the SHA256 of the bytes that were scanned. identified_vulnerability_count and packages contain the findings for the identified dependencies. RustSec informational advisories for crates in the SBOM are counted in informational_count instead (see scan_project). Zero findings does not mean the software is safe. To cover JARs without metadata, prepare an accurate SBOM for that build.
Invalid JSON returns invalid_sbom, an unsupported format unsupported_sbom_format, input over the size limit sbom_too_large, and a missing or unreadable file sbom_not_found. An empty SBOM, or one with no identifiable packages, returns a successful report with a warning. The same timeout, output limit, and concurrency limit as the other tools apply.
suggest_fix
Runs the same detection and scan as scan_project and recommends an upgrade version for each vulnerable package. Recommendations are available for Java (Maven / Gradle), JavaScript (npm), Python (PyPI), Go, and Rust (crates.io). Instead of simply taking the highest fixed version, it picks the fixed version closest to the current release line, falling back through three tiers:
Tier | Meaning |
| A fixed version in the same release line (the smallest change) |
| A fixed version within the same major version (a minor upgrade) |
| A major upgrade is needed (may include breaking changes) |
For npm, Go, PyPI, and Rust, the "same release line" is the range a caret (^) treats as compatible (PyPI has no common compatibility rule, but some 0.x packages make breaking changes in minor releases, so the same rule is used; a change of epoch is also cross_major). From 1.0.0 the line is major.minor, as for Maven. For 0.x, only the same 0.minor is the same line, so a minor update (0.3 → 0.4) is cross_major, and for 0.0.x every update is cross_major (SemVer does not guarantee compatibility for 0.x updates).
Input: the same as scan_project (project_path)
Output (success)
{
"project_dir": "/path/to/project",
"manifests": ["pom.xml", "web/package-lock.json"],
"dependency_resolution": { "transitive_resolution": "enabled" },
"coverage": { "complete": true, "manifests": [ ... ], "lockfile_missing": [], "unpinned_requirements": [], "unscannable_requirements": [], "skipped_files": [] },
"vulnerable_package_count": 4,
"unfixed_vulnerability_count": 1,
"suggestions": [
{
"package": "org.apache.logging.log4j:log4j-core",
"current_version": "2.14.1",
"ecosystem": "Maven",
"dependency_relation": "direct",
"recommended_upgrade": "2.25.4",
"upgrade_tier": "major_internal",
"upgrade_note": "…",
"update_hint": "…",
"candidate_check": "clean",
"per_cve_detail": [
{ "id": "GHSA-jfh8-c2jp-5v3q", "cve": "CVE-2021-44228", "severity": "critical", "fixed_in": "2.15.0", "tier": "major_internal", "recommended_status": "not_affected" }
],
"verification": "verified"
}
]
}recommended_upgradeis chosen from the known fixed versions: the first candidate, in tier order and then ascending version order, that is confirmed to be outside the affected ranges of every vulnerability being fixed. It does not simply take the highest fixed version per CVE, and it excludes candidates that are affected again in another release line. It does not guarantee that the candidate is the smallest among all published versions, or that it has no undetected vulnerabilities.The OSV affected ranges (
introduced/fixed/last_affected/ open-ended) are checked:ECOSYSTEMranges by Maven precedence for Maven,SEMVER/ECOSYSTEMranges by Semantic Versioning precedence for npm and Go, andECOSYSTEMranges by PEP 440 for PyPI (non-canonical forms such as1.8c1and2.8.0-rc0are normalized). Events are sorted by version before evaluation, as in the OSV specification's evaluation algorithm. Commit-basedGITranges are ignored when the same entry has anECOSYSTEMrange. Versions explicitly listed inversionsare also checked (values that are not versions, such as Git tag names, are ignored, since they can never equal a valid candidate). If the information is incomplete (missing, malformed, or unsupported ranges such asGIT,limit, or range boundaries that cannot be parsed, such as19.03.9in some GHSAs or PyTorch's2.6.0-cu124), no candidate is assumed to be safe; if no candidate can be verified,recommended_upgrade: nullandverification: "no_verified_candidate"are returned.Pre-releases (
5.0.0-beta.3,15.6.0-canary.61, Go pseudo-versions, PyPIrcanddevversions; post-releases count as stable) are recommended only when no stable candidate fixes every vulnerability, and are markedrecommended_is_prerelease: true. A stable version in a higher tier is preferred over a pre-release in a lower tier.When a version is recommended,
verificationisverified, and each CVE'srecommended_statusisaffected/not_affected/unknown(not_evaluatedwhen the recommendation is withheld).per_cve_detail.fixed_inis the candidate for that CVE alone; seerecommended_statusfor the final recommendation.CVEs with no fixed version newer than the current one get
tier: "unfixed"and are left out of the recommendation (the data may be incomplete). They are still evaluated against the recommended version, and the result is shown. If every CVE is unfixed,recommended_upgradeisnullas well. A CVE whose fixed version is listed but cannot be parsed as a version (such as13.0, which is not SemVer) getstier: "unparseable_fix"; it is not treated as unfixed but stays in the set to be fixed, so the recommendation is withheld (no_verified_candidate).Rust uses SemVer (
SEMVERranges) and Cargo caret compatibility: 0.x minor updates and 0.0.x patch updates arecross_major.unmaintainedandnoticeadvisories, andunsoundadvisories without a newer fixed version, remain inper_cve_detailwithtier: "informational", their kind, andrecommended_status, but are not fix targets or counted as unfixed. Anunsoundadvisory with a newer fixed version is a version-specific defect, so it is a fix target like a vulnerability (its tier is computed normally and it keepsinformational: "unsound"). Packages with no fix target left returnverification: "informational_only"without an upgrade. Unmaintained crates may need replacement; useexplain_vulnerabilityfor notices.Suggestions include an
update_hint, tailored to whether the package is a direct or transitive dependency:Maven (from
pom.xml): for a direct dependency, update the<dependency>version (or the parent POM, property, or BOM that manages it); for a transitive dependency, override the version in<dependencyManagement>or update the direct dependency that requires itnpm: for a direct dependency, update the version in the
package.jsonlisted indeclared_in; for a transitive dependency, update the direct dependency named inintroduced_by, or set the version withoverridesin the rootpackage.json(effective only in the root project)Go:
go get <module>@<version>(also for// indirectmodules). For a module affected byreplace, update the version in thereplacedirective instead ofrequire. Major versions 2 and later use a different module path (/v2and so on) and are separate packages in OSV, so fixes in a newer major line are not included as candidates. If the current version is a pseudo-version (an untagged commit),upgrade_notesays soPyPI: update the version in
requirements.txt,pyproject.toml, orPipfileand regenerate the lockfile; for a transitive dependency, use a pip constraints file (-c) or the override settings of uv or PoetryRust: where version requirements permit it,
cargo update -p <crate>@<current> --precise <recommended>updates the lockfile. Otherwise change the direct requirement in the declaring Cargo.toml or update the parent dependency inintroduced_by. Cross-major upgrades may break compatibility, including 0.x updates. Pre-releases may require explicit=<version>requirements and must be allowed by transitive parents. Build metadata is omitted from--precise.[patch.crates-io]changes a source, not the allowed version range; backports must satisfy parent requirements. Check yanked versions, MSRV, and compiler compatibility. Cargo is never executed by the serverWhen the relation is
unknownormixed, both cases are described (no hint is given for Maven packages fromgradle.lockfile)
Checking recommended versions against OSV: Recommendations are verified only against the vulnerabilities found in the scan (those affecting the current version), so a recommended version could be affected by newer vulnerabilities (for example, cryptography 3.2's candidate 49.0.0 is affected by two vulnerabilities introduced in 44.0.0 and fixed in 50.0.0). The server therefore queries
api.osv.devabout the recommended version, and if it is affected, adds those vulnerabilities to the set to be fixed and chooses again with their fixed versions as candidates (50.0.0 in this example, with the reason inupgrade_note). The result is reported incandidate_check:clean: OSV reports no additional fix-target vulnerabilities after excluding knownunfixedadvisories, known informational advisories that are not fix targets, and new Rustunmaintained/noticerecords. These advisories may still apply; checkper_cve_detail[].recommended_status. Newly discoveredunsoundrecords are added as fix targetshas_known_vulnerabilities: no candidate avoids them, so the recommended version has known vulnerabilities (IDs inrecommended_known_vulnerabilities)conflict: OSV reports that a candidate is affected by a vulnerability the scan had evaluated as not affecting it (the range data disagree), and no other candidate is available, so the recommendation is withheld (recommended_upgrade: null,verification: "no_verified_candidate")failed: the query failed or returned a malformed response (the recommendation, verified against the scanned vulnerabilities, is still returned)skipped: the query limit was reached (4 queries per package, 60 per call)disabled: disabled withOSV_MCP_NO_CANDIDATE_CHECK=1
Only packages with a recommendation are queried, and only the package name (already sent during the scan) and the candidate version are sent. A candidate on which OSV and the local range data disagree is never recommended. A malformed response (a response or record that is not an object, or a page token that is not a string) is treated as a failure, not as "no vulnerabilities". Vulnerabilities found this way do not affect the current version, so they are not added to
per_cve_detail.Each suggestion carries the same
dependency_relationasscan_project(andintroduced_by/declared_in/replaced_in_go_mod).requirements.txtlines with>=X/~=Xare scanned by OSV-Scanner at the lower bound X. Their suggestions are markedversion_is_lower_bound: true, andupgrade_noteexplains that the recommendation means raising the lower bound to at least the recommended version (the installed version may differ).Ecosystems without recommendation support (such as RubyGems from an SBOM) return
verification: "unsupported_ecosystem", and current versions that cannot be parsed (such as npm git or local path dependencies) returnverification: "unparseable_version". In both cases each CVE getstier: "unsupported"and is not counted as unfixed (whether a fixed version exists is not evaluated; checkfixed_versionsinscan_projectorexplain_vulnerability).coverageis the same as inscan_project. If a manifest has no lockfile or a file was excluded,completeisfalse, and those dependencies are not included in the suggestions. Theskipped_manifests/scope_warningfields of v0.4.2 and earlier were merged intocoverage.skipped_files/coverage.warning.
explain_vulnerability
Returns the details of a vulnerability, given its GHSA or CVE ID, fetched directly from the OSV database API (api.osv.dev) (no scan is run). IDs from the scan results can be passed as they are. This is especially useful for vulnerabilities published after the client LLM's knowledge cutoff.
Input
Parameter | Type | Description |
| string | The vulnerability ID (for example |
Output (success): id / aliases / summary / details (markdown description, up to 4,000 characters) / severity (CVSS vectors) / published / modified / affected (affected packages and version ranges) / references (URLs of advisories, fix commits, and so on; http/https only, up to 20)
Note: OSV's canonical IDs are GHSA and similar IDs, so a CVE ID may not be found (the error message then suggests querying with the GHSA ID).
Output (error) — common to all tools
Returns JSON with a machine-readable kind, together with isError: true:
{
"error": {
"kind": "no_manifest_found",
"message": "…"
}
}kind | Meaning |
| OSV-Scanner was not found (the message includes installation instructions) |
| The path does not exist, or is not a directory or a supported file |
| Node's permission model ( |
| No supported lockfile or manifest was found (for |
| The manifest search reached its limits (200,000 entries or 1,000 manifests). Specify a narrower directory or the manifest itself |
| No JAR/WAR archive was found ( |
| The JAR/WAR search reached its limits (depth 8, 100 archives, 10,000 entries). Narrow the target |
| The total size of the files to scan (the copies in the temporary directory) exceeds the limit (2 GiB). Narrow the target |
| The SBOM file does not exist, is not a regular file, or cannot be read |
| The SBOM is not valid JSON or lacks the required structure |
| The SBOM format or version is not supported |
| The SBOM exceeds the size limit (16 MiB) |
| The binary download failed (including unsupported platforms) |
| The downloaded binary's checksum does not match (possible tampering or corruption) |
| A Gradle project without |
| The path is outside |
| No packages to scan (for example a |
| OSV-Scanner exited abnormally (an excerpt of stderr is in |
| Timeout (120 seconds by default) |
| The concurrent scan limit (2 by default) was reached. Try again after the running scans finish |
| The output exceeds the size limit (32 MB by default) |
| The output could not be parsed as JSON |
| The vulnerability ID is malformed |
| No vulnerability with the ID exists in the OSV database |
| The request to the OSV API failed (network, timeout, or a non-2xx response) |
| An unexpected error (no internal details are returned) |
Security design
The following measures keep the vulnerability scanner itself from becoming an attack vector.
Supply chain: The automatic download is limited to the official GitHub Releases and a pinned version, and is verified against a SHA256 checksum embedded in the package (the SHA256SUMS file from the release is not trusted, so tampering on the release side is detected). The binary is not made executable until it passes verification, and cached binaries are verified again on every use.
OSV_MCP_PREFER_DOWNLOAD=1avoids unverified binaries onPATHCommand injection: Commands are run with
spawnand an argument array, never through a shell. OSV-Scanner receives only a fixed list of arguments, plus verified absolute pathsSnapshots (no gap between checking and reading): OSV-Scanner never receives the original files. Lockfiles,
pom.xml(including the parent POM chain), and JAR/WARs are read safely once, copied into a private temporary directory (accessible only by the owner, deleted afterwards), and the copies are both checked and scanned. Replacing the original files or directories after the check has no effect on the results. Parent POMs are copied into the temporary directory at their original relative positions, so OSV-Scanner can follow relative paths but only finds the verified copies (references that climb out of the temporary directory with repeated..are excluded). Reads refuse a symbolic link at the final path component and anything that is not a regular file (so named pipes cannot stall the server), and afterwards the path is resolved again to confirm that it is still inside the boundary and is the same file that was opened. The copies are limited to 2 GiB in total (scan_input_too_large). When the server is terminated (SIGTERM/SIGINT/SIGHUP, or stdin closing), the temporary directories are deleted and running OSV-Scanner processes are stopped. Directories left behind by an uncatchable termination such as SIGKILL are deleted at a later startup (only directories whose names exactly match this server's prefixes, owned by the current user, not symbolic links, and unmodified for 24 hours or more)Path traversal: Input paths are resolved with
realpath(following symbolic links) before the boundary check. Manifest searches do not follow symbolic links. OSV-Scanner never receives a directory, only the detected manifests, each with its format given explicitly (given a directory, OSV-Scanner also readsrequirements.txtfiles there and follows their-r ../x.txtincludes outside the scan scope).requirements.txtfiles are not passed as they are: only the dependency lines the server could interpret are normalized and written to a private copy without any include directives, so even if OSV-Scanner's interpretation of includes differs from the server's, files outside the scope are never read.Cargo.lockfiles are likewise replaced by a copy the server writes, listing only crates.io packages Apom.xmlwhose parent POM chain references a file outsideOSV_MCP_ALLOWED_ROOTis excluded (Parent POMs)Denial of service: Timeouts and limits on stdout and on the stderr excerpt. Scanner output is parsed defensively and malformed output does not throw. The number of concurrent scans is limited (2 by default) so that parallel requests cannot start unlimited processes. The server's own analysis reads each file once and reuses the result (lockfiles for npm workspace membership,
Cargo.lockfiles, sharedrequirements.txtincludes, parent POMs), with a limit on the total amount read. Responses do not include the internal affected-range data, which keeps large scans to a fraction of their former sizeFail-closed mode:
OSV_MCP_REQUIRE_ALLOWED_ROOT=1refuses to start the server when no allowed root is setRestricted permissions (optional): The server works under Node's permission model, reports denied access as
permission_denied, and warns about missing permissions at startup (Running with restricted permissions)Fixed and documented network destinations: Dependency resolution is set to
deps.devexplicitly, and the mode that connects to arbitrary repositories listed in the scannedpom.xml(--data-source native) is never used (guaranteed by tests). See Network destinations and privacy for the destinations and forOSV_MCP_NO_REMOTE_RESOLUTION=1, which stops sending data to deps.devInformation leakage: Unexpected exceptions are reduced to
internal_errorwithout stack traces. Text from external sources (vulnerability summaries and so on) is returned as length-limited, structured dataPrompt injection: Text from the OSV database (summary / details / IDs and so on) and OSV-Scanner's stderr are sanitized before being returned to the LLM client. Control characters (including ANSI escapes), zero-width characters, bidirectional control characters (such as RLO), Unicode tag characters (invisible text smuggling), and line separators (U+2028/2029) are removed, and NFC normalization is applied. All reads of external data go through a single sanitizing boundary, so nothing is missed
Development
npm test # run the tests (vitest)
npx vitest run --coverage # measure coverage
npm run typecheck # type check
npm run build # build into dist/Design notes and open items are in docs/DESIGN_TODO.md (in Japanese). Release notes are in docs/releases.
Roadmap
suggest_fix: recommends the fix closest to the current release line (three-tier fallback)explain_vulnerability: vulnerability details (through the OSV API)npm package (
npx osv-scanner-mcp)Automatic OSV-Scanner download (with checksum verification)
Gradle support (lockfile approach)
scan_java_artifact: JAR/WAR scanning (for projects without a lockfile, or with only shaded/fat JARs)scan_project: scans Java / JavaScript / Python / Go lockfiles togethersuggest_fixfor JavaScript / Go (SemVer)suggest_fixfor Python (PEP 440)Direct and transitive dependencies (npm, Go,
requirements.txt,pom.xml)Checking recommended versions against the OSV database
Flagging inferred coordinates in JAR/WAR scans
Running under Node's permission model
English tool descriptions and messages
scan_projectfor Rust (Cargo.lock, crates.io crates only) and RustSec informational advisoriessuggest_fixand direct/transitive dependencies for RustDirect and transitive dependencies for
gradle.lockfileand the YAML/TOML lockfiles
License
Available Tools
6 toolsexplain_vulnerability脆弱性の詳細説明の取得A
指定したGHSA-IDまたはCVE-IDの脆弱性の詳細をOSVデータベース(api.osv.dev)から取得して返す。説明(details)・CVSSベクトル・影響を受けるパッケージとバージョン範囲・参照リンク(アドバイザリや修正コミット)が含まれる。scan_java_projectやsuggest_fixの結果に含まれるIDをそのまま渡せる。スキャンは実行しない。
| Name | Required | Description | Default |
|---|---|---|---|
| vulnerability_id | Yes | 脆弱性のID(例: GHSA-jfh8-c2jp-5v3q、CVE-2021-44228) |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the full burden of behavioral disclosure. It states that the tool performs an external API lookup, returns specific data fields, and does not execute scanning, which are the key behavioral traits an agent needs. It does not mention potential network failures or rate limits, but for a read-only lookup tool, the disclosed behavior is sufficient.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is three concise sentences in Japanese, starting with the core action, then the returned fields, then usage context and a clear non-scan caveat. Every sentence earns its place with no fluff or duplication of schema details.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the simple one-parameter interface, the fully self-describing input schema, and the absence of an output schema, the description is complete enough. It explains the data source, the input, the output fields, the relationship to sibling tools, and the key behavioral constraint (no scanning), so an agent can invoke it correctly without additional information.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The schema already documents vulnerability_id with examples and length constraints (100% coverage). The description adds valuable semantic context by stating that IDs from scan_java_project or suggest_fix can be passed directly, which helps the agent understand the expected value and provenance of the parameter.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a specific action ('取得して返す' fetch and return), a specific resource (OSV database at api.osv.dev), and the input type (GHSA-ID or CVE-ID). It explicitly lists the returned contents (details, CVSS, affected packages, references), which clearly distinguishes this from the sibling scan_java_project and suggest_fix tools.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description clearly indicates this tool is for retrieving vulnerability details from IDs obtained via scan_java_project or suggest_fix, and explicitly states that scanning is not performed. This gives an agent clear contextual guidance on when to invoke it, though it does not explicitly state 'when not to use' for all alternatives.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
scan_java_artifactJAR/WAR実体の脆弱性スキャンA
JAR/WARファイルまたはディレクトリ内の実体をスキャンする。ビルドやJavaコードの実行は行わない。メタデータによるベストエフォート同定のため、coverageと同定不能ファイルを必ず確認すること。completenessは常にincomplete。検出0件でも安全性や全依存の同定を保証しない。
| Name | Required | Description | Default |
|---|---|---|---|
| artifact_path | Yes | JAR/WARファイル、または探索するディレクトリの絶対パス |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden. It transparently discloses that no build or code execution occurs, that identification is best-effort based on metadata, that completeness is always incomplete, and that zero findings do not guarantee safety. This is strong behavioral transparency, though it does not describe return format or pagination.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three concise sentences, each adding value: the action, what it avoids, and critical result-interpretation caveats. The most important information is front-loaded, with no fluff or repetition.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the single parameter and lack of output schema, the description explains key limitations (coverage, incompleteness, no guarantee) that an agent needs to correctly interpret results. It does not detail the output structure, but the caveats about coverage and unidentifiable files imply the output contains these, which is sufficient for safe usage.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, and the description does not add details beyond what the schema already states for artifact_path. It aligns with the schema's description of the parameter, but no extra semantics are provided.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool scans entities inside JAR/WAR files or directories, explicitly distinguishing it from source-level scanning (scan_java_project) and noting it does not execute builds or Java code. The verb+resource combination is specific and unambiguous.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Usage is implied by the resource type (JAR/WAR vs. source code), but there is no explicit mention of when to use this over scan_sbom or scan_java_project. The description does provide guidance on interpreting results (check coverage, unidentifiable files) but lacks explicit alternative routing.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
scan_java_projectJavaプロジェクトの脆弱性スキャンA
Java(Maven)プロジェクトをGoogle OSV-Scannerでスキャンし、依存ライブラリの既知の脆弱性(CVE/GHSA)を深刻度順のJSONレポートで返す。レポートにはパッケージごとの脆弱性一覧(CVSSスコア・5段階深刻度・修正版バージョン)とサマリ集計が含まれる。Maven(pom.xml)とGradle(gradle.lockfile)に対応。dependency_resolution.warningがある場合は推移的依存がスキャン対象外のため、検出0件でも安全とは判断しないこと。
| Name | Required | Description | Default |
|---|---|---|---|
| project_path | Yes | スキャン対象のプロジェクトディレクトリ、またはpom.xml/gradle.lockfileの絶対パス |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden and does well: it discloses the scan engine, the report shape (per-package vulnerability list with CVSS score, 5-level severity, fixed version, plus summary aggregates), and a critical non-obvious caveat that a transitivity warning invalidates a zero-finding result. It omits operational traits such as network dependency on the OSV API, credentials, or runtime expectations, which keeps it short of a 5.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Four sentences, each load-bearing: purpose plus report format, report contents, supported build systems, and the safety caveat. The purpose is front-loaded and there is no filler or repetition.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
There is no output schema, so the description must describe returns and it does so concretely (severity-ordered JSON, CVSS, severity level, fixed versions, summary aggregation). Combined with the transitive-dependency caveat, an agent has enough to call and interpret the tool, though environment prerequisites (network access, scan duration) are not addressed.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100% and there is only one parameter (project_path), whose meaning is fully documented in the schema itself as a project directory or absolute path to pom.xml/gradle.lockfile. The description adds no syntax, path-form, or default detail beyond the schema, so the baseline of 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description names a specific verb and resource (scanning a Java/Maven project with Google OSV-Scanner) and states exactly what is returned (known CVEs/GHSA in severity-ordered JSON). It narrows scope with supported build systems (pom.xml, gradle.lockfile), which implicitly separates it from scan_sbom and scan_java_artifact. However, it never names or contrasts a sibling explicitly, so an agent must infer the boundary against scan_project and scan_java_artifact.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
There is no explicit when-to-use/when-not-to-use statement or named alternative. Usage is only implied by the supported inputs (Maven pom.xml, Gradle gradle.lockfile), and the closing caveat tells the agent not to conclude safety when dependency_resolution.warning is present. That is useful operational guidance but not sibling routing.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
scan_projectプロジェクトの依存の脆弱性スキャン(Java / JavaScript / Python / Go)A
プロジェクト内のlockfile・マニフェストを検出し、依存ライブラリの既知の脆弱性(CVE/GHSA)をまとめてスキャンする。対応: Java(pom.xml / gradle.lockfile)、JavaScript(package-lock.json / npm-shrinkwrap.json / yarn.lock / pnpm-lock.yaml / bun.lock)、Python(poetry.lock / uv.lock / Pipfile.lock / pdm.lock / requirements.txt)、Go(go.mod)。パッケージマネージャーやビルドは実行しない。応答先頭のcoverageを必ず確認すること: lockfileが無いマニフェスト、バージョン未固定のrequirements行、スキャン対象から外したファイルを示す。各パッケージのdependency_relationは直接依存(direct)か推移的依存(transitive)か(package-lock.json・go.mod・requirements.txtで判定。それ以外はunknown)。coverage.complete=falseの場合は、検出0件でも安全とは判断しないこと。修正版の推奨(suggest_fix)はJava・JavaScript・Python・Goに対応。
| Name | Required | Description | Default |
|---|---|---|---|
| project_path | Yes | スキャン対象のプロジェクトディレクトリ、または対応するlockfile・マニフェストの絶対パス |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full behavioral burden and does so well: it states that package managers/builds are NOT executed, explains the coverage block (manifests without lockfiles, unpinned requirement lines, excluded files), and defines dependency_relation as direct/transitive/unknown with which files yield which. It omits any note on permissions or runtime cost, keeping it from a 5.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with the core purpose, then proceeds through supported files, behavioral caveats, and the coverage warning in a logical order. It is dense and somewhat long, but each clause adds operational detail rather than filler.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With no output schema, the description must explain the return shape, and it does specify the leading coverage object, its false case, and per-package dependency_relation. The picture is largely complete, though the exact findings format and suggest_fix linkage to the separate sibling are left implicit.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100% for the single project_path parameter, and the description adds no new semantics beyond what the schema already states (directory or absolute lockfile/manifest path). Baseline 3 is appropriate when the schema does the heavy lifting.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb+resource (detect lockfiles/manifests and scan dependencies for known CVE/GHSA vulnerabilities) and enumerates the supported ecosystems and exact filenames, which is unusually precise. However, it never distinguishes itself from overlapping siblings like scan_sbom and scan_java_project, which an agent must choose between, so it falls short of a 5.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It implicitly defines when to use it by listing the ecosystems and file types it handles, and it gives a critical caution about coverage.complete=false. But it offers no explicit routing versus scan_sbom, scan_java_project, or scan_java_artifact, so the agent cannot tell when this tool is preferred over those.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
scan_sbomSBOMの脆弱性スキャンA
CycloneDX 1.4/1.5/1.6またはSPDX 2.2/2.3のJSON SBOMから識別できる依存をスキャンする。ビルドやJARの実行は行わない。入力は16MiB以下のローカルファイル。SBOMの網羅性・鮮度・実成果物との一致は未検証であり、検出0件でも安全性を保証しない。
| Name | Required | Description | Default |
|---|---|---|---|
| sbom_path | Yes | CycloneDX/SPDX JSON SBOMファイルの絶対パス |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the full burden and does an excellent job: it discloses that only SBOM-identifiable dependencies are scanned, that builds/JARs are not executed, that SBOM comprehensiveness/freshness/artifact-match are unverified, and that zero detections do not guarantee safety. These limitations are highly useful for an agent deciding how to interpret results.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is compact, front-loaded with the core scanning behavior and supported formats, and every sentence adds operational value. It states scope, exclusions, input constraints, and key limitations without redundancy or padding.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a single-parameter tool with no annotations and no output schema, the description is quite complete: input format, size, local-file requirement, non-execution behavior, and result caveats are all covered. The main gap is that it does not describe the return value or output structure of the scan, which would help an agent know what to expect after calling it.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The input schema already defines sbom_path as an absolute path to a CycloneDX/SPDX JSON SBOM file, with 100% coverage. The description adds meaningful constraints beyond the schema: supported format versions, the 16MiB size limit, and that the input must be a local file, which helps the agent validate the parameter before calling the tool.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description uses a specific verb ('スキャンする') and resource ('CycloneDX 1.4/1.5/1.6またはSPDX 2.2/2.3のJSON SBOM'), and states what is scanned: dependencies identifiable from the SBOM. It is clearly distinguished from siblings like scan_java_project and scan_java_artifact by focusing on an SBOM file input rather than a project or artifact.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description establishes clear usage context: this tool is for scanning a local JSON SBOM file, with a 16MiB size limit, and explicitly says it does not build or execute JARs. It does not explicitly name sibling alternatives or exclusion conditions, but the input constraints and non-execution note make the intended scenario reasonably clear.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
suggest_fix脆弱性を解消する推奨アップグレードの提案A
scan_projectと同じ検出でプロジェクトをスキャンし、脆弱な依存パッケージごとに推奨アップグレードバージョンを提案する。推奨はJava(Maven / Gradle)・JavaScript(npm)・Python(PyPI)・Goに対応。現在のバージョンに最も近いリリース系統の修正版を優先する3段階フォールバック(same_minor: 同一系統内 → major_internal: 同一メジャー内 → cross_major: メジャーアップグレード)で選定し、推奨バージョン・アップグレード距離(upgrade_tier)・CVEごとの修正版を返す。npm・Go・PyPIでは0.x系のマイナー更新もcross_major(破壊的変更の可能性)。requirements.txtの下限(>=)でスキャンした依存はversion_is_lower_boundを付け、推奨は下限の引き上げを意味する。直接/推移的依存の別(dependency_relation、npmはintroduced_by・declared_in)に応じて更新方法(update_hint)を示す。推奨先はapi.osv.devに照会し、現在の版には該当しない既知の脆弱性があれば避けて選び直す(結果はcandidate_check。has_known_vulnerabilities・failed・skippedの場合は推奨先の安全性が確認できていない。conflictは判定の食い違いで推奨を保留)。候補を全修正対象CVEの影響範囲と照合し、情報不足の場合は推奨を保留する。プレリリース版は正式版で解消できない場合だけ推奨し、recommended_is_prereleaseを付ける。現在より新しい修正版候補のないCVEはunfixedとして別表示し、推奨先での判定も返す。応答のcoverageを必ず確認すること(complete=falseなら提案に含まれない依存がある)。dependency_resolution.warningがある場合は推移的依存の脆弱性が提案に含まれない。
| Name | Required | Description | Default |
|---|---|---|---|
| project_path | Yes | スキャン対象のプロジェクトディレクトリ、または対応するlockfile・マニフェストの絶対パス |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations to carry the load, the description discloses substantial behavior: the three-tier fallback selection (same_minor/major_internal/cross_major), candidate_check safety re-query against api.osv.dev, handling of prerelase and unfixed CVEs, coverage and dependency_resolution.warning caveats. This is exactly the kind of behavioral context an agent needs for a complex remediation tool.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Purpose is front-loaded in the first sentence and nearly every subsequent sentence conveys distinct behavioral detail, so little is wasted. However it is delivered as one dense unbroken paragraph with no structuring, which makes the rich detail harder to scan than it needs to be.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a tool with no annotations and no output schema, the description covers the return semantics extensively (upgrade_tier, version_is_lower_bound, dependency_relation/update_hint, recommended_is_prerelease, candidate_check, unfixed, coverage). An agent has everything needed to interpret results and handle caveats correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100% and there is a single parameter, so the schema already documents project_path fully; the description adds no extra meaning about the parameter. Baseline of 3 applies when the schema does the heavy lifting.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource: it scans the project with the same detection as scan_project and proposes a recommended upgrade version per vulnerable dependency. It explicitly names the sibling scan_project and clarifies how it differs (same scan, plus fix recommendations), so an agent can distinguish it from the other scan_* tools.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Usage is implied through the reference to scan_project (same detection, but adds remediation) and post-call guidance to verify coverage, but there is no explicit when/when-not statement or alternative routing (e.g. when to prefer scan_project or explain_vulnerability). It is enough to infer intent but leaves the choice among siblings to the agent.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
2 tool updates
v0.7.1- Added
scan_project - Changed
suggest_fix1 field changed- changed
Input schema / properties / project_path / descriptionPrevious value: -"スキャン対象のプロジェクトディレクトリ、またはpom.xml/gradle.lockfileの絶対パス"New value: +"スキャン対象のプロジェクトディレクトリ、または対応するlockfile・マニフェストの絶対パス"
2 tool updates
v0.3.0- Added
scan_java_artifact - Added
scan_sbom
3 tool updates
v0.1.11- Added
explain_vulnerability - Changed
scan_java_project1 field changed- changed
Input schema / properties / project_path / descriptionPrevious value: -"スキャン対象のプロジェクトディレクトリまたはpom.xmlの絶対パス"New value: +"スキャン対象のプロジェクトディレクトリ、またはpom.xml/gradle.lockfileの絶対パス"
- Added
suggest_fix
1 tool update
v0.1.0- First observed
scan_java_project
TDQS
Scored across 6 tools
scan_project, scan_java_project, and suggest_fix all perform project scanning, and scan_project already covers Java Maven/Gradle, making scan_java_project largely redundant. scan_java_artifact and scan_sbom are distinct by input type, but the boundary between generic and Java-specific project scans is fuzzy.
All names use snake_case and follow a clear verb_noun or verb_object pattern such as scan_*, explain_*, and suggest_*. Minor ordering variation in scan_java_project versus scan_project is readable and consistent overall.
Six tools is well-scoped for a vulnerability scanner and remediation server. Each tool covers a distinct input type or action without obvious bloat.
The core lifecycle is covered: scanning projects, Java artifacts, and SBOMs; explaining individual vulnerabilities; and suggesting fixes. Some gaps remain, such as no general non-Java artifact scanning or batch fix application, but agents can work around these.
Maintenance
Related MCP Connectors
Scan packages and lockfiles (npm, PyPI, Go, Maven, Cargo, NuGet) for vulnerabilities and malware.
Generate SBOMs, scan vulnerabilities, and analyze dependencies from local projects or Git repos.
Vulnerability management: scan projects, search sealed packages, manage sealing rules and reports.
Scan code for quantum-vulnerable cryptography and get NIST post-quantum migration guidance.
Related MCP Servers
- AlicenseNot gradedqualityNot gradedmaintenanceEnables security scanning of code projects to identify common vulnerabilities like XSS, injections, SSRF, and path traversal issues. Provides local, offline scanning with severity-grouped results and actionable fix suggestions for improving code security.38 npm-
- AlicenseBqualityDmaintenanceEnables searching Maven Central artifacts, retrieving versions, and analyzing dependencies via natural language.5MIT
- FlicenseNot gradedqualityCmaintenanceScans Python, Node.js, Java/Spring, and PHP dependency manifests for known vulnerabilities using OSV and GitHub Advisory APIs.-
- AlicenseNot gradedqualityDmaintenanceEnables scanning projects for dependency vulnerabilities, secrets, license conflicts, code quality, and git health, returning a 0-100 health score with actionable suggestions.7 npmMIT