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
JavaScript
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'}`);
}
}
}