Skip to main content
Glama
joepangallo

image-delta-mcp

image-delta-mcp — UNOFFICIAL: built as interview prep, not affiliated with or endorsed by Chainguard.

An MCP server over Chainguard's public, free-tier image data that answers, inside Claude Code:

"What changed between these two versions of this image, and what CVEs did we shed?"

It shells out to three standard supply-chain tools — crane (tags/digests/manifests), cosign (SPDX SBOM attestations), and grype (registry-direct CVE scanning) — wraps them in strict validation, rate limiting, and a digest-keyed cache, and exposes four small tools to any MCP client.

Guardrails (why this exists and what it will not do)

  • Unofficial. Interview-prep project. Not affiliated with, endorsed by, or representing Chainguard in any way.

  • Free-tier public data only. cgr.dev free-tier images, their public Sigstore attestations, and public upstream registries. No gated catalog access, no scraping.

  • Polite to registries. Every registry-touching call goes through a token bucket (4 burst, 1/s refill) and a digest-keyed on-disk cache under .cache/, so repeated queries do not hammer cgr.dev or Docker Hub.

  • Honest about gaps. Missing SBOMs, unresolvable platforms, free-tier tag limitations, and scanner-database freshness are reported in data_gaps fields — never papered over as empty-but-successful results.

Related MCP server: MCP Image Compare Server

The four tools

Tool

Question it answers

list_versions(image)

What tags exist, with digests and created dates — plus how many historical digests are publicly recoverable from signature tags.

diff_packages(image, ref_a, ref_b)

Package-level delta (added / removed / version-changed) between two refs, from the SPDX SBOM attestations attached to each digest.

diff_cves(image, ref_a, ref_b)

CVEs shed and introduced between two refs, from identical registry-direct grype scans.

compare_to_upstream(image, upstream_ref)

The money tool: a Chainguard image vs. its upstream equivalent, CVE counts side by side.

Refs are tags (latest) or digests (sha256:<64 hex>); platform defaults to linux/amd64 (linux/arm64 supported).

Real demo results (run 2026-07-21, grype DB of 2026-07-21)

These are actual outputs from npm run smoke on this machine — no numbers below are invented, and they will drift as images and the vulnerability database update.

1. "What CVEs did we shed?" — cgr.dev/chainguard/node, an older digest vs latest

Older ref sha256:0029ab60fc5a… (created 2023-01-14) → latest sha256:c002402b3552… (created 2026-07-17):

454 CVEs → 2 CVEs
447 CVE instances shed (16 Critical, 127 High, 100 Medium, 15 Low, 189 Unknown)
1 introduced (1 Medium), 1 retained
packages: 189 added, 2 removed, 17 version-changed

The older digest was recovered from public signature tags (sha256-*.sig) — the free tier exposes only rolling tags, but 7,697 historical index digests of node are publicly enumerable and diffable this way.

2. Chainguard vs upstream — cgr.dev/chainguard/nginx vs docker.io/library/nginx:latest

cgr.dev/chainguard/nginx:latest      0 CVEs
docker.io/library/nginx:latest     340 CVEs (21 Critical, 58 High, 86 Medium, 7 Low, 103 Negligible, 65 Unknown)

Same scanner, same database, same platform (linux/amd64), both registry-direct — an apples-to-apples scanner comparison, not an official vendor count.

The 3-minute demo script (inside Claude Code)

  1. "What CVEs did we shed moving chainguard/node from sha256:0029ab60fc5a… to latest?" → clean shed/introduced delta with the package changes behind it.

  2. "Compare chainguard/nginx to docker.io/library/nginx." → the zero-CVE claim reproduced live from public data by a tool an agent can call.

  3. Close: built in a day on hardened template conventions, 144 tests, externally reviewed — and an agent can call this before choosing a base image.

Architecture

Claude Code (stdio, primary)          optional: Streamable HTTP (127.0.0.1, Bearer auth)
        │                                        │
        └────────────► McpServer (4 tools, zod-validated inputs)
                           │
             validate.ts   │  tight regexes for image/tag/digest/platform,
             (before any   │  checked BEFORE anything is spawned
              spawn)       ▼
                       exec.ts ── execFile ONLY, binary allowlist {crane, cosign, grype},
                           │      per-arg character checks, hard timeouts + SIGKILL,
                           │      bounded output buffers
                           ▼
            rate-limit.ts (token bucket) ──► crane / cosign / grype ──► registries
                           │
                       cache.ts — digest-keyed on-disk cache (.cache/):
                       immutable entries for digest-addressed content
                       (manifests, configs, SBOMs), TTL entries for tag
                       lists (30m), tag→digest (15m), grype scans (24h)

Data flow for a diff: tag/digest → index digest (crane digest) → platform image digest (crane manifest) → SBOM (cosign download attestation, predicate https://spdx.dev/Document, with legacy .sbom-attachment fallback) and CVEs (grype registry:image@digest -o json) → pure diff logic (src/diff.ts).

Honest limitations

  • Free tier only sees rolling tags. latest, latest-dev, etc. Versioned tags are a paid feature; "older refs" here are historical digests recovered from public signature tags, which are unordered and must be dated individually.

  • CVE counts are scanner-relative. grype against its daily DB — not Chainguard's advisory feed, not an official count from either vendor. Numbers move as the DB updates; scans are cached up to 24h.

  • Upstream images have no SBOMs. diff_packages works where SBOM attestations exist (Chainguard images); for most upstream images it reports a data gap instead.

  • SBOM parity. Chainguard SBOMs are per-platform in-toto attestations; very old digests may predate them (legacy attachment fallback included, but some gaps remain and are reported as such).

  • Attestations are downloaded, not verified. cosign download attestation fetches; it does not verify signatures against Fulcio/Rekor. A production version would cosign verify-attestation against Chainguard's identity.

  • grype's own registry traffic is not rate-limited by this server's token bucket (it pulls layers internally). The bucket gates how often scans start; the 24h scan cache keeps repeats near zero.

Install & run

Prereqs: Node >= 20 and the three binaries:

brew install crane cosign grype

Build and test:

npm install
npm test          # 144 vitest tests — fully offline, binaries mocked
npm run build
npm run smoke     # the real pipeline against public registries (network!)

Claude Code (stdio — primary mode)

claude mcp add image-delta -- node /absolute/path/to/image-delta-mcp/dist/index.js

or in .mcp.json:

{
  "mcpServers": {
    "image-delta": {
      "command": "node",
      "args": ["/absolute/path/to/image-delta-mcp/dist/index.js"]
    }
  }
}

Optional HTTP mode (hardened, off by default)

IMAGE_DELTA_API_KEY="$(openssl rand -hex 24)" PORT=3900 node dist/index.js --http
# POST /mcp with Authorization: Bearer <key>; binds 127.0.0.1; /healthz unauthenticated

Refuses to start without a key (>= 16 chars). Timing-safe Bearer comparison, per-IP token bucket, 2 MB body cap, stateless transport.

Env: IMAGE_DELTA_CACHE_DIR overrides the cache location (default .cache/ in the package root).

Quality gate

  • 144 vitest tests (validation, exec safety, rate limiting, cache, SBOM and grype parsing, diff logic, all four tools against a mocked registry, MCP end-to-end over an in-memory transport, HTTP auth/limits integration). Tests never touch the network.

  • Integration smoke script (scripts/smoke.mjs) exercises the real pipeline; the demo numbers above are its output.

  • Adversarial security self-review + external Codex review — findings and fixes recorded in SECURITY-REVIEW.md.

F
license - not found
-
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/joepangallo/image-delta-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server