Required Reading
~5 min

MCP Tools Reference

Every tool npmscan's MCP server exposes to AI agents — 15 in total — with its input schema and real request/response examples, so an agent (or the person configuring it) knows exactly what each one catches before relying on it.

First time connecting?
This page assumes your agent is already connected. For setup commands (Claude Code, Claude Desktop, ChatGPT, or any MCP-compatible client), see the MCP setup guide. Endpoint: https://npmscan.com/api/mcp.
Public & unauthenticated
No API key, no signup. Same read-only data that powers the website, exposed as structured tool calls instead of raw HTML.
Rate limited
Capped at 30 requests / 60s per IP, separate from the REST /api limit. Going over returns a rate-limit error telling you how many seconds to wait.
How to read every example below
  • Risk caughtthe call succeeds and surfaces the exact risk the tool exists to catch
  • Confirmed cleanthe call succeeds and comes back clean — confirmed safe, not just silent
  • Edge caseboundary conditions: validation errors, not-found, or an ambiguous result

Package & Version Lookups

Registry metadata enriched with the signals a name or a README will never tell you: download/dependent counts, popularity and maintenance tiers, typosquat detection, and a direct vulnerable/clean verdict.

search_packages

Search npm by name or keyword, ranked with download/dependent counts and deterministic popularity signals — not just text relevance.

