From c078ecd0f460b992727c0cf919e8c7b618eba0dc Mon Sep 17 00:00:00 2001 From: Christian Andersson Date: Mon, 26 Jan 2026 12:49:19 +0100 Subject: [PATCH] Add SHA256 digest support for Docker images Docker tags are NOT immutable - they can be moved to point to different images at any time. This creates a supply chain attack vector. Changes: - Add digest, secureReference, and securityNotes fields to VersionInfo - Add digest field to VersionDetail for list_versions output - Update Docker client to return SHA256 digests from Docker Hub API - Include security warnings explaining tag mutability risks - Document digest-pinned references in README security section The secureReference field provides ready-to-use digest-pinned format: nginx@sha256:1948e0c46da16a3565a844aa65ab848e1546f85cf47e47d044a567906a3a497f Co-Authored-By: Claude Opus 4.5 --- README.md | 224 ++++++++++++++++++++++++------------ src/registries/docker.ts | 75 ++++++++---- src/registries/types.ts | 20 +++- src/tools/list-versions.ts | 25 +++- src/tools/lookup-version.ts | 36 ++++-- 5 files changed, 273 insertions(+), 107 deletions(-) diff --git a/README.md b/README.md index 9564411..177ca04 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,71 @@ # MCP Dependency Version -A Model Context Protocol (MCP) server for looking up package versions across multiple package registries. +A Model Context Protocol (MCP) server for looking up package versions across +multiple package registries. ## Features -- **Multi-registry support**: npm, Maven Central, PyPI, crates.io, Go proxy, JSR, NuGet, Docker Hub +- **Multi-registry support**: npm, Maven Central, PyPI, crates.io, Go proxy, + JSR, NuGet, Docker Hub - **Version lookup**: Get the latest stable (and optionally prerelease) versions - **Version listing**: List all available versions with metadata -- **Vulnerability scanning**: Check packages against the OSV (Open Source Vulnerabilities) database +- **Vulnerability scanning**: Check packages against the OSV (Open Source + Vulnerabilities) database - **Dependency analysis**: Analyze dependency files and check for updates -- **Docker support**: Look up image tags and analyze Dockerfile/docker-compose.yml dependencies +- **Docker support**: Look up image tags and analyze + Dockerfile/docker-compose.yml dependencies ## Security: Use Exact Versions -**Always use exact versions instead of version ranges to prevent supply chain attacks.** +**Always use exact versions instead of version ranges to prevent supply chain +attacks.** | Bad (vulnerable) | Good (secure) | -|------------------|---------------| -| `^1.2.3` | `1.2.3` | -| `~1.2.3` | `1.2.3` | -| `>=1.2.3` | `1.2.3` | -| `1.x` | `1.2.3` | +| ---------------- | ------------- | +| `^1.2.3` | `1.2.3` | +| `~1.2.3` | `1.2.3` | +| `>=1.2.3` | `1.2.3` | +| `1.x` | `1.2.3` | -Version ranges (like `^1.2.3` or `~1.2.3`) allow automatic updates when new minor or patch versions are published. If an attacker compromises a package and publishes a malicious version, your project could automatically pull it in without your knowledge. +Version ranges (like `^1.2.3` or `~1.2.3`) allow automatic updates when new +minor or patch versions are published. If an attacker compromises a package and +publishes a malicious version, your project could automatically pull it in +without your knowledge. -Using exact versions ensures you control exactly which code runs in your project. When you want to update, explicitly change the version and review the changes. +Using exact versions ensures you control exactly which code runs in your +project. When you want to update, explicitly change the version and review the +changes. + +### Docker: Use Digest-Pinned References + +**Docker tags are NOT immutable.** Unlike package versions in npm/PyPI/etc., a +Docker tag can be moved to point to a completely different image at any time. + +| Bad (vulnerable) | Good (secure) | +| ---------------- | --------------------------- | +| `nginx:1.27.3` | `nginx@sha256:1948e0c46...` | +| `postgres:16` | `postgres@sha256:abc123...` | + +When you use `nginx:1.27.3`, the image you pull today may be different from the +one you pull tomorrow if the tag is updated. This creates a supply chain attack +vector. + +**Use digest-pinned references** (`image@sha256:...`) to ensure you always pull +the exact same image. The `lookup_version` and `list_versions` tools return the +`digest` and `secureReference` fields for Docker images to make this easy. ## Supported Registries -| Registry | API Endpoint | Package Format | -|----------|--------------|----------------| -| npm | registry.npmjs.org | `package-name`, `@scope/package` | -| maven | repo1.maven.org/maven2 | `groupId:artifactId` | -| pypi | pypi.org | `package-name` | -| cargo | crates.io | `crate-name` | -| go | proxy.golang.org | `github.com/user/repo` | -| jsr | api.jsr.io | `@scope/name` | -| nuget | api.nuget.org | `Package.Name` | -| docker | hub.docker.com | `image`, `user/image` | +| Registry | API Endpoint | Package Format | +| -------- | ---------------------- | -------------------------------- | +| npm | registry.npmjs.org | `package-name`, `@scope/package` | +| maven | repo1.maven.org/maven2 | `groupId:artifactId` | +| pypi | pypi.org | `package-name` | +| cargo | crates.io | `crate-name` | +| go | proxy.golang.org | `github.com/user/repo` | +| jsr | api.jsr.io | `@scope/name` | +| nuget | api.nuget.org | `Package.Name` | +| docker | hub.docker.com | `image`, `user/image` | ## Installation @@ -47,7 +75,9 @@ Using exact versions ensures you control exactly which code runs in your project ### Setup with Claude Desktop -Add to your Claude Desktop configuration file (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS, `~/.config/claude-desktop/claude_desktop_config.json` on Linux): +Add to your Claude Desktop configuration file +(`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS, +`~/.config/claude-desktop/claude_desktop_config.json` on Linux): ```json { @@ -121,7 +151,9 @@ docker run --rm -i ghcr.io/tripletex/mcp-dependency-version:latest ## Configuration -The server supports custom repository configurations for each registry type. This allows you to use private registries, mirrors, or multiple repositories per registry. +The server supports custom repository configurations for each registry type. +This allows you to use private registries, mirrors, or multiple repositories per +registry. ### Configuration File @@ -180,7 +212,8 @@ Create a configuration file at `~/.config/mcp-dependency-version/config.json`: ### Environment Variable -You can override the config file path using the `MCP_DEPENDENCY_VERSION_CONFIG` environment variable: +You can override the config file path using the `MCP_DEPENDENCY_VERSION_CONFIG` +environment variable: ```bash export MCP_DEPENDENCY_VERSION_CONFIG=/path/to/config.json @@ -191,6 +224,7 @@ export MCP_DEPENDENCY_VERSION_CONFIG=/path/to/config.json The configuration supports two authentication methods: **Bearer Token:** + ```json { "auth": { @@ -200,6 +234,7 @@ The configuration supports two authentication methods: ``` **Basic Auth:** + ```json { "auth": { @@ -213,16 +248,16 @@ The configuration supports two authentication methods: If no configuration file exists, the server uses the official public registries: -| Registry | Default URL | -|----------|-------------| -| npm | https://registry.npmjs.org | -| maven | https://repo1.maven.org/maven2 | -| pypi | https://pypi.org/pypi | -| cargo | https://crates.io/api/v1/crates | -| go | https://proxy.golang.org | -| jsr | https://api.jsr.io | -| nuget | https://api.nuget.org/v3 | -| docker | https://hub.docker.com | +| Registry | Default URL | +| -------- | ------------------------------- | +| npm | https://registry.npmjs.org | +| maven | https://repo1.maven.org/maven2 | +| pypi | https://pypi.org/pypi | +| cargo | https://crates.io/api/v1/crates | +| go | https://proxy.golang.org | +| jsr | https://api.jsr.io | +| nuget | https://api.nuget.org/v3 | +| docker | https://hub.docker.com | ## Tools @@ -231,12 +266,15 @@ If no configuration file exists, the server uses the official public registries: Look up the latest version of a package. **Parameters:** -- `registry` (required): Package registry (`npm`, `maven`, `pypi`, `cargo`, `go`, `jsr`, `nuget`, `docker`) + +- `registry` (required): Package registry (`npm`, `maven`, `pypi`, `cargo`, + `go`, `jsr`, `nuget`, `docker`) - `package` (required): Package name - `includePrerelease` (optional): Include alpha/beta/rc versions - `versionPrefix` (optional): Filter versions by prefix (e.g., `"2."` for 2.x) **Example:** + ```json { "registry": "npm", @@ -245,6 +283,7 @@ Look up the latest version of a package. ``` **Output:** + ```json { "packageName": "lodash", @@ -254,16 +293,37 @@ Look up the latest version of a package. } ``` +**Docker Output (includes digest for secure pinning):** + +```json +{ + "packageName": "nginx", + "registry": "docker", + "latestStable": "1.27.3", + "publishedAt": "2024-12-04T18:51:59.819Z", + "digest": "sha256:1948e0c46da16a3565a844aa65ab848e1546f85cf47e47d044a567906a3a497f", + "secureReference": "nginx@sha256:1948e0c46da16a3565a844aa65ab848e1546f85cf47e47d044a567906a3a497f", + "securityNotes": [ + "WARNING: Docker tags are NOT immutable. A tag can be moved to point to a different image at any time.", + "Using the digest-pinned reference (image@sha256:...) provides protection against tag tampering.", + "Digest-pinned references ensure you always pull the exact same image, preventing supply chain attacks.", + "When updating, explicitly change the digest and verify the new image before deployment." + ] +} +``` + ### list_versions List all available versions of a package. **Parameters:** + - `registry` (required): Package registry - `package` (required): Package name - `limit` (optional): Maximum versions to return (default: 20) **Example:** + ```json { "registry": "pypi", @@ -273,6 +333,7 @@ List all available versions of a package. ``` **Output:** + ```json { "packageName": "requests", @@ -295,12 +356,15 @@ List all available versions of a package. Check a package version for known security vulnerabilities. **Parameters:** + - `registry` (required): Package registry - `package` (required): Package name - `version` (required): Version to check -- `severityThreshold` (optional): Minimum severity (`LOW`, `MEDIUM`, `HIGH`, `CRITICAL`) +- `severityThreshold` (optional): Minimum severity (`LOW`, `MEDIUM`, `HIGH`, + `CRITICAL`) **Example:** + ```json { "registry": "npm", @@ -310,6 +374,7 @@ Check a package version for known security vulnerabilities. ``` **Output:** + ```json { "packageName": "lodash", @@ -340,26 +405,32 @@ Check a package version for known security vulnerabilities. Analyze a dependency file and check for available updates. **Parameters:** -- `content` (required): File content (package.json, pom.xml, build.gradle, build.gradle.kts, requirements.txt, Cargo.toml, go.mod, deno.json, *.csproj) + +- `content` (required): File content (package.json, pom.xml, build.gradle, + build.gradle.kts, requirements.txt, Cargo.toml, go.mod, deno.json, *.csproj) - `registry` (required): Package registry (use `maven` for Gradle files) -- `checkVulnerabilities` (optional): Also scan for vulnerabilities (default: false) +- `checkVulnerabilities` (optional): Also scan for vulnerabilities (default: + false) **Supported Dependency Files:** -| Registry | File Formats | -|----------|--------------| -| npm | `package.json` | -| maven | `pom.xml`, `build.gradle` (Groovy), `build.gradle.kts` (Kotlin) | -| pypi | `requirements.txt` | -| cargo | `Cargo.toml` | -| go | `go.mod` | -| jsr | `deno.json` (supports jsr: and npm: imports) | -| nuget | `*.csproj` (PackageReference format) | -| docker | `Dockerfile`, `docker-compose.yml` | - -**Note:** For Gradle files, variable references (`$version`, `${libs.xxx}`, version catalogs) are skipped since they can't be resolved without evaluating the build. +| Registry | File Formats | +| -------- | --------------------------------------------------------------- | +| npm | `package.json` | +| maven | `pom.xml`, `build.gradle` (Groovy), `build.gradle.kts` (Kotlin) | +| pypi | `requirements.txt` | +| cargo | `Cargo.toml` | +| go | `go.mod` | +| jsr | `deno.json` (supports jsr: and npm: imports) | +| nuget | `*.csproj` (PackageReference format) | +| docker | `Dockerfile`, `docker-compose.yml` | + +**Note:** For Gradle files, variable references (`$version`, `${libs.xxx}`, +version catalogs) are skipped since they can't be resolved without evaluating +the build. **Example (npm):** + ```json { "content": "{\"dependencies\": {\"lodash\": \"^4.17.20\", \"express\": \"^4.18.0\"}}", @@ -369,6 +440,7 @@ Analyze a dependency file and check for available updates. ``` **Example (Gradle Kotlin DSL):** + ```json { "content": "dependencies {\n implementation(\"org.springframework.boot:spring-boot-starter:3.2.0\")\n testImplementation(\"org.junit.jupiter:junit-jupiter:5.10.0\")\n}", @@ -377,6 +449,7 @@ Analyze a dependency file and check for available updates. ``` **Output:** + ```json { "registry": "npm", @@ -417,23 +490,27 @@ Analyze a dependency file and check for available updates. Get README documentation for a package. **Parameters:** -- `registry` (required): Package registry (`npm`, `maven`, `pypi`, `cargo`, `go`, `jsr`, `nuget`, `docker`) + +- `registry` (required): Package registry (`npm`, `maven`, `pypi`, `cargo`, + `go`, `jsr`, `nuget`, `docker`) - `package` (required): Package name - `version` (optional): Specific version to get documentation for **Documentation Sources:** -| Registry | README Source | Repository URL Source | -|----------|---------------|----------------------| -| npm | Registry API | `repository` field | -| pypi | Registry API (description) | `project_urls` field | -| cargo | Registry API | `repository` field | -| maven | GitHub (fallback) | POM `` section | -| go | GitHub (fallback) | Module path (if github.com) | -| jsr | GitHub (fallback) | `githubRepository` field | -| nuget | GitHub (fallback) | Catalog entry | -| docker | GitHub (fallback) | Docker Hub page | + +| Registry | README Source | Repository URL Source | +| -------- | -------------------------- | --------------------------- | +| npm | Registry API | `repository` field | +| pypi | Registry API (description) | `project_urls` field | +| cargo | Registry API | `repository` field | +| maven | GitHub (fallback) | POM `` section | +| go | GitHub (fallback) | Module path (if github.com) | +| jsr | GitHub (fallback) | `githubRepository` field | +| nuget | GitHub (fallback) | Catalog entry | +| docker | GitHub (fallback) | Docker Hub page | **Example:** + ```json { "registry": "npm", @@ -442,6 +519,7 @@ Get README documentation for a package. ``` **Output:** + ``` # lodash Documentation Registry: npm @@ -539,17 +617,17 @@ mcp-dependency-version/ ### Registry APIs -| Registry | API Endpoint | Documentation | -|----------|--------------|---------------| -| npm | `registry.npmjs.org/{package}` | [docs](https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md) | -| Maven | `repo1.maven.org/maven2` | [docs](https://central.sonatype.com/search) | -| PyPI | `pypi.org/pypi/{package}/json` | [docs](https://warehouse.pypa.io/api-reference/json.html) | -| Cargo | `crates.io/api/v1/crates/{crate}` | [docs](https://crates.io/data-access) | -| Go | `proxy.golang.org/{module}/@v/list` | [docs](https://go.dev/ref/mod#goproxy-protocol) | -| JSR | `api.jsr.io/scopes/{scope}/packages/{name}` | [docs](https://jsr.io/docs/api) | -| NuGet | `api.nuget.org/v3-flatcontainer/{id}/index.json` | [docs](https://learn.microsoft.com/en-us/nuget/api/overview) | -| Docker | `hub.docker.com/v2/repositories/{image}/tags` | [docs](https://docs.docker.com/docker-hub/api/latest/) | -| OSV | `api.osv.dev/v1/query` | [docs](https://osv.dev/docs/) | +| Registry | API Endpoint | Documentation | +| -------- | ------------------------------------------------ | ------------------------------------------------------------------------ | +| npm | `registry.npmjs.org/{package}` | [docs](https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md) | +| Maven | `repo1.maven.org/maven2` | [docs](https://central.sonatype.com/search) | +| PyPI | `pypi.org/pypi/{package}/json` | [docs](https://warehouse.pypa.io/api-reference/json.html) | +| Cargo | `crates.io/api/v1/crates/{crate}` | [docs](https://crates.io/data-access) | +| Go | `proxy.golang.org/{module}/@v/list` | [docs](https://go.dev/ref/mod#goproxy-protocol) | +| JSR | `api.jsr.io/scopes/{scope}/packages/{name}` | [docs](https://jsr.io/docs/api) | +| NuGet | `api.nuget.org/v3-flatcontainer/{id}/index.json` | [docs](https://learn.microsoft.com/en-us/nuget/api/overview) | +| Docker | `hub.docker.com/v2/repositories/{image}/tags` | [docs](https://docs.docker.com/docker-hub/api/latest/) | +| OSV | `api.osv.dev/v1/query` | [docs](https://osv.dev/docs/) | ## License diff --git a/src/registries/docker.ts b/src/registries/docker.ts index 5a6aefb..2e58908 100644 --- a/src/registries/docker.ts +++ b/src/registries/docker.ts @@ -5,20 +5,20 @@ */ import type { + LookupOptions, + PackageMetadata, Registry, RegistryClient, - VersionInfo, VersionDetail, - PackageMetadata, - LookupOptions, + VersionInfo, } from "./types.ts"; import { - isPrerelease, - sortVersionsDescending, - findLatestStable, - findLatestPrerelease, filterByPrefix, + findLatestPrerelease, + findLatestStable, + isPrerelease, parseVersion, + sortVersionsDescending, } from "../utils/version.ts"; import { versionCache } from "../utils/cache.ts"; import { fetchWithHeaders } from "../utils/http.ts"; @@ -59,6 +59,19 @@ interface ParsedImageName { fullName: string; } +/** + * Generate security notes for Docker images + * Explains that Docker tags are NOT immutable and recommends digest-pinned references + */ +function generateDockerSecurityNotes(): string[] { + return [ + "WARNING: Docker tags are NOT immutable. A tag can be moved to point to a different image at any time.", + "Using the digest-pinned reference (image@sha256:...) provides protection against tag tampering.", + "Digest-pinned references ensure you always pull the exact same image, preventing supply chain attacks.", + "When updating, explicitly change the digest and verify the new image before deployment.", + ]; +} + export class DockerClient implements RegistryClient { readonly registry = "docker" as const satisfies Registry; @@ -114,7 +127,8 @@ export class DockerClient implements RegistryClient { // Check for version-variant patterns like "1.0-alpine" const versionPart = tag.split("-")[0]; - return parseVersion(versionPart) !== null || /^\d+(\.\d+)*$/.test(versionPart); + return parseVersion(versionPart) !== null || + /^\d+(\.\d+)*$/.test(versionPart); } /** @@ -148,7 +162,7 @@ export class DockerClient implements RegistryClient { private async fetchTags( imageName: string, repositoryName?: string, - limit = 100 + limit = 100, ): Promise<{ tags: DockerHubTagResult[]; total: number }> { const repoConfig = getRepositoryConfig("docker", repositoryName); const parsed = this.parseImageName(imageName); @@ -160,7 +174,10 @@ export class DockerClient implements RegistryClient { } // Docker Hub API for tags - const url = `${repoConfig.url}/v2/repositories/${parsed.fullName}/tags?page_size=${Math.min(limit, 100)}`; + const url = + `${repoConfig.url}/v2/repositories/${parsed.fullName}/tags?page_size=${ + Math.min(limit, 100) + }`; const response = await fetchWithHeaders(url, { auth: repoConfig.auth }); if (!response.ok) { @@ -168,7 +185,7 @@ export class DockerClient implements RegistryClient { throw new Error(`Image '${imageName}' not found on ${repoConfig.name}`); } throw new Error( - `${repoConfig.name} error: ${response.status} ${response.statusText}` + `${repoConfig.name} error: ${response.status} ${response.statusText}`, ); } @@ -180,7 +197,7 @@ export class DockerClient implements RegistryClient { private async fetchRepoInfo( imageName: string, - repositoryName?: string + repositoryName?: string, ): Promise { const repoConfig = getRepositoryConfig("docker", repositoryName); const parsed = this.parseImageName(imageName); @@ -199,7 +216,7 @@ export class DockerClient implements RegistryClient { throw new Error(`Image '${imageName}' not found on ${repoConfig.name}`); } throw new Error( - `${repoConfig.name} error: ${response.status} ${response.statusText}` + `${repoConfig.name} error: ${response.status} ${response.statusText}`, ); } @@ -210,7 +227,7 @@ export class DockerClient implements RegistryClient { async lookupVersion( imageName: string, - options?: LookupOptions & { repository?: string } + options?: LookupOptions & { repository?: string }, ): Promise { const { tags } = await this.fetchTags(imageName, options?.repository, 100); @@ -246,15 +263,27 @@ export class DockerClient implements RegistryClient { if (!latestStable) { throw new Error( - `No stable version found for '${imageName}'${options?.versionPrefix ? ` with prefix '${options.versionPrefix}'` : ""}` + `No stable version found for '${imageName}'${ + options?.versionPrefix + ? ` with prefix '${options.versionPrefix}'` + : "" + }`, ); } // Find the tag data for the latest stable const latestTagData = tags.find( - (t) => t.name === latestStable || this.getBaseVersion(t.name) === latestStable + (t) => + t.name === latestStable || this.getBaseVersion(t.name) === latestStable, ); + // Get digest for secure reference + const digest = latestTagData?.digest; + const parsed = this.parseImageName(imageName); + const displayName = parsed.namespace === "library" + ? parsed.repository + : `${parsed.namespace}/${parsed.repository}`; + const result: VersionInfo = { packageName: imageName, registry: "docker", @@ -263,6 +292,9 @@ export class DockerClient implements RegistryClient { ? new Date(latestTagData.last_updated) : undefined, deprecated: latestTagData?.tag_status === "stale", + digest, + secureReference: digest ? `${displayName}@${digest}` : undefined, + securityNotes: generateDockerSecurityNotes(), }; // Include latest prerelease if requested @@ -283,7 +315,7 @@ export class DockerClient implements RegistryClient { async listVersions( imageName: string, - options?: { repository?: string } + options?: { repository?: string }, ): Promise { const { tags } = await this.fetchTags(imageName, options?.repository, 100); @@ -293,7 +325,7 @@ export class DockerClient implements RegistryClient { // Sort semver tags by version const sortedSemver = sortVersionsDescending( - semverTags.map((t) => t.name) + semverTags.map((t) => t.name), ).map((name) => tags.find((t) => t.name === name)!); // Sort non-semver tags by date @@ -311,13 +343,14 @@ export class DockerClient implements RegistryClient { publishedAt: tag.last_updated ? new Date(tag.last_updated) : undefined, isPrerelease: isPrerelease(tag.name), isDeprecated: tag.tag_status === "stale", + digest: tag.digest, })); } async getMetadata( imageName: string, _version?: string, - options?: { repository?: string } + options?: { repository?: string }, ): Promise { const repoInfo = await this.fetchRepoInfo(imageName, options?.repository); const parsed = this.parseImageName(imageName); @@ -326,7 +359,9 @@ export class DockerClient implements RegistryClient { name: imageName, registry: "docker", description: repoInfo.description || undefined, - homepage: `https://hub.docker.com/${parsed.namespace === "library" ? "_" : "r"}/${parsed.fullName}`, + homepage: `https://hub.docker.com/${ + parsed.namespace === "library" ? "_" : "r" + }/${parsed.fullName}`, repository: undefined, // Docker Hub doesn't expose source repository in API }; } diff --git a/src/registries/types.ts b/src/registries/types.ts index 36eb325..df7a9ae 100644 --- a/src/registries/types.ts +++ b/src/registries/types.ts @@ -1,7 +1,15 @@ /** * Supported package registries */ -export type Registry = "npm" | "maven" | "pypi" | "cargo" | "go" | "jsr" | "nuget" | "docker"; +export type Registry = + | "npm" + | "maven" + | "pypi" + | "cargo" + | "go" + | "jsr" + | "nuget" + | "docker"; /** * Version information for a package @@ -14,6 +22,12 @@ export interface VersionInfo { publishedAt?: Date; deprecated?: boolean; deprecationMessage?: string; + /** SHA256 digest for Docker images - provides immutable reference */ + digest?: string; + /** Secure reference using digest (e.g., nginx@sha256:abc123...) */ + secureReference?: string; + /** Security notes about tag mutability and recommended practices */ + securityNotes?: string[]; } /** @@ -25,6 +39,8 @@ export interface VersionDetail { isPrerelease: boolean; isDeprecated: boolean; yanked?: boolean; + /** SHA256 digest for Docker images - provides immutable reference */ + digest?: string; } /** @@ -54,7 +70,7 @@ export interface RegistryClient { readonly registry: Registry; lookupVersion( packageName: string, - options?: LookupOptions + options?: LookupOptions, ): Promise; listVersions(packageName: string): Promise; getMetadata(packageName: string, version?: string): Promise; diff --git a/src/tools/list-versions.ts b/src/tools/list-versions.ts index cb6bbdd..80d1343 100644 --- a/src/tools/list-versions.ts +++ b/src/tools/list-versions.ts @@ -9,14 +9,23 @@ import { getClient, supportedRegistries } from "../registries/index.ts"; import type { Registry } from "../registries/types.ts"; const inputSchema = z.object({ - registry: z.enum(["npm", "maven", "pypi", "cargo", "go", "jsr", "nuget", "docker"]).describe( - "Package registry (npm, maven, pypi, cargo, go, jsr, nuget, docker)" + registry: z.enum([ + "npm", + "maven", + "pypi", + "cargo", + "go", + "jsr", + "nuget", + "docker", + ]).describe( + "Package registry (npm, maven, pypi, cargo, go, jsr, nuget, docker)", ), package: z.string().describe( - "Package name. Maven uses groupId:artifactId format, Go uses full module path, JSR uses @scope/name, Docker uses image name (nginx, user/repo)" + "Package name. Maven uses groupId:artifactId format, Go uses full module path, JSR uses @scope/name, Docker uses image name (nginx, user/repo)", ), limit: z.number().optional().default(20).describe( - "Maximum number of versions to return (default: 20)" + "Maximum number of versions to return (default: 20)", ), }); @@ -51,9 +60,15 @@ Supported registries: ${supportedRegistries.join(", ")}`, isPrerelease: v.isPrerelease, isDeprecated: v.isDeprecated, ...(v.yanked !== undefined && { yanked: v.yanked }), + ...(v.digest !== undefined && { digest: v.digest }), })), totalCount: versions.length, showing: limitedVersions.length, + // Add security note for Docker registry + ...(registry === "docker" && { + securityNote: + "Docker tags are NOT immutable. Use digest-pinned references (image@sha256:...) for supply chain security.", + }), }; return { @@ -76,6 +91,6 @@ Supported registries: ${supportedRegistries.join(", ")}`, isError: true, }; } - } + }, ); } diff --git a/src/tools/lookup-version.ts b/src/tools/lookup-version.ts index 762173a..e8b883f 100644 --- a/src/tools/lookup-version.ts +++ b/src/tools/lookup-version.ts @@ -9,17 +9,26 @@ import { getClient, supportedRegistries } from "../registries/index.ts"; import type { Registry } from "../registries/types.ts"; const inputSchema = z.object({ - registry: z.enum(["npm", "maven", "pypi", "cargo", "go", "jsr", "nuget", "docker"]).describe( - "Package registry (npm, maven, pypi, cargo, go, jsr, nuget, docker)" + registry: z.enum([ + "npm", + "maven", + "pypi", + "cargo", + "go", + "jsr", + "nuget", + "docker", + ]).describe( + "Package registry (npm, maven, pypi, cargo, go, jsr, nuget, docker)", ), package: z.string().describe( - "Package name. Maven uses groupId:artifactId format, Go uses full module path, JSR uses @scope/name, Docker uses image name (nginx, user/repo)" + "Package name. Maven uses groupId:artifactId format, Go uses full module path, JSR uses @scope/name, Docker uses image name (nginx, user/repo)", ), includePrerelease: z.boolean().optional().describe( - "Include alpha/beta/rc versions in results" + "Include alpha/beta/rc versions in results", ), versionPrefix: z.string().optional().describe( - 'Filter versions by prefix (e.g., "2." for 2.x versions)' + 'Filter versions by prefix (e.g., "2." for 2.x versions)', ), }); @@ -42,7 +51,9 @@ Examples: SECURITY: Always use exact versions (e.g., "1.2.3") instead of ranges (e.g., "^1.2.3" or "~1.2.3") to prevent dependency supply chain attacks.`, inputSchema.shape, - async ({ registry, package: packageName, includePrerelease, versionPrefix }) => { + async ( + { registry, package: packageName, includePrerelease, versionPrefix }, + ) => { try { const client = getClient(registry as Registry); const result = await client.lookupVersion(packageName, { @@ -69,6 +80,17 @@ SECURITY: Always use exact versions (e.g., "1.2.3") instead of ranges (e.g., "^1 } } + // Include Docker-specific digest and security info + if (result.digest) { + output.digest = result.digest; + } + if (result.secureReference) { + output.secureReference = result.secureReference; + } + if (result.securityNotes && result.securityNotes.length > 0) { + output.securityNotes = result.securityNotes; + } + return { content: [ { @@ -89,6 +111,6 @@ SECURITY: Always use exact versions (e.g., "1.2.3") instead of ranges (e.g., "^1 isError: true, }; } - } + }, ); }