---
name: devupdate-dependency-upgrades
description: Plan and apply dependency upgrades safely using the DevUpdate.io MCP server. Use when the user asks to update/upgrade dependencies, work through available upgrades, address security updates in pinned packages, or act on DevUpdate.io findings. Requires the DevUpdate.io MCP server (https://api.devupdate.io/mcp) to be connected.
---

# Safe dependency upgrades with DevUpdate.io

DevUpdate.io analyzes every release of your monitored dependencies — diffs,
changelogs, security fixes, and undocumented behavior changes — and computes
which upgrades are available for each lockfile. Its claims are grounded in
external truth you can verify: offered versions are validated against the
package's own registry, `security` flags carry OSV/GHSA advisory ids checked
against your pinned versions (the same data `npm audit` / `pip-audit` read),
and upgrades a parent constraint pins are marked `blocked` with the
constraint that blocks them. Your job is to combine that intelligence with
the project's own tooling and tests. The MCP tools tell you *what's in an
upgrade*; only the repository tells you *whether this project survives it*.

Scope note: this skill covers *upgrades*. Security issues that no upgrade
fixes (vulnerable at latest, unreleased fix) live in `get_security_issues`
and the `devupdate-audit` / `devupdate-security-triage` skills — run the
audit skill when the ask is "are we exposed?" rather than "catch us up".

## Workflow

> **If `get_upgrades` isn't working, diagnose accurately before reporting.**
> Load it by name first — clients expose MCP tools lazily, so an unloaded
> tool looks missing but isn't. A call that *errors* ("scope does not
> allow…", expired token, plan/tier gate, transport/5xx) is the connection
> or plan: name the error and point the user at the connector's auth or
> devupdate.io/pricing. A tool that's simply *absent* is almost never a
> server fault — DevUpdate.io provides all its tools — so it's most likely
> the client rationing tools when many connectors are active; tell the user
> to run this in a session with fewer connectors enabled. Either way, stop
> and report the real reason; never substitute a hand-rolled registry audit
> that hides that the product returned nothing.

### 1. Triage

Call `get_upgrades(lockfile_id=…)` **without a `risk_level` filter** — one
call returns both a per-lockfile rollup and the full list. Read the rollup
first: upgradeable / direct vs. transitive / blocked-by-parent / major /
security / high-risk / not-monitored. The direct-vs-blocked split is your
effort estimate — blocked transitive upgrades aren't independently takeable,
so a large headline count with few direct entries is mostly parent-migration
work, not a long checklist. Pick the lockfiles you'll work on.

### 2. Read the list

The same call already listed every upgrade beneath that rollup — which is
why you skip the `risk_level` filter: a filtered list that comes back short
still sits under a rollup showing the real totals, so it can't be mistaken
for "up to date". Risk levels mean:

| Level | Meaning | Action |
|---|---|---|
| `critical` | An OSV/GHSA advisory affects the pinned version and its fix ships in the (pinned, latest] range — advisory ids included | Take first, today (via parent/override if blocked) |
| `high` | Major jump or documented breaking changes in the range | Read details before taking |
| `medium` | Unconfirmed signals: undocumented changes detected via diff, security-flavored release notes with no matching advisory, an advisory whose fix is NOT in the range, or a release scoring 40+ | Batch, verify with tests |
| `low` | Routine catch-up | Batch freely |

`critical` is verifiable: each one names its advisory ids and the fixing
version. Trust it as you would a registry audit finding — they're the same
data — and spend your skepticism on `medium` instead.

Per-upgrade annotations to read before planning:

- **`direct` / `transitive`** — direct deps are yours to bump in the
  manifest; transitive pins move only within their parents' ranges.
- **`blocked`** — the listed parent constraint excludes the offered version
  (e.g. `aioboto3` exact-pins `aiobotocore`; `next-auth` requires
  `uuid@^8.3.2`). Don't fight the resolver: upgrade the parent, or defer
  the coupled set together. The one exception is a blocked `critical` —
  deliver the fix anyway via the parent or an override/resolution.

Never conclude "up to date" from a *filtered* query that returns nothing —
re-query without the filter.

### 3. Partition into waves

- **Wave 1 — security (`critical`).** Apply each one, even when the range
  also contains breaking changes; in that case read the details first
  (step 4) and do the migration as part of the same wave. For a **blocked**
  critical, deliver the fix through the parent when a compatible parent
  release exists, otherwise add the ecosystem's override (npm `overrides`,
  yarn `resolutions`, pnpm `pnpm.overrides`, uv `override-dependencies`,
  pip constraints) — verify the overridden version still satisfies the
  dependent's usage, then run the tests.