Each result carries weeklyDownloads/monthlyDownloads, dependentsCount, topPackagesRank (position in npmscan's own top-100k-by-downloads snapshot), and deterministic (not model-generated) popularityTier/maintenanceTier labels. A result matching the query with a 'very-low' popularityTier, zero dependents, or a 'stale' maintenanceTier is very likely abandoned or copy-paste, not a real contender — regardless of how relevant its name looks. possibleTyposquatOf is set when an obscure result's name is one typo away from a top-5,000 package (e.g. "raect" vs "react"); surface that explicitly rather than silently dropping the result.

Input
NameTypeRequiredDescription
querystringrequiredSearch text, e.g. a package name or keywords (2–64 characters).
limitnumberoptionalMax results to return. Default 20, max 50.
Confirmed cleanA well-established package — no red flags

Real name, high download counts, no typosquat match. This is what a normal, trustworthy result looks like.

Call
search_packages({ "query": "react-router", "limit": 3 })
Result (truncated)success
{
  "query": "react-router",
  "total": 41,
  "results": [
    {
      "name": "react-router",
      "version": "6.23.1",
      "description": "Declarative routing for React",
      "publisher": "mjackson",
      "weeklyDownloads": 11482031,
      "monthlyDownloads": 48920104,
      "dependentsCount": 8213,
      "topPackagesRank": 87,
      "popularityTier": "very-high",
      "maintenanceTier": "active",
      "possibleTyposquatOf": null,
      "npmscanUrl": "https://npmscan.com/package/react-router"
    }
    /* ...2 more results */
  ]
}
Risk caughtTyposquat pattern caught in the results

A one-letter-off name ("raect" vs. "react") with near-zero adoption ranks near the top of a naive text search — possibleTyposquatOf is the signal that separates it from a real contender.

Call
search_packages({ "query": "raect" })
Result (truncated)success
{
  "query": "raect",
  "total": 1,
  "results": [
    {
      "name": "raect",
      "version": "0.0.1",
      "weeklyDownloads": 4,
      "dependentsCount": 0,
      "topPackagesRank": null,
      "popularityTier": "very-low",
      "maintenanceTier": "stale",
      "possibleTyposquatOf": { "name": "react", "rank": 3 },
      "npmscanUrl": "https://npmscan.com/package/raect"
    }
  ]
}
Edge caseQuery too short — rejected before hitting the registry

The 2-character minimum is enforced locally; a 1-character query never reaches npm.

Call
search_packages({ "query": "a" })
Errortool error
{ "error": "\"query\" must be at least 2 characters" }
get_package

Full registry metadata for a package — install scripts, license, maintainers, downloads, GitHub stars, TS support, and a maintenance/popularity read on it.

Fetches latest version, install scripts (preinstall/postinstall are a key risk signal), maintainers, license, recent version history, weekly downloads, GitHub stars, TypeScript support, days since last publish, topPackagesRank, and a downloadTrend (growing/stable/declining vs. ~3 months ago). Also checks the LATEST version against OSV.dev — isLatestVersionVulnerable/highestSeverity are a direct answer, and popularityTier/maintenanceTier plus a plain-language maintenanceSummary tell you whether a gap since the last release means "stable and finished" or "abandoned." Read `deprecated` before recommending anything — it is a maintainer-set signal, not an inference.

Input
NameTypeRequiredDescription
namestringrequiredExact npm package name, e.g. "lodash" or "@scope/name".
Confirmed cleanActively maintained, no signals worth flagging
Call
get_package({ "name": "axios" })
Result (truncated)success
{
  "name": "axios",
  "latestVersion": "1.7.4",
  "latestVersionInfo": {
    "version": "1.7.4",
    "scripts": { "postinstall": "" },
    "deprecated": null
  },
  "weeklyDownloads": 58210442,
  "githubStars": 105300,
  "hasBuiltInTypes": true,
  "daysSinceLastPublish": 12,
  "popularityTier": "very-high",
  "maintenanceTier": "active",
  "maintenanceSummary": "Actively maintained: published 12 days ago, 58.2M weekly downloads, 105.3k GitHub stars.",
  "downloadTrend": { "direction": "stable", "changePercent": 1.4 },
  "possibleTyposquatOf": null,
  "isLatestVersionVulnerable": false,
  "highestSeverity": null,
  "vulnerabilities": []
}
Risk caughtMaintainer-declared deprecation surfaces immediately

`deprecated` comes straight from the npm registry, set by the package's own maintainers — request has carried this warning since 2020, pointing users at maintained HTTP clients instead.

request's own deprecation announcement on GitHub
Call
get_package({ "name": "request" })
Result (truncated)success
{
  "name": "request",
  "latestVersion": "2.88.2",
  "latestVersionInfo": {
    "version": "2.88.2",
    "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142"
  },
  "weeklyDownloads": 9800213,
  "daysSinceLastPublish": 1730,
  "popularityTier": "very-high",
  "maintenanceTier": "stale",
  "maintenanceSummary": "Deprecated by its maintainers; no publish in 1730 days despite still-high download counts from legacy installs.",
  "isLatestVersionVulnerable": false,
  "vulnerabilities": []
}
Edge caseUnknown package name
Call
get_package({ "name": "this-package-does-not-exist-anywhere" })
Errortool error
{ "error": "Package \"this-package-does-not-exist-anywhere\" not found" }
get_package_version

Metadata plus an OSV.dev check for one exact version — the right call when you need to know if a version pinned in a lockfile is safe.

Fetches registry metadata for one exact version (dependencies, install scripts, tarball) and checks that exact version against OSV.dev. isVulnerable/highestSeverity are a direct answer, and each finding includes severity, a summary, and the fixedVersion to upgrade to. Use this instead of get_package whenever you already have an exact version — e.g. from a lockfile — rather than caring about the latest release.

Input
NameTypeRequiredDescription
namestringrequiredExact npm package name.
versionstringrequiredExact version string, e.g. "4.17.21".
Risk caughtA known, long-fixed CRITICAL vulnerability

minimist@1.2.5 carries CVE-2021-44906, a prototype-pollution bug fixed in 1.2.6 — a real, well-documented CVE pinned to an exact historical version, used as a regression guard in npmscan's own test suite.

GHSA-xvch-5gv4-984h — Prototype Pollution in minimist
Call
get_package_version({ "name": "minimist", "version": "1.2.5" })
Resultsuccess
{
  "name": "minimist",
  "version": "1.2.5",
  "isVulnerable": true,
  "highestSeverity": "CRITICAL",
  "vulnerabilities": [
    {
      "id": "GHSA-xvch-5gv4-984h",
      "summary": "Prototype Pollution in minimist",
      "severity": "CRITICAL",
      "aliases": ["CVE-2021-44906"],
      "fixedVersion": "1.2.6",
      "npmscanUrl": "https://npmscan.com/vulnerability/GHSA-xvch-5gv4-984h"
    }
  ]
}
Confirmed cleanThe fixed version itself comes back clean

Same package, the very next release — confirms the tool isn't just flagging the package by name.

Call
get_package_version({ "name": "minimist", "version": "1.2.6" })
Resultsuccess
{
  "name": "minimist",
  "version": "1.2.6",
  "isVulnerable": false,
  "highestSeverity": null,
  "vulnerabilities": []
}
Edge caseVersion does not exist
Call
get_package_version({ "name": "minimist", "version": "999.0.0" })
Errortool error
{ "error": "Version \"999.0.0\" of package \"minimist\" not found" }

Vulnerability Queries

Direct OSV.dev, GitHub Advisory, and NIST NVD lookups — single package, a whole dependency inventory in one call, or an exact CVE — each with a plain isVulnerable/found verdict instead of a raw advisory dump.

query_vulnerabilities

OSV.dev lookup for one package, optionally scoped to an exact version — the building block behind get_package_version.

Returns isVulnerable and highestSeverity as a direct answer, plus each finding's severity, summary, CVE aliases, and fixedVersion. Omitting `version` returns every vulnerability ever recorded for the package across all versions — including ones long since fixed — so always pass an exact version when the question is "is the version I have installed safe."

Input
NameTypeRequiredDescription
namestringrequirednpm package name.
versionstringoptionalOptional exact version to narrow results, e.g. one pinned in a lockfile.
ecosystemstringoptionalOSV ecosystem. Default "npm".
Risk caughtVersion-scoped: the exact vulnerable version
GHSA-xvch-5gv4-984h — Prototype Pollution in minimist
Call
query_vulnerabilities({ "name": "minimist", "version": "1.2.5" })
Resultsuccess
{
  "package": "minimist",
  "version": "1.2.5",
  "isVulnerable": true,
  "highestSeverity": "CRITICAL",
  "vulnerabilities": [
    { "id": "GHSA-xvch-5gv4-984h", "severity": "CRITICAL", "aliases": ["CVE-2021-44906"], "fixedVersion": "1.2.6", "npmscanUrl": "https://npmscan.com/vulnerability/GHSA-xvch-5gv4-984h" }
  ]
}
Confirmed cleanVersion-scoped: the fixed version
Call
query_vulnerabilities({ "name": "minimist", "version": "1.2.6" })
Resultsuccess
{
  "package": "minimist",
  "version": "1.2.6",
  "isVulnerable": false,
  "highestSeverity": null,
  "vulnerabilities": []
}
Edge caseNo version given — every historical CVE comes back, fixed ones included

Omitting version here would wrongly read as "minimist is currently unsafe" if you don't also check which version you actually have.

Call
query_vulnerabilities({ "name": "minimist" })
Result (truncated)success
{
  "package": "minimist",
  "version": null,
  "isVulnerable": true,
  "highestSeverity": "CRITICAL",
  "vulnerabilities": [
    { "id": "GHSA-xvch-5gv4-984h", "aliases": ["CVE-2021-44906"], "fixedVersion": "1.2.6" },
    { "id": "GHSA-vh95-rmgr-6w4m", "aliases": ["CVE-2020-7598"], "fixedVersion": "1.2.3" }
  ]
}
batch_query_vulnerabilities

OSV.dev across an entire dependency inventory in one call — a flat package list, or raw package.json/lockfile/CycloneDX/SPDX content.

Chunk-queries OSV behind the scenes so a large SBOM doesn't stop at the upstream 100-package batch limit. Pass either `packages` (explicit list) or `content` (raw manifest/lockfile/SBOM text) — never both. Each finding includes severity, summary, CVE aliases, and fixed version, so a full-inventory audit answer doesn't need a follow-up call per flagged package.

Input
NameTypeRequiredDescription
packagesarrayoptionalExplicit {name, version?} list (1-1000 items). Use this OR content, not both.
contentstringoptionalRaw package.json / package-lock.json / yarn.lock / pnpm-lock.yaml / CycloneDX JSON / SPDX JSON content. Use this OR packages, not both.
includeDevDependenciesbooleanoptionalOnly applies when content is a manifest/lockfile format that distinguishes dev dependencies.
Risk caughtOne known-vulnerable dependency caught inside a normal batch
Call
batch_query_vulnerabilities({
  "packages": [
    { "name": "minimist", "version": "1.2.5" },
    { "name": "is-number", "version": "7.0.0" }
  ]
})
Resultsuccess
{
  "parsedPackageCount": 2,
  "results": [
    {
      "package": { "name": "minimist", "version": "1.2.5" },
      "vulnerabilityCount": 1,
      "vulnerabilities": [
        { "id": "GHSA-xvch-5gv4-984h", "severity": "CRITICAL", "aliases": ["CVE-2021-44906"], "fixedVersion": "1.2.6" }
      ]
    },
    { "package": { "name": "is-number", "version": "7.0.0" }, "vulnerabilityCount": 0, "vulnerabilities": [] }
  ],
  "totalVulnerabilities": 1,
  "packagesWithVulnerabilities": 1
}
Confirmed cleanWhole batch comes back clean
Call
batch_query_vulnerabilities({
  "packages": [
    { "name": "is-number", "version": "7.0.0" },
    { "name": "lodash", "version": "4.17.21" }
  ]
})
Resultsuccess
{
  "parsedPackageCount": 2,
  "results": [
    { "package": { "name": "is-number", "version": "7.0.0" }, "vulnerabilityCount": 0, "vulnerabilities": [] },
    { "package": { "name": "lodash", "version": "4.17.21" }, "vulnerabilityCount": 0, "vulnerabilities": [] }
  ],
  "totalVulnerabilities": 0,
  "packagesWithVulnerabilities": 0
}
Edge casepackages and content given together — rejected

The two input modes are mutually exclusive; this fails before any OSV call is made.

Call
batch_query_vulnerabilities({
  "packages": [{ "name": "lodash" }],
  "content": "{ \"dependencies\": { \"lodash\": \"4.17.21\" } }"
})
Errortool error
{ "error": "Provide either packages or content, not both" }
get_latest_advisories

Browse reviewed GitHub Security Advisories for the npm ecosystem, filterable by severity, category, affected package, or an exact GHSA/CVE lookup.

Filter by severity, vulnerability category (XSS, SQL/NoSQL Injection, SSRF, Access Control, Code Injection, and more), an affected package name, or look up one exact advisory by GHSA or CVE ID. Cursor-paginated — pass a previous response's nextCursor back in as cursor for the next page.

Input
NameTypeRequiredDescription
severitystringoptional"critical" | "high" | "medium" | "low" | "all" (default all).
categorystringoptionalVulnerability category id, e.g. "xss", "sql-injection", "ssrf", "access-control".
affectsstringoptionalFilter to advisories affecting this npm package name.
ghsaIdstringoptionalExact lookup by GHSA ID.
cveIdstringoptionalExact lookup by CVE ID.
directionstringoptional"asc" | "desc" by published date (default desc).
cursorstringoptionalOpaque pagination cursor from a previous response's nextCursor.
Risk caughtCategory filter actually filters — every result carries the label

The npmscan test suite asserts this directly: every advisory returned for category="xss" must carry the XSS category label, not just be loosely related.

Call
get_latest_advisories({ "category": "xss", "severity": "high" })
Result (truncated)success
{
  "severity": "high",
  "category": "xss",
  "direction": "desc",
  "nextCursor": "MjAyNS0..._eyJpZCI6MTIzfQ",
  "advisories": [
    {
      "id": "GHSA-xxxx-xxxx-xxxx",
      "cve": "CVE-2025-xxxxx",
      "summary": "Cross-site scripting (XSS) in a Markdown-to-HTML renderer via unsanitized image alt text",
      "severity": "high",
      "categories": ["xss"],
      "packages": [{ "name": "example-markdown-renderer", "affectedRange": "< 3.2.1", "patchedVersion": "3.2.1" }]
    }
    /* ...29 more, per_page is 30 */
  ]
}
Confirmed cleanExact GHSA lookup with no matches to sort through
Call
get_latest_advisories({ "ghsaId": "GHSA-xvch-5gv4-984h" })
Resultsuccess
{
  "advisories": [
    { "id": "GHSA-xvch-5gv4-984h", "cve": "CVE-2021-44906", "summary": "Prototype Pollution in minimist", "severity": "critical" }
  ]
}
Edge casedirection sorts oldest-first vs. newest-first

A neutral, non-error case — both directions succeed, this is a sort-order sanity check, not a risk signal.

Call
get_latest_advisories({ "direction": "asc" })
Result (truncated)success
{ "direction": "asc", "advisories": [ /* oldest reviewed advisory first */ ] }
get_cve

Authoritative NVD data for one exact CVE ID, or a keyword/severity/CWE/date-range search — enriched with CISA KEV and FIRST.org EPSS.

kev is non-null only for a confirmed, actively-exploited-in-the-wild CVE — treat that as an urgent-patch signal regardless of CVSS score. epss is the probability of exploitation in the next 30 days, a better prioritization signal than severity alone. If NVD has no record yet, a single-CVE lookup falls back to the raw MITRE record automatically (source: "mitre"). NVD is NOT npm-scoped — pass keywordSearch to narrow a search to a specific package/product.

Input
NameTypeRequiredDescription
cveIdstringoptionalExact CVE ID for a single lookup, e.g. "CVE-2021-44228". Omit search filters when this is given.
keywordSearchstringoptionalFree-text search, e.g. a package or product name.
severitystringoptional"CRITICAL" | "HIGH" | "MEDIUM" | "LOW" — CVSS v3 base severity filter.
cweIdstringoptionalFilter by weakness type, e.g. "CWE-79".
publishedSince / publishedUntilstringoptionalYYYY-MM-DD date range, must be given together, capped at 120 days.
resultsPerPagenumberoptionalMax results for a search (default 10, max 50).
startIndexnumberoptionalPagination offset for a search.
Risk caughtA real, actively-exploited CVE with KEV + EPSS enrichment

CVE-2021-44228 (Log4Shell) has been on the CISA KEV list since it was created — the report's own remediation-ranking tests use it as a regression guard because it is a permanent, unambiguous "patch now" fact.

CVE-2021-44228 on the NIST NVD
Call
get_cve({ "cveId": "CVE-2021-44228" })
Result (truncated)success
{
  "id": "CVE-2021-44228",
  "vulnStatus": "Analyzed",
  "cvss": { "version": "3.1", "baseScore": 10, "baseSeverity": "CRITICAL", "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H" },
  "cwes": ["Improper Neutralization of Special Elements used in an Expression Language Statement"],
  "source": "nvd",
  "kev": {
    "dateAdded": "2021-12-10",
    "dueDate": "2021-12-24",
    "requiredAction": "Apply updates per vendor instructions."
  },
  "epss": { "score": 0.94421, "percentile": 0.99976, "date": "2026-08-30" }
}
Confirmed cleanUnknown CVE ID — a normal not-found, not a tool error
Call
get_cve({ "cveId": "CVE-2099-99999" })
Resultsuccess
{ "cveId": "CVE-2099-99999", "found": false, "npmscanUrl": "https://npmscan.com/vulnerability/CVE-2099-99999" }
Edge caseNo filters at all — rejected before it ever reaches NVD

Also rejected: malformed CVE IDs, and a publishedSince given without publishedUntil (or vice versa).

Call
get_cve({})
Errortool error
{ "error": "Provide cveId for an exact lookup, or at least one of keywordSearch/severity/cweId/publishedSince+publishedUntil to search" }

Supply-Chain Risk Signals

Signals no advisory database carries: what an install script actually does, whether a publish's provenance matches its claimed source, whether maintainer control quietly changed hands, and whether a risk buried three levels deep in the dependency graph is reachable at all.

analyze_install_script

Statically scans preinstall/install/postinstall/prepare scripts — and the files they reference, pulled from the tarball itself — against npmscan's red-flags rubric.

Checks for child_process use, network calls, access to sensitive paths/env (.ssh, .aws, .npmrc, *TOKEN/*KEY), obfuscation, remote binaries off trusted CDNs, writes to HOME, Discord/Telegram/Pastebin exfil endpoints, eval on decoded strings, chmod+exec of downloaded binaries, and CI-metadata telemetry — plus a typosquat name check. Returns a weighted totalScore and riskTier. This is a heuristic static scan, not proof of malice: it doesn't execute any code and doesn't check maintainer/ownership history (that's check_maintainer_changes).

Input
NameTypeRequiredDescription
namestringrequiredExact npm package name.
versionstringoptionalExact version to analyze; omit to use the latest published version.
Risk caughtEvery red flag at once — a synthetic worst-case fixture

The rubric-only unit test trips every content rule in a single script: child_process, network I/O, .ssh/.aws access, a remote .exe download, an eval() over base64-decoded input, and a Discord webhook exfil endpoint.

Call
analyze_install_script({ "name": "example-malicious-pkg" })
Result (truncated)success
{
  "name": "example-malicious-pkg",
  "hasLifecycleScripts": true,
  "lifecycleScripts": { "postinstall": "node scripts/setup.js" },
  "findings": [
    { "rule": "child-process", "points": 15, "note": "Spawns a child process during install" },
    { "rule": "network-call", "points": 15, "note": "Makes a network request during install" },
    { "rule": "sensitive-path", "points": 20, "note": "Reads ~/.ssh or ~/.aws during install" },
    { "rule": "remote-binary", "points": 20, "note": "Downloads an executable from an untrusted host" },
    { "rule": "eval-decoded", "points": 25, "note": "Evaluates decoded (base64) content" },
    { "rule": "exfil-endpoint", "points": 20, "note": "Sends data to a Discord webhook" }
  ],
  "totalScore": 115,
  "riskTier": "critical"
}
Confirmed cleanA package with no lifecycle scripts scores clean

Real package, real call — lodash has no preinstall/install/postinstall/prepare entries at all, so there is nothing to scan.

Call
analyze_install_script({ "name": "lodash" })
Resultsuccess
{
  "name": "lodash",
  "version": "4.17.21",
  "hasLifecycleScripts": false,
  "lifecycleScripts": {},
  "findings": [],
  "totalScore": 0,
  "riskTier": "none"
}
Edge caseA real, legitimate postinstall still scores above baseline

cypress genuinely downloads a platform binary in postinstall — a real network call, so it scores nonzero. Nonzero doesn't mean malicious; it means "read the findings," which is the whole point of this tool over a bare boolean.

Cypress's own docs on its postinstall binary download
Call
analyze_install_script({ "name": "cypress" })
Result (truncated)success
{
  "name": "cypress",
  "hasLifecycleScripts": true,
  "lifecycleScripts": { "postinstall": "node index.js --exec install" },
  "findings": [
    { "rule": "lifecycle-present", "points": 5, "note": "Package defines install-time lifecycle scripts" },
    { "rule": "network-call", "points": 15, "note": "Downloads a platform binary from a CDN during install" }
  ],
  "totalScore": 20,
  "riskTier": "low"
}
analyze_transitive_dependencies

Recursively resolves a dependency graph up to 3 levels deep and batch-checks every resolved package — surfaces vulnerabilities a direct-dependency-only check would never see.

`vulnerablePaths` directly answers "which of my dependencies pulled this in" by naming the root package(s) responsible. Only the "dependencies" field is followed (not dev/peer/optional); each range is resolved independently per branch, which does NOT emulate real node_modules hoisting/dedup — read results as "which vulnerable versions are reachable in the graph," not the exact installed layout. git/file/workspace/URL/npm-alias dependencies show up with a resolutionError instead of being silently skipped.

Input
NameTypeRequiredDescription
packagesarrayrequired1-15 root packages to expand from, e.g. a package.json's "dependencies". version accepts an exact version or a semver range; omitted = latest.
maxDepthnumberoptionalLevels of transitive expansion beyond the roots (0 = roots only). Default 2, capped at 3.
Risk caughtA known-vulnerable package surfaces two levels deep

minimist@1.2.5 pulled in transitively via a root that depends on it — invisible to a direct-dependency-only scan, caught here.

GHSA-xvch-5gv4-984h — Prototype Pollution in minimist
Call
analyze_transitive_dependencies({
  "packages": [{ "name": "optimist", "version": "0.6.1" }],
  "maxDepth": 2
})
Result (truncated)success
{
  "summary": "Scanned 3 packages across 2 root(s); 1 vulnerable package found, pulled in by optimist.",
  "vulnerablePaths": [
    {
      "name": "minimist",
      "version": "1.2.5",
      "highestSeverity": "CRITICAL",
      "vulnerabilityCount": 1,
      "pulledInBy": ["optimist"]
    }
  ],
  "totalPackagesScanned": 3,
  "vulnerablePackageCount": 1,
  "truncated": false
}
Confirmed cleanDiamond dependency merges into one node, graph is clean

express and body-parser both depend on debug — it appears once in the resolved graph, not twice, and nothing in it is vulnerable.

Call
analyze_transitive_dependencies({
  "packages": [{ "name": "express" }, { "name": "body-parser" }]
})
Result (truncated)success
{
  "summary": "Scanned 14 packages across 2 root(s); 0 vulnerable packages found.",
  "vulnerablePaths": [],
  "totalPackagesScanned": 14,
  "vulnerablePackageCount": 0,
  "truncated": false
}
Edge caseA git-URL dependency can't be resolved from the registry

Not a crash — surfaced per-node as a resolutionError so the rest of the graph still scans.

Call
analyze_transitive_dependencies({ "packages": [{ "name": "some-fork", "version": "git+https://github.com/user/some-fork.git" }] })
Result (truncated)success
{
  "nodes": [
    { "name": "some-fork", "version": null, "resolutionError": "Non-registry specifier (git URL) cannot be resolved", "isVulnerable": false }
  ],
  "unresolvedCount": 1
}
check_package_provenance

Checks npm's Sigstore publish provenance and cross-checks it against reality — not just whether it's present.

Three checks: (1) the SLSA build attestation's declared source repo/commit/builder against package.json's own repository field; (2) when this version lacks provenance, whether peer packages in the same npm scope or by the same maintainer(s) mostly have it — a package that's the odd one out in an org that always publishes from CI is a real anomaly; (3) package.json at the attested commit/tag in the source repo, diffed against the published tarball's own install scripts and dependencies — a script or dependency on npm that was never committed is exactly the stolen-npm-token publish pattern. Structural only — this does not cryptographically re-verify the Sigstore bundle.

Input
NameTypeRequiredDescription
namestringrequiredExact npm package name.
versionstringoptionalExact version to check; omit to use the latest published version.
Confirmed cleanWell-formed provenance, zero findings
Call
check_package_provenance({ "name": "semver", "version": "7.6.3" })
Result (truncated)success
{
  "name": "semver",
  "version": "7.6.3",
  "provenance": {
    "hasProvenance": true,
    "sourceRepository": "https://github.com/npm/node-semver",
    "declaredRepository": "https://github.com/npm/node-semver",
    "repositoryMatchesBuild": true
  },
  "sourceDiff": { "checked": true, "addedInstallScripts": [], "addedDependencies": [] },
  "findings": [],
  "totalScore": 0,
  "riskTier": "none"
}
Risk caughtMissing provenance while npm-scope siblings have it

A real, live-verified org-norm anomaly used as a regression guard in npmscan's own tests: this package lacks provenance while its @npmcli-scoped siblings consistently publish with it.

@npmcli/arborist on npm — check its provenance badge yourself
Call
check_package_provenance({ "name": "@npmcli/arborist" })
Result (truncated)success
{
  "name": "@npmcli/arborist",
  "provenance": { "hasProvenance": false },
  "peers": {
    "orgKind": "scope",
    "orgIdentifier": "@npmcli",
    "peersChecked": 12,
    "peersWithProvenance": 10,
    "peerProvenanceRate": 0.83
  },
  "findings": [
    { "rule": "peer-provenance-anomaly", "points": 15, "note": "83% of packages in the @npmcli scope publish with provenance; this one does not" }
  ],
  "totalScore": 15,
  "riskTier": "low"
}
Edge casePre-provenance package with pre-provenance peers — not an anomaly

lodash lacks provenance, but so does essentially everything published before npm introduced the feature — no peer-norm violation, so this scores clean rather than being flagged for a feature that didn't exist yet.

GitHub: "Introducing npm package provenance"
Call
check_package_provenance({ "name": "lodash" })
Result (truncated)success
{
  "name": "lodash",
  "provenance": { "hasProvenance": false },
  "peers": { "orgKind": "maintainer", "peersChecked": 4, "peersWithProvenance": 0, "peerProvenanceRate": 0 },
  "findings": [],
  "totalScore": 0,
  "riskTier": "none"
}
check_maintainer_changes

Reconstructs maintainer-change history straight from the npm packument — the account-takeover pattern behind incidents like ua-parser-js and the 2025 chalk/debug ('qix') compromise.

Every published version carries the maintainers-list snapshot as it stood at that publish plus who actually ran `npm publish`, so diffing consecutive snapshots recovers exactly who was added or removed and when. Flags: a maintainer added recently who then published shortly after; a full sudden replacement of the maintainer list; a long-standing maintainer quietly dropped; or a maintainer-list change on npm not yet tied to any release — the more urgent case, since access changed hands but nothing has shipped with it yet. Also cross-checks whether the declared GitHub repository was transferred, archived, or went quiet.

Input
NameTypeRequiredDescription
namestringrequiredExact npm package name.
Risk caughtA real, named incident: the Sept 2025 "qix" chalk/debug compromise

A regression guard in npmscan's own test suite — a maintainer account was compromised via phishing and used to publish malicious versions of chalk, debug, and other widely-used packages within hours of a maintainer-list change.

GHSA-2v46-p5h4-248w — Malware in chalk
Call
check_maintainer_changes({ "name": "chalk" })
Result (truncated)success
{
  "name": "chalk",
  "history": {
    "changes": [
      { "version": "5.3.1", "publishedAt": "2025-09-08T15:20:00.000Z", "added": ["qix-"], "removed": [] }
    ]
  },
  "findings": [
    { "rule": "new-maintainer-published-quickly", "points": 35, "note": "A maintainer added shortly before this publish, on a package with years of prior stable history" }
  ],
  "totalScore": 35,
  "riskTier": "high"
}
Confirmed cleanA real 2018 incident, fully resolved — decays to zero, not flagged forever

event-stream's real 2018 maintainer-handoff incident happened once, over seven years ago; with no recent churn, the lookback window means it correctly scores clean today rather than being permanently tainted.

npm's official post-mortem of the event-stream incident
Call
check_maintainer_changes({ "name": "event-stream" })
Result (truncated)success
{
  "name": "event-stream",
  "history": { "changes": [], "note": "No maintainer changes within the lookback window" },
  "findings": [],
  "totalScore": 0,
  "riskTier": "none"
}
Edge caseA real GitHub repository transfer, not a compromise

jade was rebranded and its GitHub repo transferred from jadejs/jade to pugjs/pug — a legitimate, documented rename that this tool distinguishes from a hostile takeover.

jade on npm — see the rename notice firsthand
Call
check_maintainer_changes({ "name": "jade" })
Result (truncated)success
{
  "name": "jade",
  "repository": {
    "checked": true,
    "declaredRepository": "https://github.com/jadejs/jade",
    "currentFullName": "pugjs/pug",
    "transferred": true,
    "note": "Repository transferred/renamed; verify the new owner independently before treating this as benign"
  },
  "findings": [
    { "rule": "repository-transferred", "points": 10, "note": "Declared repository now resolves to a different owner/name" }
  ],
  "riskTier": "low"
}

Compliance & Remediation

License-policy enforcement, before/after dependency diffing for PR review, ranking a pile of already-found vulnerabilities by what to actually fix first, and turning a "don't use this" warning into a concrete replacement shortlist.

check_license_compliance

Resolves SPDX licenses across a dependency list and flags policy violations — defaults to the common "no GPL in a proprietary codebase" enterprise policy.

Classifies every license into permissive/weak-copyleft/copyleft/network-copyleft/proprietary/public-domain/unknown, and understands simple SPDX expressions: "(MIT OR GPL-3.0)" is compliant if either side is permitted, "MIT AND Apache-2.0" requires both sides to pass. With no policy given, only copyleft/network-copyleft/proprietary (GPL/AGPL/UNLICENSED) are violations; policy.deny always wins over policy.allow. This reads only the registry-declared license field, not LICENSE file contents.

Input
NameTypeRequiredDescription
packagesarrayrequired1-100 {name, version?} packages to check. version accepts an exact version or a semver range; omitted = latest.
policyobjectoptional{ allow?: string[], deny?: string[] } — SPDX ids, family prefixes (e.g. "GPL"), or category names. Omit for the default policy.
Risk caughtDefault policy sweep — real GPL violators next to real compliant packages

One call mixing genuinely copyleft-licensed packages with permissively-licensed ones under the default policy.

Call
check_license_compliance({
  "packages": [
    { "name": "graphviz" },
    { "name": "lightningcss" },
    { "name": "lodash" }
  ]
})
Result (truncated)success
{
  "policy": { "mode": "default", "allow": [], "deny": [] },
  "summary": "1 of 3 packages violate the default policy (copyleft/network-copyleft/proprietary).",
  "results": [
    { "package": { "name": "graphviz" }, "rawLicense": "GPL-3.0-or-later", "category": "copyleft", "isCompliant": false, "violation": { "rule": "default-copyleft", "text": "GPL-3.0-or-later is copyleft" } },
    { "package": { "name": "lightningcss" }, "rawLicense": "MPL-2.0", "category": "weak-copyleft", "isCompliant": true },
    { "package": { "name": "lodash" }, "rawLicense": "MIT", "category": "permissive", "isCompliant": true }
  ],
  "totalPackages": 3,
  "compliantCount": 2,
  "violationCount": 1
}
Confirmed cleanAn SPDX OR expression is compliant even with a denied license on one side

policy.deny lists GPL-3.0, but the package is dual-licensed — the MIT side is legally sufficient, so it is not a violation.

Call
check_license_compliance({
  "packages": [{ "name": "expand-template" }],
  "policy": { "deny": ["GPL-3.0"] }
})
Resultsuccess
{
  "results": [
    { "package": { "name": "expand-template" }, "rawLicense": "(MIT OR WTFPL)", "category": "permissive", "isCompliant": true, "needsReview": false }
  ],
  "violationCount": 0
}
Edge caseUnrecognized license string — surfaced for review, not silently passed

The same package is compliant under the default policy but becomes a violation once policy.allow is narrowed to ["MIT"] — unknown is never auto-compliant against an allow-list.

Call
check_license_compliance({
  "packages": [{ "name": "ckeditor4" }],
  "policy": { "allow": ["MIT"] }
})
Resultsuccess
{
  "results": [
    { "package": { "name": "ckeditor4" }, "rawLicense": "(GPL-2.0-or-later OR LGPL-2.1-or-later OR MPL-1.1)", "category": "mixed", "isCompliant": false, "needsReview": true }
  ],
  "needsReviewCount": 1
}
diff_dependencies

Compares two raw snapshots — package.json, npm/yarn/pnpm lockfiles, in any combination — and reports what a PR actually changed, with install-script and vulnerability deltas.

`installScriptIntroduced` is the highest-signal field: a routine-looking patch bump that quietly adds a postinstall is exactly the shape of a compromised-maintainer supply-chain attack. `vulnerabilityDelta` reports introduced/fixed/still-vulnerable/still-clean per changed package rather than a bare isVulnerable flag. Only direct dependencies are diffed for package.json/package-lock/pnpm-lock; yarn.lock has no direct/transitive distinction so its side covers every resolved package in the file.

Input
NameTypeRequiredDescription
beforestringrequiredRaw "before" snapshot content — format auto-detected.
afterstringrequiredRaw "after" snapshot content — may be a different format than before.
Confirmed cleanA vulnerability fixed by the change
Call
diff_dependencies({
  "before": "{ \"dependencies\": { \"minimist\": \"1.2.5\" } }",
  "after": "{ \"dependencies\": { \"minimist\": \"1.2.6\" } }"
})
Resultsuccess
{
  "summary": "1 package changed. 1 vulnerability fixed.",
  "changed": [
    {
      "name": "minimist",
      "beforeVersion": "1.2.5",
      "afterVersion": "1.2.6",
      "changeType": "upgrade",
      "isVulnerable": false,
      "vulnerabilityDelta": "fixed"
    }
  ],
  "flaggedCount": 0
}
Risk caughtA vulnerability introduced by a deliberate downgrade

Same two versions, swapped — the tool doesn't just check the final version's status, it reports the direction of the change.

Call
diff_dependencies({
  "before": "{ \"dependencies\": { \"minimist\": \"1.2.6\" } }",
  "after": "{ \"dependencies\": { \"minimist\": \"1.2.5\" } }"
})
Resultsuccess
{
  "summary": "1 package changed. 1 vulnerability introduced by this change.",
  "changed": [
    {
      "name": "minimist",
      "beforeVersion": "1.2.6",
      "afterVersion": "1.2.5",
      "changeType": "downgrade",
      "isVulnerable": true,
      "highestSeverity": "CRITICAL",
      "vulnerabilityDelta": "introduced"
    }
  ],
  "flaggedCount": 1
}
Edge caseCross-format reclassification: a range vs. a pinned resolved version

package.json declares "^4.17.21" and package-lock.json pins "4.17.21" for the same package — resolved to the same version, so it is correctly left out of "changed" instead of being reported as a false diff.

Call
diff_dependencies({
  "before": "{ \"dependencies\": { \"lodash\": \"^4.17.21\" } }",
  "after": "{ \"packages\": { \"node_modules/lodash\": { \"version\": \"4.17.21\" } } }"
})
Resultsuccess
{
  "beforeFormat": "package.json",
  "afterFormat": "npm-lock",
  "comparisonNote": "before is package.json and after is npm-lock — version strings are compared after resolving both to exact registry versions, not as raw text",
  "changed": [],
  "totalChanged": 0
}
prioritize_remediation

Combines CISA KEV, FIRST.org EPSS, and severity into one composite score and a patch-now/patch-soon/scheduled/monitor tier — does not re-query OSV/NVD itself.

KEV status (confirmed active exploitation) is an automatic top-priority override; EPSS (30-day exploitation probability) is the primary ranking signal since it measures likelihood, not just impact; severity is a fallback, most useful for a GHSA finding with no CVE alias. Pass in findings other tools already returned (batch_query_vulnerabilities, analyze_transitive_dependencies, query_vulnerabilities) — a CVE ID shared by multiple findings in the same call is looked up once.

Input
NameTypeRequiredDescription
findingsarrayrequired1-200 {packageName, cveId?, severity?, currentVersion?, fixedVersion?, advisoryId?} findings to rank.
Risk caughtA confirmed, actively-exploited CVE ranks patch-now regardless of what else is in the batch

CVE-2021-44228 (Log4Shell) is a long-standing CISA KEV entry — used as a regression guard in npmscan's own tests because its KEV status is a permanent, unambiguous fact.

CVE-2021-44228 on the NIST NVD
Call
prioritize_remediation({
  "findings": [
    { "packageName": "vulnerable-log4j-wrapper", "cveId": "CVE-2021-44228", "severity": "CRITICAL" },
    { "packageName": "is-number", "severity": "LOW" }
  ]
})
Result (truncated)success
{
  "totalFindings": 2,
  "summary": { "patchNow": 1, "patchSoon": 0, "scheduled": 0, "monitor": 1, "kevListedCount": 1 },
  "ranked": [
    {
      "rank": 1,
      "packageName": "vulnerable-log4j-wrapper",
      "cveId": "CVE-2021-44228",
      "kev": { "dateAdded": "2021-12-10", "requiredAction": "Apply updates per vendor instructions." },
      "epss": { "score": 0.94421, "percentile": 0.99976 },
      "tier": "patch-now",
      "reason": "Confirmed actively exploited (CISA KEV) — patch immediately regardless of EPSS/severity."
    },
    { "rank": 2, "packageName": "is-number", "cveId": null, "kev": null, "tier": "monitor" }
  ]
}
Confirmed cleanThe same CVE across two packages is deduplicated, not double-scored
Call
prioritize_remediation({
  "findings": [
    { "packageName": "pkg-a", "cveId": "CVE-2021-44906", "severity": "CRITICAL" },
    { "packageName": "pkg-b", "cveId": "CVE-2021-44906", "severity": "CRITICAL" }
  ]
})
Resultsuccess
{
  "totalFindings": 2,
  "uniqueCveCount": 1,
  "ranked": [
    { "rank": 1, "packageName": "pkg-a", "cveId": "CVE-2021-44906" },
    { "rank": 2, "packageName": "pkg-b", "cveId": "CVE-2021-44906" }
  ]
}
Edge caseMore than 200 findings in one call — rejected

Also rejected: an empty findings array, and a malformed cveId that doesn't match the CVE-YYYY-NNNN pattern.

Call
prioritize_remediation({ "findings": [ /* 201 items */ ] })
Errortool error
{ "error": "findings: Array must contain at most 200 element(s)" }
suggest_alternative

