image-delta-mcp

image-delta-mcp

Enables comparing container image versions to see package and CVE changes, including shedding/introductions, and comparing Chainguard images to upstream.

Category
Visit Server

README

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.

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.

Recommended Servers

playwright-mcp

playwright-mcp

A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.

Official
Featured
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.

Official
Featured
Local
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
graphlit-mcp-server

graphlit-mcp-server

The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.

Official
Featured
TypeScript
Kagi MCP Server

Kagi MCP Server

An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.

Official
Featured
Python
E2B

E2B

Using MCP to run code via e2b.

Official
Featured
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
Exa Search

Exa Search

A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured