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.
https://npmscan.com/api/mcp.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.- 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_packagesSearch 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.
| Name | Type | Required | Description |
|---|---|---|---|
| query | string | required | Search text, e.g. a package name or keywords (2–64 characters). |
| limit | number | optional | Max results to return. Default 20, max 50. |
Real name, high download counts, no typosquat match. This is what a normal, trustworthy result looks like.
search_packages({ "query": "react-router", "limit": 3 }){
"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 */
]
}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.
search_packages({ "query": "raect" }){
"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"
}
]
}The 2-character minimum is enforced locally; a 1-character query never reaches npm.
search_packages({ "query": "a" }){ "error": "\"query\" must be at least 2 characters" }get_packageFull 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.
| Name | Type | Required | Description |
|---|---|---|---|
| name | string | required | Exact npm package name, e.g. "lodash" or "@scope/name". |
get_package({ "name": "axios" }){
"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": []
}`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 GitHubget_package({ "name": "request" }){
"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": []
}get_package({ "name": "this-package-does-not-exist-anywhere" }){ "error": "Package \"this-package-does-not-exist-anywhere\" not found" }get_package_versionMetadata 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.
| Name | Type | Required | Description |
|---|---|---|---|
| name | string | required | Exact npm package name. |
| version | string | required | Exact version string, e.g. "4.17.21". |
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 minimistget_package_version({ "name": "minimist", "version": "1.2.5" }){
"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"
}
]
}Same package, the very next release — confirms the tool isn't just flagging the package by name.
get_package_version({ "name": "minimist", "version": "1.2.6" }){
"name": "minimist",
"version": "1.2.6",
"isVulnerable": false,
"highestSeverity": null,
"vulnerabilities": []
}get_package_version({ "name": "minimist", "version": "999.0.0" }){ "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_vulnerabilitiesOSV.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."
| Name | Type | Required | Description |
|---|---|---|---|
| name | string | required | npm package name. |
| version | string | optional | Optional exact version to narrow results, e.g. one pinned in a lockfile. |
| ecosystem | string | optional | OSV ecosystem. Default "npm". |
query_vulnerabilities({ "name": "minimist", "version": "1.2.5" }){
"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" }
]
}query_vulnerabilities({ "name": "minimist", "version": "1.2.6" }){
"package": "minimist",
"version": "1.2.6",
"isVulnerable": false,
"highestSeverity": null,
"vulnerabilities": []
}Omitting version here would wrongly read as "minimist is currently unsafe" if you don't also check which version you actually have.
query_vulnerabilities({ "name": "minimist" }){
"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_vulnerabilitiesOSV.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.
| Name | Type | Required | Description |
|---|---|---|---|
| packages | array | optional | Explicit {name, version?} list (1-1000 items). Use this OR content, not both. |
| content | string | optional | Raw package.json / package-lock.json / yarn.lock / pnpm-lock.yaml / CycloneDX JSON / SPDX JSON content. Use this OR packages, not both. |
| includeDevDependencies | boolean | optional | Only applies when content is a manifest/lockfile format that distinguishes dev dependencies. |
batch_query_vulnerabilities({
"packages": [
{ "name": "minimist", "version": "1.2.5" },
{ "name": "is-number", "version": "7.0.0" }
]
}){
"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
}batch_query_vulnerabilities({
"packages": [
{ "name": "is-number", "version": "7.0.0" },
{ "name": "lodash", "version": "4.17.21" }
]
}){
"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
}The two input modes are mutually exclusive; this fails before any OSV call is made.
batch_query_vulnerabilities({
"packages": [{ "name": "lodash" }],
"content": "{ \"dependencies\": { \"lodash\": \"4.17.21\" } }"
}){ "error": "Provide either packages or content, not both" }get_latest_advisoriesBrowse 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.
| Name | Type | Required | Description |
|---|---|---|---|
| severity | string | optional | "critical" | "high" | "medium" | "low" | "all" (default all). |
| category | string | optional | Vulnerability category id, e.g. "xss", "sql-injection", "ssrf", "access-control". |
| affects | string | optional | Filter to advisories affecting this npm package name. |
| ghsaId | string | optional | Exact lookup by GHSA ID. |
| cveId | string | optional | Exact lookup by CVE ID. |
| direction | string | optional | "asc" | "desc" by published date (default desc). |
| cursor | string | optional | Opaque pagination cursor from a previous response's nextCursor. |
The npmscan test suite asserts this directly: every advisory returned for category="xss" must carry the XSS category label, not just be loosely related.
get_latest_advisories({ "category": "xss", "severity": "high" }){
"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 */
]
}get_latest_advisories({ "ghsaId": "GHSA-xvch-5gv4-984h" }){
"advisories": [
{ "id": "GHSA-xvch-5gv4-984h", "cve": "CVE-2021-44906", "summary": "Prototype Pollution in minimist", "severity": "critical" }
]
}A neutral, non-error case — both directions succeed, this is a sort-order sanity check, not a risk signal.
get_latest_advisories({ "direction": "asc" }){ "direction": "asc", "advisories": [ /* oldest reviewed advisory first */ ] }get_cveAuthoritative 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.
| Name | Type | Required | Description |
|---|---|---|---|
| cveId | string | optional | Exact CVE ID for a single lookup, e.g. "CVE-2021-44228". Omit search filters when this is given. |
| keywordSearch | string | optional | Free-text search, e.g. a package or product name. |
| severity | string | optional | "CRITICAL" | "HIGH" | "MEDIUM" | "LOW" — CVSS v3 base severity filter. |
| cweId | string | optional | Filter by weakness type, e.g. "CWE-79". |
| publishedSince / publishedUntil | string | optional | YYYY-MM-DD date range, must be given together, capped at 120 days. |
| resultsPerPage | number | optional | Max results for a search (default 10, max 50). |
| startIndex | number | optional | Pagination offset for a search. |
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 NVDget_cve({ "cveId": "CVE-2021-44228" }){
"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" }
}get_cve({ "cveId": "CVE-2099-99999" }){ "cveId": "CVE-2099-99999", "found": false, "npmscanUrl": "https://npmscan.com/vulnerability/CVE-2099-99999" }Also rejected: malformed CVE IDs, and a publishedSince given without publishedUntil (or vice versa).
get_cve({}){ "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_scriptStatically 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).
| Name | Type | Required | Description |
|---|---|---|---|
| name | string | required | Exact npm package name. |
| version | string | optional | Exact version to analyze; omit to use the latest published version. |
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.
analyze_install_script({ "name": "example-malicious-pkg" }){
"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"
}Real package, real call — lodash has no preinstall/install/postinstall/prepare entries at all, so there is nothing to scan.
analyze_install_script({ "name": "lodash" }){
"name": "lodash",
"version": "4.17.21",
"hasLifecycleScripts": false,
"lifecycleScripts": {},
"findings": [],
"totalScore": 0,
"riskTier": "none"
}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 downloadanalyze_install_script({ "name": "cypress" }){
"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_dependenciesRecursively 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.
| Name | Type | Required | Description |
|---|---|---|---|
| packages | array | required | 1-15 root packages to expand from, e.g. a package.json's "dependencies". version accepts an exact version or a semver range; omitted = latest. |
| maxDepth | number | optional | Levels of transitive expansion beyond the roots (0 = roots only). Default 2, capped at 3. |
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 minimistanalyze_transitive_dependencies({
"packages": [{ "name": "optimist", "version": "0.6.1" }],
"maxDepth": 2
}){
"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
}express and body-parser both depend on debug — it appears once in the resolved graph, not twice, and nothing in it is vulnerable.
analyze_transitive_dependencies({
"packages": [{ "name": "express" }, { "name": "body-parser" }]
}){
"summary": "Scanned 14 packages across 2 root(s); 0 vulnerable packages found.",
"vulnerablePaths": [],
"totalPackagesScanned": 14,
"vulnerablePackageCount": 0,
"truncated": false
}Not a crash — surfaced per-node as a resolutionError so the rest of the graph still scans.
analyze_transitive_dependencies({ "packages": [{ "name": "some-fork", "version": "git+https://github.com/user/some-fork.git" }] }){
"nodes": [
{ "name": "some-fork", "version": null, "resolutionError": "Non-registry specifier (git URL) cannot be resolved", "isVulnerable": false }
],
"unresolvedCount": 1
}check_package_provenanceChecks 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.
| Name | Type | Required | Description |
|---|---|---|---|
| name | string | required | Exact npm package name. |
| version | string | optional | Exact version to check; omit to use the latest published version. |
check_package_provenance({ "name": "semver", "version": "7.6.3" }){
"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"
}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 yourselfcheck_package_provenance({ "name": "@npmcli/arborist" }){
"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"
}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"check_package_provenance({ "name": "lodash" }){
"name": "lodash",
"provenance": { "hasProvenance": false },
"peers": { "orgKind": "maintainer", "peersChecked": 4, "peersWithProvenance": 0, "peerProvenanceRate": 0 },
"findings": [],
"totalScore": 0,
"riskTier": "none"
}check_maintainer_changesReconstructs 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.
| Name | Type | Required | Description |
|---|---|---|---|
| name | string | required | Exact npm package name. |
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 chalkcheck_maintainer_changes({ "name": "chalk" }){
"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"
}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 incidentcheck_maintainer_changes({ "name": "event-stream" }){
"name": "event-stream",
"history": { "changes": [], "note": "No maintainer changes within the lookback window" },
"findings": [],
"totalScore": 0,
"riskTier": "none"
}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 firsthandcheck_maintainer_changes({ "name": "jade" }){
"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_complianceResolves 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.
| Name | Type | Required | Description |
|---|---|---|---|
| packages | array | required | 1-100 {name, version?} packages to check. version accepts an exact version or a semver range; omitted = latest. |
| policy | object | optional | { allow?: string[], deny?: string[] } — SPDX ids, family prefixes (e.g. "GPL"), or category names. Omit for the default policy. |
One call mixing genuinely copyleft-licensed packages with permissively-licensed ones under the default policy.
check_license_compliance({
"packages": [
{ "name": "graphviz" },
{ "name": "lightningcss" },
{ "name": "lodash" }
]
}){
"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
}policy.deny lists GPL-3.0, but the package is dual-licensed — the MIT side is legally sufficient, so it is not a violation.
check_license_compliance({
"packages": [{ "name": "expand-template" }],
"policy": { "deny": ["GPL-3.0"] }
}){
"results": [
{ "package": { "name": "expand-template" }, "rawLicense": "(MIT OR WTFPL)", "category": "permissive", "isCompliant": true, "needsReview": false }
],
"violationCount": 0
}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.
check_license_compliance({
"packages": [{ "name": "ckeditor4" }],
"policy": { "allow": ["MIT"] }
}){
"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_dependenciesCompares 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.
| Name | Type | Required | Description |
|---|---|---|---|
| before | string | required | Raw "before" snapshot content — format auto-detected. |
| after | string | required | Raw "after" snapshot content — may be a different format than before. |
diff_dependencies({
"before": "{ \"dependencies\": { \"minimist\": \"1.2.5\" } }",
"after": "{ \"dependencies\": { \"minimist\": \"1.2.6\" } }"
}){
"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
}Same two versions, swapped — the tool doesn't just check the final version's status, it reports the direction of the change.
diff_dependencies({
"before": "{ \"dependencies\": { \"minimist\": \"1.2.6\" } }",
"after": "{ \"dependencies\": { \"minimist\": \"1.2.5\" } }"
}){
"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
}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.
diff_dependencies({
"before": "{ \"dependencies\": { \"lodash\": \"^4.17.21\" } }",
"after": "{ \"packages\": { \"node_modules/lodash\": { \"version\": \"4.17.21\" } } }"
}){
"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_remediationCombines 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.
| Name | Type | Required | Description |
|---|---|---|---|
| findings | array | required | 1-200 {packageName, cveId?, severity?, currentVersion?, fixedVersion?, advisoryId?} findings to rank. |
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 NVDprioritize_remediation({
"findings": [
{ "packageName": "vulnerable-log4j-wrapper", "cveId": "CVE-2021-44228", "severity": "CRITICAL" },
{ "packageName": "is-number", "severity": "LOW" }
]
}){
"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" }
]
}prioritize_remediation({
"findings": [
{ "packageName": "pkg-a", "cveId": "CVE-2021-44906", "severity": "CRITICAL" },
{ "packageName": "pkg-b", "cveId": "CVE-2021-44906", "severity": "CRITICAL" }
]
}){
"totalFindings": 2,
"uniqueCveCount": 1,
"ranked": [
{ "rank": 1, "packageName": "pkg-a", "cveId": "CVE-2021-44906" },
{ "rank": 2, "packageName": "pkg-b", "cveId": "CVE-2021-44906" }
]
}Also rejected: an empty findings array, and a malformed cveId that doesn't match the CVE-YYYY-NNNN pattern.
prioritize_remediation({ "findings": [ /* 201 items */ ] }){ "error": "findings: Array must contain at most 200 element(s)" }suggest_alternativeTurns 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.
| Name | Type | Required | Description |
|---|---|---|---|
| name | string | required | Exact npm package name, e.g. "request" or "node-sass". |
| reason | string | optional | "deprecated" | "vulnerable" | "abandoned" | "typosquat" | "general" — biases filtering/ranking. |
| limit | number | optional | Max suggestions to return. Default 5, max 10. |
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 yourselfsuggest_alternative({ "name": "node-sass" }){
"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." }
]
}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 yourselfsuggest_alternative({ "name": "request-promise" }){
"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." }
]
}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 MDNsuggest_alternative({ "name": "left-pad" }){
"source": { "name": "left-pad" },
"reason": "general",
"nonPackageAlternatives": ["String.prototype.padStart()"],
"suggestions": []
}