unripe
This MCP server lets AI coding agents check dependency version ages against a cooldown policy and inspect existing cooldown settings.
Check a package version's age, or list recent releases and identify the newest version old enough to use.
Support many ecosystems, including npm, PyPI, Cargo, Go, Composer, NuGet, Maven, GitHub Actions, and more.
Check lockfiles under a directory for versions that are too new or have unknown ages.
Limit lockfile checks to versions added since a git ref such as
origin/main.Show the project's unripe policy, existing package manager/update bot cooldown settings, disagreements, and missing config lines.
Override the minimum age for package or lockfile checks.
All tools are read-only and non-destructive.
Parses the text bun.lock file and checks dependency version ages via the npm registry (the legacy binary bun.lockb cannot be read).
Parses Podfile.lock and dates each pod against the CocoaPods trunk.
Parses composer.lock, using the lockfile's own time field or falling back to Packagist, to flag PHP dependencies published more recently than the minimum age.
Parses conan.lock for Conan 1 and 2, dating a dependency by the recipe revision pinned in the lockfile using ConanCenter.
Parses Dart pubspec.lock and checks package version ages against pub.dev.
Parses deno.lock and checks dependency publish times from both the npm registry and jsr.io.
Parses Flutter pubspec.lock lockfiles and checks package version ages against pub.dev.
With --workflows, reads the uses: lines in .github/workflows/*.yml and composite action.yml files, dating each action by the GitHub release whose tag points at the same commit (falling back to commit time, which is marked as such).
Parses gradle.lockfile and gradle/dependency-locks/*.lockfile, resolving publish times from Maven Central, Google's Maven repository and the Gradle plugin portal (with extra repositories configurable).
Used as a publish-time source for JSR packages referenced in Deno lockfiles.
Reads npm lockfiles (package-lock.json, npm-shrinkwrap.json, and the pnpm/yarn/bun/deno lockfiles) and looks up each package's publish time on the npm registry, failing when a version is newer than the configured minimum age and reporting which older version would pass.
Parses NuGet packages.lock.json and looks up package versions on nuget.org to flag too-recent releases.
Serves as the publish-time source for Composer packages when composer.lock does not record a time field.
Parses pnpm-lock.yaml and checks each dependency's version age against the npm registry.
Parses poetry.lock and dates each locked package using PyPI publish times.
Checks Python dependencies from uv.lock, pylock.toml, poetry.lock, Pipfile.lock and pinned requirements*/constraints* files, taking upload times from the lockfile itself where available and otherwise from PyPI.
Parses Gemfile.lock and checks gem version ages against rubygems.org.
Parses Swift Package.resolved files, dating packages hosted on GitHub by their GitHub releases.
Parses .terraform.lock.hcl and checks provider versions against the Terraform registry (providers from other registries such as OpenTofu's are skipped).
Parses yarn.lock (both classic and berry formats) and checks dependency version ages via the npm registry.
unripe
unripe checks your lockfiles and fails if any dependency version was published too recently. The default is 3 days. It works across npm, pnpm, yarn, bun, Deno, PyPI, Cargo, Go, Composer, RubyGems, NuGet, Gradle, Dart and Flutter, CocoaPods, Swift, Terraform, Conan, R, Elixir, Nix, Bazel and vcpkg, and it can check the actions your GitHub workflows use too. A repo with a Python backend, a JavaScript frontend and a Flutter app gets one check and one policy instead of a different setting in every tool.
It runs in CI, as a pre-commit hook, from Python, and as an MCP server so AI coding agents check a version's age before they add it.
Here it is on a small test project with Deno, Python and PHP parts, the
evening of October 21, 2026. uv add had just locked a certifi released two
days earlier:
$ unripe check
unripe: checked 867 versions in 4 lockfiles, minimum age 3d
unripe: 2 versions published less than 3d ago
composer laravel/framework 12.70.1 13h old, published 2026-10-21 12:55 UTC, old enough at 2026-10-24 12:55 UTC
php/composer.lock:1
12.70.0 is old enough (published 2026-10-13)
pypi certifi 2026.10.19 47h old, published 2026-10-20 02:24 UTC, old enough at 2026-10-23 02:24 UTC
api/pylock.toml:8, api/uv.lock:6
2026.10.18 is old enough (published 2026-10-18)
fail: 2 unripe, 0 unknown, 865 ok, 0 excludedNothing is wrong with either of those. That's normal. Most new versions are fine. The point is that when one isn't, you find out after everyone else does, not before. And when one is too new, unripe tells you which older version would pass.
Why bother
When a popular package gets hijacked, the bad version is usually spotted and pulled within hours. In August 2026 a worm went through npm and published malicious versions of 444 packages in about four hours, stealing CI and cloud credentials from whoever installed them. In March, two backdoored LiteLLM releases sat on PyPI for somewhere between 40 minutes and three hours. Waiting a few days before taking a new version would have skipped both.
You can check that claim. Here is a lockfile with the debug and chalk
versions from the September 2025 npm compromise, checked as of two hours
after they went out:
$ unripe check --as-of 2025-09-08T15:00Z
unripe: checked 3 versions in 1 lockfile, minimum age 3d
unripe: 2 versions published less than 3d ago
npm chalk 5.6.1 1h old, published 2025-09-08 13:13 UTC, old enough at 2025-09-11 13:13 UTC
package-lock.json:3
5.6.0 is old enough (published 2025-08-17)
npm debug 4.4.2 1h old, published 2025-09-08 13:12 UTC, old enough at 2025-09-11 13:12 UTC
package-lock.json:2
4.4.1 is old enough (published 2025-05-13)
fail: 2 unripe, 0 unknown, 1 ok, 0 excludedRun it today and both come back as unpublished, which is how npm takes malicious versions down, so a lockfile that still has one fails either way.
Most package managers have added a setting for this in the last year, and if yours has one you should turn it on. unripe is for what those settings don't do. It checks what is already in the lockfile, including whatever a bot, an agent or an old tool put there. It reports on just the versions a pull request adds. It applies one policy to every ecosystem in the repo. And it covers Go, Composer, NuGet, Gradle, Dart, CocoaPods, Swift, Terraform, Conan, R, Nix, Bazel, vcpkg and GitHub Actions, which don't have a setting yet.
docs/case-studies.md replays more incidents the same way, and docs/threat-model.md says plainly what a cooldown does not protect against.
Related MCP server: package-version-check-mcp
Install
You need Python 3.11 or newer. unripe has no dependencies, on purpose: a tool that checks your supply chain shouldn't add to it.
uvx unripe checkor pipx install unripe, or pip install unripe. To start a config and a
GitHub workflow in one go:
unripe init --githubunripe doctor checks the setup: which config and lockfiles it finds,
whether each lockfile reads, and whether each registry they use answers.
Examples
Check everything under the current directory:
unripeOnly the versions a branch adds, the way a pull request check does:
unripe check --since origin/mainPick a version before adding it. Without a version, explain lists recent
releases and marks the newest one that is old enough:
$ unripe explain pypi certifi
pypi certifi: 77 versions, minimum age 3d
2026.10.19 2026-10-20 02:24 UTC 2d 8h ago too new, old enough at 2026-10-23 02:24 UTC
2026.10.18 2026-10-18 16:21 UTC 3d 19h ago old enough <- newest old enough
2026.7.22 2026-07-22 03:35 UTC 13w ago old enough
...A stricter rule for one ecosystem and a looser one for type packages, in
.unripe.toml:
min-age = "3d"
[min-age-for]
npm = "7d"
"npm:@types/*" = "1d"Your own packages are always new; skip them:
unripe check --exclude "npm:@myorg/*"A version you need now, let through until a date, with the reason in the report:
[[allow]]
package = "npm:axios@1.13.2"
reason = "fixes the redirect bug we hit in production"
until = 2026-11-15Results for a script:
unripe check --format json | jq -r '.findings[] | select(.state == "unripe") | "\(.name) \(.version)"'Quiet unless something is wrong, for a git hook or a cron job:
unripe check --quietLockfiles it reads
File | Publish times come from |
| npm registry |
| npm registry |
| npm registry |
| npm registry |
| npm registry and jsr.io |
| the lockfile's own upload times |
| the lockfile's own upload times, else PyPI |
| PyPI |
| PyPI (exact |
| crates.io sparse index |
| Go module proxy and checksum database |
| the lockfile's own |
| rubygems.org |
| Hex (hex.pm) |
| the NixOS channels for nixpkgs releases, GitHub for GitHub inputs |
| the Bazel Central Registry |
| GitHub, for the |
| nuget.org |
| Maven Central, Google's Maven repository, the Gradle plugin portal |
| pub.dev |
| the CocoaPods trunk |
| GitHub releases, for packages on GitHub |
| the Terraform registry |
| ConanCenter, by recipe revision |
| CRAN |
| GitHub releases |
Dependencies from git, a local path, a workspace or another registry are
skipped, since there's nothing public to ask about them. The old binary
bun.lockb can't be read; bun 1.2 and later write a text bun.lock.
NuGet's packages.lock.json doesn't record which feed a package came from,
so unripe looks everything up on nuget.org. Packages from a private feed come
back unknown; exclude them. For Gradle, artifacts that live in another Maven
repository (Confluent's, say) can be found by adding it to
maven-repositories in the config.
For Go it reads go.mod, not go.sum. Since Go 1.17, go.mod lists every
module in the build, while go.sum also remembers versions that aren't used.
Swift packages are dated like GitHub Actions, below, so only packages hosted on GitHub are checked. Terraform providers from other registries, including OpenTofu's, are skipped. A Conan version is dated by the recipe revision the lockfile pins, since a new revision of an old version is new code; recipes with a user and channel aren't on ConanCenter and are skipped.
In a flake.lock, nixpkgs from a NixOS channel is dated by when the channel
published that release, which is often a day after the commit. Inputs on
GitHub are dated like GitHub Actions; other hosts, plain git servers and
local paths are skipped. A MODULE.bazel.lock lists every module version
Bazel read while resolving, and Bazel uses the highest of each, so that is
the one checked; a version the registry yanked is reported as unknown. vcpkg
has no lockfile: a manifest pins a builtin-baseline, a commit of
microsoft/vcpkg that every port's version comes from. Moving the baseline is
how a project takes new versions, so unripe checks that the baseline commit
is old enough.
renv.lock packages are read from CRAN, or from a Posit Package Manager mirror
serving CRAN under /cran/, under whatever name the repository was given.
Packages from Bioconductor, GitHub, a local path or another repository are
skipped, since CRAN is the only public registry unripe can ask about them.
GitHub Actions
With --workflows (or workflows = true in the config), unripe also reads
the uses: lines in your workflows and composite actions. Each one is dated
by the GitHub release whose tag points at the same commit, so
actions/checkout@v7 and actions/checkout@3d3c42e... are both judged by
when that commit was released. A commit with no release falls back to its
commit time. An action used at a branch, like @main, has no release to go
by, so it comes back unknown. Set GITHUB_TOKEN to get past GitHub's limit
of 60 requests an hour; in the Action it is set for you.
A commit time is whatever the commit's author says it is, so a version that
passes on one is marked: by its commit time in the text report and
"dated_by": "commit" in JSON.
In GitHub Actions
name: unripe
on: pull_request
permissions:
contents: read
pull-requests: write # only needed for comment: true
jobs:
unripe:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: TexWoW/unripe@v0.10.1
with:
min-age: 3d
comment: trueOn a pull request it only looks at versions the PR adds, compared with the
base commit. It fetches that commit if the checkout is shallow, so you don't
need fetch-depth: 0. On a push it checks everything.
Each version that is too new is marked on its line in the lockfile, in the
pull request's files view. The report goes to the job summary, and with
comment: true also to a comment on the pull request, so reviewers see it
without opening the logs. That's one comment, posted when something is too
new or unknown and edited in place on later runs, so it doesn't pile up. If
the job can't comment, for example on a pull request from a fork, it says so
in the log and the check result stays the same.
Pin the action to a full commit sha rather than a tag if you can. The action runs unripe from its own checkout, so the code that runs is the commit you pinned, and nothing gets downloaded from PyPI.
Inputs:
Input | Default | |
|
|
|
| PR base | a git ref, or |
| whole repo | files or directories, space or newline separated |
| patterns, one per line | |
|
| set to |
|
| set to |
|
| also check the actions your workflows use |
|
| mark problems on the lockfile lines |
|
| write the job summary |
| also write SARIF to this path | |
|
| comment on the pull request when something is too new or unknown |
|
| the token used for that comment and for dating actions |
| a config file other than the defaults |
Outputs: result (pass, fail or error), unripe and unknown (how
many versions), and report and json (paths to the markdown and JSON
reports).
If you want the results as code scanning alerts, write SARIF and upload it
(this needs security-events: write):
- uses: TexWoW/unripe@v0.10.1
with:
sarif: unripe.sarif
- uses: github/codeql-action/upload-sarif@v4
if: always()
with:
sarif_file: unripe.sarifAs a pre-commit hook
repos:
- repo: https://github.com/TexWoW/unripe
rev: v0.10.1
hooks:
- id: unripeIt runs when a lockfile changes. Use the unripe-changed hook instead if
you only want it to look at what the commit adds.
In GitLab CI
include:
- remote: https://raw.githubusercontent.com/TexWoW/unripe/v0.10.1/ci/gitlab.ymlThat adds an unripe job to the test stage. On merge requests it checks
only the versions the merge request adds; on the default branch it checks
everything. Findings show in the merge request's code quality widget and
test report, and the markdown report is kept as a job artifact. Set
UNRIPE_MIN_AGE or UNRIPE_ARGS (for example --exclude npm:@myorg/*) in
your own variables: to change it.
In other CI systems
docs/ci.md has pipelines for Azure Pipelines, Jenkins,
CircleCI and Bitbucket. --format junit works with all of them, and
--report FORMAT:FILE writes any format to a file next to the normal
output. docs/bots.md covers Renovate and Dependabot: set
their cooldowns too, so they stop opening pull requests unripe would fail.
With AI coding agents
claude mcp add unripe -- uvx unripe@0.10.1 mcpunripe mcp is a Model Context Protocol server with three read-only tools:
check_package (how old a version is, or the newest version that is old
enough), check_lockfiles and cooldown_settings. It works with Claude
Code, Codex, Copilot in VS Code, Cursor, Claude Desktop, Windsurf and Gemini
CLI; docs/agents.md has the setup for each, and a line to
put in AGENTS.md so the agent uses it before adding a dependency.
Configuration
Put settings in .unripe.toml, or under [tool.unripe] in
pyproject.toml. unripe looks in the current directory and then each one
above it, up to the top of the git repository, or reads the file named in
UNRIPE_CONFIG. Flags on the command line win. Excludes from both are
combined.
min-age = "3d"
unknown = "fail"
paths = ["web", "api"]
maven-repositories = ["https://packages.confluent.io/maven"]
exclude = [
"npm:@myorg/*",
"left-pad@1.3.0",
]
# check workflow actions too
workflows = true
# let a version through early when a security advisory names it as the fix
allow-security-fixes = true
# ask your own registries instead of the public ones
[registries]
npm = { url = "https://artifactory.example.com/api/npm/npm-remote", token-env = "NPM_TOKEN" }
pypi = "https://pypi.example.com"
# a different minimum age for an ecosystem or some packages
[min-age-for]
npm = "7d"
"npm:@types/*" = "1d"
# one version let through early, with a reason and an end date
[[allow]]
package = "npm:axios@1.13.2"
reason = "fixes the redirect bug we hit in production"
until = 2026-11-15An exclude is a package name or a glob. Prefix it with an ecosystem
(npm:, pypi:, cargo:, go:, composer:, rubygems:, nuget:,
maven:, jsr:, actions:, pub:, cocoapods:, swift:, terraform:,
conan:, cran:, hex:, nix:, bazel:, vcpkg:) to limit it to one, and add @version to limit
it to certain versions, like @types/node@20.*. Gradle packages are named
group:artifact, like maven:com.mycorp:*. PyPI names are normalised the
way PyPI does it, so Typing_Extensions and typing-extensions are the same
package, and NuGet names ignore case.
Excludes are for packages you never want checked, like your own. For a
version you need now, use [[allow]] instead. It only lets that version
through while it is too new, it shows up in the report as allowed, and once
the until date has passed it stops applying. unripe also says when an allow
entry is no longer needed because the version has aged past the minimum.
[registries] points npm, PyPI, Conan or the Go proxy at another server,
like an Artifactory, Nexus or Verdaccio mirror. Each takes a URL, or a URL
with token-env (the name of an environment variable holding a token, sent
as a bearer token) or basic-env (one holding user:password). The secret
itself never goes in the file. It is only sent over https, only to that
URL's host and path, and never follows a redirect to another host. The
environment variables below win over the file, and credentials stay with
the URL they were set for. unripe config and unripe doctor say whether
each variable is set, without showing it.
min-age-for takes an ecosystem name or a package pattern. A package
pattern wins over an ecosystem, and the first pattern that matches is used.
allow-security-fixes asks osv.dev about each version
that is too new. If an advisory names that exact version as the fix, the
version is allowed through and the report says which advisory. A newer
version that happens to come after the fix doesn't count, and neither do
malware reports. It's off by default, since it means one more request for
each version that is too new.
unripe config shows the settings in use and where they came from. It also
reads the cooldown settings your package managers and update bots already
have (.npmrc, pnpm-workspace.yaml, .yarnrc.yml, bunfig.toml, uv,
Poetry, Deno, Cargo, Bundler, Dependabot and Renovate), says where they
disagree with unripe, and prints the lines you could add for the ones that
are missing.
Command line
unripe check [paths...]
--min-age AGE default 3d
--since REF only versions added since this git ref
--exclude PATTERN can be repeated
--unknown fail|warn default fail
--allow-security-fixes let through versions an advisory names as the fix
--workflows also check GitHub Actions in workflows
--format FORMAT text, json, sarif, markdown, github, codequality or junit
--output FILE write the report here instead of stdout
--summary FILE also append a markdown report, e.g. $GITHUB_STEP_SUMMARY
--sarif FILE also write a SARIF report
--json FILE also write a JSON report
--report FORMAT:FILE also write any other format, can be repeated
--annotate also print GitHub annotations
--as-of TIME check as if it were then, like 2025-09-08T15:00Z
--exit-zero report, but don't fail
--offline use only cached publish times
--no-suggest don't look for an older version that passes
--config FILE
--no-cache
--verbose list the versions that passed too, and the timing
--quiet print nothing when the check passes
unripe explain ECOSYSTEM NAME [VERSION]
unripe config
unripe doctor [paths...]
unripe init [--github] [--dry-run]
unripe mcpunripe with no command runs unripe check. It exits 0 when everything is
old enough, 1 when something is too new or has an unknown age, and 2 when a
lockfile can't be read, no lockfile was found, or an option is wrong. The
JSON report's fields are described in docs/json.md.
explain answers a single question:
$ unripe explain npm left-pad 1.3.0
npm left-pad 1.3.0: published 2018-04-09 01:10 UTC, 8y ago
old enough: the minimum age is 3dLeave the version out to see the recent releases and which one is the newest you could use today:
$ unripe explain composer laravel/framework
composer laravel/framework: 1303 versions, minimum age 3d, 11 prereleases not shown
12.70.1 2026-10-21 12:55 UTC 13h ago too new, old enough at 2026-10-24 12:55 UTC
13.38.0 2026-10-20 10:45 UTC 39h ago too new, old enough at 2026-10-23 10:45 UTC
13.37.0 2026-10-16 04:59 UTC 5d 21h ago old enough <- newest old enoughDetails worth knowing
If a registry can't tell unripe when a version was published, because it's
down or rate limiting or has never heard of the version, that version counts
as unknown, and unknown fails. A check that quietly passes when the registry
is unreachable is one an attacker can turn off. Use --unknown warn if you'd
rather not. A version that npm has unpublished counts as unknown too, since
that's how npm takes malicious versions down.
A publish time more than an hour in the future is not a real one, so that version counts as unknown too.
When a version is too new, unripe looks for the newest version in the same release line (same major version, or same minor below 1.0) that is old enough, skipping prereleases and anything yanked or unpublished, and preferring versions that aren't deprecated. It does this for npm, PyPI, Cargo, Composer, RubyGems, NuGet, JSR, pub.dev, CocoaPods, Terraform, CRAN and Hex.
Publish times never change, so answers are cached on disk forever, in
~/.cache/unripe (%LOCALAPPDATA%\unripe\cache on Windows), or wherever
UNRIPE_CACHE points. A second run on the same lockfiles makes no requests,
apart from finding older versions for anything that is too new, and
--offline makes none at all. Each version is looked up once even if
several lockfiles have it, lookups run 16 at a time, and each keeps its
connection to a registry open between requests. For npm,
Cargo, Composer, RubyGems, JSR, pub.dev, CocoaPods, Terraform, CRAN and Hex
one request covers every version of a package. GitHub Action tags like v7 move, so those are looked up every run.
NuGet and Maven needed care too. nuget.org reports an unlisted version as
published in 1900, so unripe reads its real time from the NuGet catalog
instead; otherwise a version unlisted the day it came out would look 126
years old. Maven repositories never change a published file, so a POM's
Last-Modified header is when the version was published, and it's there
the moment the file is, before the search index catches up.
Mirrors can also be set with UNRIPE_NPM_REGISTRY, UNRIPE_PYPI_URL and
UNRIPE_CONAN_REMOTE (any server with Conan's v2 API), and
UNRIPE_MAVEN_REPOS (comma separated) adds Maven repositories the same way
maven-repositories does. For Go, unripe uses the first proxy in GOPROXY,
then the one in [registries].
Behind a corporate proxy, HTTPS_PROXY and NO_PROXY work as usual, and a
company CA bundle can be given with SSL_CERT_FILE, REQUESTS_CA_BUNDLE or
UNRIPE_CA_BUNDLE. A certificate error says so, rather than being retried.
Go needed extra work. The module proxy only reports a version's commit
time, and a commit time is whatever the author says it is, so a tag pushed
today on an old commit (or a commit dated last year on purpose) would look
old. So unripe also asks Go's checksum database, sum.golang.org, which gives
every module version a number in the order it first saw them and can't be
rewritten. Placing that number against the timeline of the Go module index
says roughly when the version first appeared. If the commit claims to be
older than that, unripe goes by first appearance. For recent versions the
estimate is within minutes; for old ones, where it can't narrow things down,
it uses the youngest age it can't rule out. GOSUMDB, GONOSUMDB and
GOPRIVATE are honoured, and private modules fall back to the commit time.
docs/go.md explains how it works.
uv.lock, pylock.toml and composer.lock carry publish times themselves
and unripe trusts them. Someone who can edit your lockfile can change those
times, but they could also point it at any package they like, so that isn't
something unripe tries to defend against.
Next to the built-in settings
As of October 2026:
Setting | |
npm 11.10+ |
|
pnpm 10.16+ |
|
yarn 4.10+ |
|
bun 1.3+ |
|
Deno |
|
uv |
|
pip 26.1+ |
|
Poetry 2.4+ |
|
Bundler 4.0.13+ |
|
Hex 2.5.0+ |
|
Cargo |
|
Composer | proposed |
Go, NuGet, Maven, Gradle, GitHub Actions | nothing yet |
Dart, CocoaPods, Swift, Terraform, Conan, R, Nix, Bazel, vcpkg | nothing we know of |
These stop a new version at install time, which unripe can't do, so use
them where you can; unripe config prints the lines for your repo. unripe
is the check in CI afterwards: one policy for the whole repo, run against
what actually got locked. One more difference: some of the built-in
settings let a version through when they can't find its publish date.
unripe fails instead.
cooldowns.dev keeps a longer, up to date list of these settings, with examples.
Using it from Python
import unripe
for finding in unripe.scan(["."], min_age="3d", exclude=["npm:@myorg/*"]):
if finding.state != "ok":
print(finding.package, finding.state, finding.reason)Each finding's state is ok, unripe, unknown, allowed or excluded.
scan() raises unripe.check.Problem if a lockfile can't be read or the
since ref doesn't exist.
Questions
Our own packages are always new. Exclude them, e.g. npm:@myorg/*.
We need a version that came out yesterday. Add an [[allow]] entry for
that exact version with a reason and an until date. If it's a security
fix, allow-security-fixes = true handles it without an entry.
We use a private registry. Point unripe at it with [registries] in
the config, with a token from an environment variable if it needs one. A
private registry that proxies the public one answers for everything. If
yours only holds your own packages, exclude them instead.
Does this replace npm audit or similar? No. Audit tools know about
reported vulnerabilities. unripe doesn't know anything about any package. It
just won't trust a version until it has been around for a while. They catch
different things, so run both.
Can we try it without breaking builds? Use fail: false in the Action,
or --exit-zero on the command line. Everything is still reported.
Is it slow? Not very. Discourse's Gemfile.lock, Cargo's Cargo.lock
and Vite's pnpm-lock.yaml, 2,048 versions between them, took 7 seconds
with nothing cached and a third of a second after that.
More
docs/agents.md: AI coding agents and MCP
docs/ci.md: Azure Pipelines, Jenkins, CircleCI, Bitbucket
docs/bots.md: Renovate and Dependabot
docs/case-studies.md: past attacks, replayed with
--as-ofdocs/threat-model.md: what it protects against, and what it doesn't
docs/json.md: the JSON report
docs/go.md: how Go versions are dated
ROADMAP.md: what's next, and what isn't planned
Contributing
Bug reports and pull requests are welcome. CONTRIBUTING.md explains how to run the tests and how to add a new ecosystem, which is the most useful thing anyone could add right now.
License
MIT
Available Tools
3 toolscheck_lockfilesCheck lockfiles for versions that are too newARead-onlyIdempotent
Check every lockfile under a directory (package-lock.json, pnpm-lock.yaml, yarn.lock, bun.lock, deno.lock, uv.lock, poetry.lock, requirements.txt, Cargo.lock, go.mod, composer.lock, Gemfile.lock, packages.lock.json, gradle.lockfile, pubspec.lock, Podfile.lock, Package.resolved, .terraform.lock.hcl, conan.lock, renv.lock, mix.lock, flake.lock, MODULE.bazel.lock, vcpkg.json and more) and report every locked version that is younger than the minimum age or whose age is unknown. Set since to a git ref, like origin/main, to check only what changed since then.
| Name | Required | Description | Default |
|---|---|---|---|
| path | No | A directory or lockfile. Defaults to the current directory. | |
| since | No | Only check versions added since this git ref, like origin/main or HEAD. | |
| min_age | No | Override the minimum age, like 72h, 3d or 1w. Leave out to use the project's policy. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already establish read-only, idempotent, non-destructive, open-world behavior. The description adds meaningful context beyond annotations: it recursively checks every lockfile, flags versions younger than the minimum age or with unknown age, and supports git-ref-based filtering.
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 purpose is front-loaded in the first sentence, but the extensive parenthetical list of lockfile types is long and ends with 'and more,' reducing its precision. It is functional but not tightly concise.
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?
Covers the core behavior, supported file types, age threshold, unknown-age handling, and git-ref filtering. With no output schema, it could say more about report format or error behavior, but it is sufficient for an agent to invoke the tool 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%, so all three parameters are already documented. The description reinforces the meaning of 'since' and 'min_age' but adds little syntax or format detail beyond the schema baseline.
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: checks lockfiles under a directory and reports locked versions that are too new or have unknown age. The supported-file list makes the scope concrete and distinguishes it from a package-level check tool.
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?
Explains one useful invocation pattern: use 'since' with a git ref to limit checks to changed versions. However, it does not explicitly say when to use this tool versus check_package or cooldown_settings, nor does it provide when-not guidance.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
check_packageCheck a package version's ageARead-onlyIdempotent
Say when a package version was published and whether it is old enough under the project's minimum age. Leave version out to list recent releases and get the newest version that is old enough: use that version when adding or upgrading a dependency. A version that is too new comes with the newest older version that passes.
| Name | Required | Description | Default |
|---|---|---|---|
| name | Yes | The package name, exactly as the registry knows it, like left-pad, @types/node or requests. | |
| min_age | No | Override the minimum age, like 72h, 3d or 1w. Leave out to use the project's policy. | |
| version | No | The exact version. Leave out to list recent releases. | |
| ecosystem | Yes | The registry: npm (also pnpm, yarn, bun), pypi, cargo, go, composer, rubygems, nuget, maven (group:artifact), jsr, pub (Dart and Flutter), cocoapods, swift (owner/repo on GitHub), terraform (namespace/provider), conan (C and C++ from ConanCenter), cran (R packages on CRAN), hex (Elixir and Erlang), nix (owner/repo and a commit), bazel, vcpkg (microsoft/vcpkg and a baseline commit), actions (owner/repo). |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already cover safety (readOnlyHint, idempotentHint, destructiveHint false), and the description adds real behavioral detail beyond them: a too-new version returns the newest older version that passes, and omitting version switches to a release-listing mode. It does not mention registry rate limits, auth needs, or error behavior when a version doesn't exist, which keeps it below 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?
Three short sentences, purpose first, then the omit-version behavior, then the too-new fallback. Every sentence carries information, though the last sentence is slightly compressed and takes a second read to parse.
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 takes on the job of explaining returns and does so adequately: publish date, age verdict, the list of recent releases, and the fallback older version. For a four-parameter read-only tool this covers what an agent needs before calling, with only error paths 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 coverage is 100%, so the baseline is 3, but the description adds meaning the schema does not: leaving version out changes the tool from a point lookup into a listing that also picks the newest old-enough release. It also reinforces that min_age overrides project policy. That is genuine added semantics, though the ecosystem enum's alias notes remain schema-only.
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 opens with a specific verb+resource pair: it reports when a package version was published and whether it clears the project's minimum-age policy. The purpose is unambiguous, but it never contrasts itself with the sibling tools cooldown_settings or check_lockfiles, so the agent must infer the boundary from the name alone.
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 gives concrete routing guidance for two cases: omit version to list recent releases, and use the returned old-enough version when adding or upgrading a dependency. That is clear context for use, but it stops short of stating exclusions or when to reach for the sibling tools instead.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
cooldown_settingsShow cooldown settingsARead-onlyIdempotent
Show the unripe policy for a directory, the minimum release age settings its package managers and update bots already have (npm, pnpm, yarn, bun, uv, Poetry, Deno, Cargo, Bundler, Dependabot, Renovate), where they disagree, and the lines that would add the missing ones.
| Name | Required | Description | Default |
|---|---|---|---|
| path | No | The project directory. Defaults to the current directory. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnly/idempotent/non-destructive, so the bar is lower; the description still adds useful context by saying it reports where configs disagree and the lines that 'would add' the missing ones, implying it suggests rather than writes changes. It stays consistent with readOnlyHint and adds cross-tool aggregation behavior not captured elsewhere.
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?
One dense sentence, front-loaded with the primary purpose, with the return content listed afterward. The eleven-item ecosystem enumeration is somewhat long but does convey real scope.
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 carries the burden of describing what comes back — current settings, disagreements, and suggested lines — and does so adequately for a simple read-only, single-parameter tool. Only the sibling-routing context is missing.
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?
Only one optional parameter with 100% schema description coverage, so the schema already explains path and its default. The description adds nothing about path semantics, so baseline 3 applies.
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: shows the 'unripe policy' / minimum release age settings for a directory, enumerating the package managers covered. This clearly distinguishes it from check_package and check_lockfiles by content, though it never names the siblings explicitly and uses unexplained jargon ('unripe policy').
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?
Describes what the output contains but gives no when-to-use guidance, no prerequisites, and no differentiation from check_package or check_lockfiles. An agent must infer that this is the settings-audit tool rather than a package or lockfile checker.
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.
1 tool update
v0.10.1- Changed
check_package2 fields changed- changed
Input schema / properties / ecosystem / descriptionPrevious value: -"The registry: npm (also pnpm, yarn, bun), pypi, cargo, go, composer, rubygems, nuget, maven (group:artifact), jsr, pub (Dart and Flutter), cocoapods, swift (owner/repo on GitHub), terraform (namespace/provider), conan (C and C++ from ConanCenter), actions (owner/repo)."New value: +"The registry: npm (also pnpm, yarn, bun), pypi, cargo, go, composer, rubygems, nuget, maven (group:artifact), jsr, pub (Dart and Flutter), cocoapods, swift (owner/repo on GitHub), terraform (namespace/provider), conan (C and C++ from ConanCenter), cran (R packages on CRAN), hex (Elixir and Erlang), nix (owner/repo and a commit), bazel, vcpkg (microsoft/vcpkg and a baseline commit), actions (owner/repo)." - changed
Input schema / properties / ecosystem / enumPrevious value: -[ - "npm", - "pypi", - "cargo", - "go", - "composer", - "rubygems", - "nuget", - "maven", - "jsr", - "actions", - "pub", - "cocoapods", - "swift", - "terraform", - "conan" -]New value: +[ + "npm", + "pypi", + "cargo", + "go", + "composer", + "rubygems", + "nuget", + "maven", + "jsr", + "actions", + "pub", + "cocoapods", + "swift", + "terraform", + "conan", + "cran", + "hex", + "nix", + "bazel", + "vcpkg" +]
3 tool updates
v0.1.0- First observed
check_lockfiles - First observed
check_package - First observed
cooldown_settings
TDQS
Scored across 3 tools
Each tool targets a clearly different object: project configuration/policy, a single package version, and lockfiles across a directory. Their boundaries are easy to infer, and an agent should not confuse checking one package with scanning all lockfiles or reading cooldown settings.
All names use snake_case, and two follow a clear verb_noun pattern (check_package, check_lockfiles). cooldown_settings is a noun phrase rather than a verb-led name, but it remains readable and semantically clear.
Three tools is well scoped for a focused minimum-release-age auditing server. Each tool has a distinct role: inspect settings, inspect one package, and inspect lockfiles.
The server covers policy inspection and two important audit paths, but there is no remediation or bulk upgrade tool to fix lockfiles or manifests. Agents can likely work around this manually, but the lifecycle stops short of applying compliant versions.
Maintenance
Related MCP Connectors
Package intelligence for AI agents across npm, PyPI, crates.io and deps.dev. No API keys.
Package intelligence for AI agents across npm, PyPI, crates.io and deps.dev. No API keys.
Check an npm or Python package, or a package.json, for vulnerabilities and upkeep.
Abandonment checks for WordPress, npm and PyPI packages, priced per call.
Related MCP Servers
- AlicenseAqualityCmaintenanceChecks npm and PyPI packages for outdated versions, deprecation status, and breaking changes with cited sources, enabling AI agents to verify dependency freshness.14 npmISC
- AlicenseAqualityAmaintenanceEnables AI coding agents to retrieve the latest stable versions of packages and tools across multiple ecosystems, preventing outdated dependency versions in generated code.4272 PyPI9Apache 2.0
- AlicenseNot gradedqualityBmaintenanceEnables checking whether software packages are deprecated, archived, stale, active, or unknown with supporting evidence, auditing dependency manifests, and looking up runtime support and end-of-life dates.MIT
- AlicenseNot gradedqualityAmaintenanceEnables AI coding agents to fetch the real latest versions of packages from npm, PyPI, crates.io and the Go module proxy, then check manifests or whole projects for outdated dependencies and rewrite them in place. It also reports the current releases and underlying dependency stacks of AI coding tools like Claude Code, Codex and Gemini CLI.1MIT