- **Wave 2 — routine (`low` + `medium`).** Apply as one batch with the
  ecosystem's native bulk command. Skip `blocked` entries — they won't
  move; their parents are the real work item.
- **Wave 3 — careful (`high`).** Decide per package using step 4. Never
  bundle a major upgrade into a batch you can't bisect. Treat a parent
  major that unblocks many `blocked` transitives as one unit of work and
  note how many entries it clears.

### 4. Read the range before risky upgrades

For every `high` upgrade (and `critical` ones that also flag `breaking` or
`major`), call `get_upgrades(upgrade_ids=[…])` (max 5 per call). It
returns each release in the exact (pinned, latest] range with breaking
changes, security fixes, migration notes, and undocumented changes detected
from the diff. Decide from that — not from the version number alone.

**Green tests are not enough when the dependency is mocked.** If the test
suite stubs the integration (an HTTP SDK, a cloud client, an LLM API),
passing tests prove nothing about a major upgrade of it. Check how the
dependency is exercised before trusting the suite; if it's mocked, weigh the
migration notes more heavily and say so in your report.

### 5. Apply with native tooling

Use the ecosystem's own tools so resolution stays correct; DevUpdate.io never
edits your project.

- **Python (uv):** `uv lock --upgrade-package <name> …` then `uv sync`
- **Python (pip-tools/poetry):** bump in `pyproject.toml`, re-lock
- **npm:** `npm update` for in-range; `npm install <name>@<version>` for
  targeted bumps. (`npm outdated` shows "Wanted" vs "Latest" — semver-range
  ceilings, not safety.)
- **Cargo / Composer / Go:** `cargo update -p`, `composer update <name>`,
  `go get <module>@<version>`

Respect **coupled pins**: some packages exact-pin siblings (e.g.
`aiobotocore` pins `botocore`/`boto3`; framework families like
`next`/`eslint-config-next` move together). The `blocked` annotation and its
constraint tell you which parent gates each pin — upgrade coupled sets
together or defer them together, rather than discovering the coupling from
resolver errors.

### 6. Verify

Install, then run the project's test suite, lint, and typecheck. If a wave
fails, bisect within the wave rather than reverting everything.

### 7. Close the loop

- **Don't dismiss what you upgraded.** The next lockfile sync auto-resolves
  taken upgrades.
- **Do dismiss what you deferred**: `dismiss_upgrades(upgrade_ids=[…])` for
  each upgrade you deliberately decided not to take, so the next session
  starts from a clean list. A dismissal auto-clears when a newer version
  appears.
- **Cover the gap**: if the rollup showed *not monitored* dependencies,
  DevUpdate.io has no data for them — run the registry audit
  (`npm audit`, `pip-audit`, `cargo audit`) **scoped to that gap** and
  handle its findings separately. For *monitored* dependencies the
  `security` flag is already OSV/GHSA-verified against your pins — the same
  database the audit reads — so a full re-audit should only confirm it; if
  the two ever disagree, say so explicitly in your report. Beware
  `npm audit fix --force`: it may apply breaking *downgrades*; never accept
  one without checking what it changes.

### 8. Report

Summarize per wave: what was taken (call out the security fixes by package
and advisory id — they're in the upgrade data), what was deferred and *why*
(with the dismissed ids and, for blocked entries, the parent that gates
them), and what's unmonitored or otherwise out of scope. Flag any upgrade
you took on weak evidence (mocked integration, sparse tests).

## Pitfalls

- A filtered `get_upgrades` list that returns nothing is **not** "up to
  date" — the rollup above it shows the real totals; re-read them.
- Don't treat the risk level as the whole story — the flags
  (`major`/`security`/`breaking`/`blocked`), the advisory ids, and
  `get_upgrades(upgrade_ids=…)` detail are what justify a decision.
- Don't batch `blocked` upgrades — the resolver will no-op or fight you;
  their parents are the actionable unit.
- Upgrade ids change as upgrades resolve; don't reuse ids across sessions.
- DevUpdate.io's view lags the registry by up to one sync cycle; for a
  just-published fix, cross-check the registry directly.
