image-delta-mcp
Enables comparing container image versions to see package and CVE changes, including shedding/introductions, and comparing Chainguard images to upstream.
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.devfree-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_gapsfields — 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)
- "What CVEs did we shed moving chainguard/node from
sha256:0029ab60fc5a…to latest?" → clean shed/introduced delta with the package changes behind it. - "Compare chainguard/nginx to docker.io/library/nginx." → the zero-CVE claim reproduced live from public data by a tool an agent can call.
- 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_packagesworks 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 attestationfetches; it does not verify signatures against Fulcio/Rekor. A production version wouldcosign verify-attestationagainst 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
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
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.
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.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
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.
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.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
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.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.