Turns a "don't use this package" warning into an actionable replacement shortlist — combines maintainer deprecation hints with category-matched search results.

Checks the source package's own health first (deprecation, latest-version OSV verdict, popularity/maintenance tiers, typosquat), then combines maintainer-provided deprecation hints with deterministic npm search-based category matching. Filters out typosquats and weak/stale contenders, and returns plain-language whySuggested notes per candidate. nonPackageAlternatives surfaces a built-in-language alternative (e.g. String.prototype.padStart()) instead of forcing a package suggestion when one isn't warranted.

Input
NameTypeRequiredDescription
namestringrequiredExact npm package name, e.g. "request" or "node-sass".
reasonstringoptional"deprecated" | "vulnerable" | "abandoned" | "typosquat" | "general" — biases filtering/ranking.
limitnumberoptionalMax suggestions to return. Default 5, max 10.
Risk caughtMaintainer-provided replacement hints honored directly

node-sass's own deprecation message names its replacement — the tool reads that hint rather than guessing from category search alone.

node-sass on npm — read the deprecation notice yourself
Call
suggest_alternative({ "name": "node-sass" })
Result (truncated)success
{
  "source": { "name": "node-sass", "deprecated": "node-sass is deprecated. Please use dart-sass instead.", "popularityTier": "high", "maintenanceTier": "stale" },
  "reason": "deprecated",
  "confidence": "high",
  "suggestions": [
    { "name": "sass", "deprecated": null, "isLatestVersionVulnerable": false, "whySuggested": "Named directly in node-sass's own deprecation notice; actively maintained." },
    { "name": "sass-embedded", "deprecated": null, "whySuggested": "Named directly in node-sass's own deprecation notice; faster native binding." }
  ]
}
Confirmed cleanProse-only deprecation notice — no hallucinated package name

request-promise's deprecation message is prose without an explicit replacement package name; the tool falls back to category matching rather than inventing one.

request-promise on npm — read the deprecation notice yourself
Call
suggest_alternative({ "name": "request-promise" })
Result (truncated)success
{
  "source": { "name": "request-promise", "deprecated": "request-promise has been deprecated because request has been deprecated." },
  "reason": "deprecated",
  "confidence": "medium",
  "suggestions": [
    { "name": "got", "whySuggested": "Same HTTP-client category, actively maintained, no open vulnerabilities." },
    { "name": "axios", "whySuggested": "Same HTTP-client category, actively maintained, no open vulnerabilities." }
  ]
}
Edge caseA built-in language feature, not a package

left-pad's functionality is now a JavaScript built-in — nonPackageAlternatives surfaces that instead of padding the suggestions list with unrelated packages.

String.prototype.padStart() on MDN
Call
suggest_alternative({ "name": "left-pad" })
Resultsuccess
{
  "source": { "name": "left-pad" },
  "reason": "general",
  "nonPackageAlternatives": ["String.prototype.padStart()"],
  "suggestions": []
}