UNPKG

mcp-package-version

Version:

An MCP server to provide LLMs the latest (stable) version of packages in package.json and requirements.txt files

185 lines (184 loc) 8.68 kB
import axios from 'axios'; import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js'; export class DockerHandler { dockerHubRegistry = 'https://registry.hub.docker.com/v2'; githubContainerRegistry = 'https://ghcr.io/v2'; async getDockerHubToken(repository) { try { const response = await axios.get(`https://auth.docker.io/token?service=registry.docker.io&scope=repository:${repository}:pull`); return response.data.token; } catch (error) { console.error(`Error getting Docker Hub token for ${repository}:`, error); throw new McpError(ErrorCode.InternalError, `Failed to authenticate with Docker Hub for ${repository}`); } } async getGithubContainerRegistryToken(repository) { // GitHub Container Registry can be accessed anonymously for public images // For private images, a token would be needed from environment variables const token = process.env.GITHUB_TOKEN; return token || null; } async getCustomRegistryToken(registry, repository) { // For custom registries, authentication would typically be provided via environment variables // This is a simplified implementation const username = process.env.CUSTOM_REGISTRY_USERNAME; const password = process.env.CUSTOM_REGISTRY_PASSWORD; const token = process.env.CUSTOM_REGISTRY_TOKEN; if (token) { return token; } else if (username && password) { // Some registries support basic auth token generation // This is a simplified implementation return Buffer.from(`${username}:${password}`).toString('base64'); } return null; } async fetchTags(registry, repository, token, limit = 10) { try { const headers = {}; if (token) { headers.Authorization = `Bearer ${token}`; } // First, get the list of tags const tagsUrl = `${registry}/${repository}/tags/list`; const tagsResponse = await axios.get(tagsUrl, { headers }); const tags = tagsResponse.data.tags || []; const limitedTags = tags.slice(0, limit); // For Docker Hub and registries that support the v2 API, we can get more details const details = []; const digests = {}; // Get manifest for each tag to extract digest and other details for (const tag of limitedTags) { try { const manifestUrl = `${registry}/${repository}/manifests/${tag}`; const manifestHeaders = { ...headers, Accept: 'application/vnd.docker.distribution.manifest.v2+json' }; const manifestResponse = await axios.get(manifestUrl, { headers: manifestHeaders }); if (manifestResponse.headers['docker-content-digest']) { digests[tag] = manifestResponse.headers['docker-content-digest']; } details.push({ tag, digest: manifestResponse.headers['docker-content-digest'], contentType: manifestResponse.headers['content-type'], size: manifestResponse.data.config?.size, layers: manifestResponse.data.layers?.length }); } catch (error) { console.error(`Error fetching manifest for ${repository}:${tag}:`, error); } } return { name: repository, tags: limitedTags, digests, details }; } catch (error) { console.error(`Error fetching tags for ${repository}:`, error); throw new McpError(ErrorCode.InternalError, `Failed to fetch tags for ${repository}: ${error.message || 'Unknown error'}`); } } async getDockerHubTags(image, limit = 10, includeDigest = false) { // For Docker Hub, the repository format is typically namespace/image const repository = image.includes('/') ? image : `library/${image}`; const token = await this.getDockerHubToken(repository); const result = await this.fetchTags(this.dockerHubRegistry, repository, token, limit); // For Docker Hub, we can also get additional information about each tag const versions = []; for (const tag of result.tags) { try { // Get more details about this specific tag const detailsUrl = `https://hub.docker.com/v2/repositories/${repository}/tags/${tag}`; const detailsResponse = await axios.get(detailsUrl); const tagInfo = detailsResponse.data; versions.push({ name: image, tag, registry: 'docker', digest: includeDigest ? result.digests?.[tag] : undefined, created: tagInfo.last_updated, size: tagInfo.full_size ? `${Math.round(tagInfo.full_size / 1024 / 1024)} MB` : undefined }); } catch (error) { // If we can't get detailed info, just add the basic tag info versions.push({ name: image, tag, registry: 'docker', digest: includeDigest ? result.digests?.[tag] : undefined }); } } return versions; } async getGithubContainerRegistryTags(image, limit = 10, includeDigest = false) { const token = await this.getGithubContainerRegistryToken(image); const result = await this.fetchTags(this.githubContainerRegistry, image, token, limit); return result.tags.map(tag => ({ name: image, tag, registry: 'ghcr', digest: includeDigest ? result.digests?.[tag] : undefined })); } async getCustomRegistryTags(registry, image, limit = 10, includeDigest = false) { const token = await this.getCustomRegistryToken(registry, image); // Remove protocol and trailing slash from registry URL for API calls const registryUrl = registry.replace(/^(https?:\/\/)/, '').replace(/\/$/, ''); const apiUrl = `https://${registryUrl}/v2`; const result = await this.fetchTags(apiUrl, image, token, limit); return result.tags.map(tag => ({ name: image, tag, registry: 'custom', digest: includeDigest ? result.digests?.[tag] : undefined })); } async getLatestVersion(args) { if (!args.image) { throw new McpError(ErrorCode.InvalidParams, 'Image name is required'); } const limit = args.limit || 10; const includeDigest = args.includeDigest || false; let results = []; try { switch (args.registry) { case 'ghcr': results = await this.getGithubContainerRegistryTags(args.image, limit, includeDigest); break; case 'custom': if (!args.customRegistry) { throw new McpError(ErrorCode.InvalidParams, 'Custom registry URL is required when registry type is "custom"'); } results = await this.getCustomRegistryTags(args.customRegistry, args.image, limit, includeDigest); break; case 'dockerhub': default: results = await this.getDockerHubTags(args.image, limit, includeDigest); break; } // Filter tags if filterTags is provided if (args.filterTags && args.filterTags.length > 0) { const filterRegexes = args.filterTags.map(pattern => new RegExp(pattern)); results = results.filter(result => filterRegexes.some(regex => regex.test(result.tag))); } return { content: [ { type: 'text', text: JSON.stringify(results, null, 2), }, ], }; } catch (error) { console.error(`Error checking Docker image ${args.image}:`, error); throw new McpError(ErrorCode.InternalError, `Failed to fetch Docker image information: ${error.message || 'Unknown error'}`); } } }