https://github.com/sammcj/mcp-package-version
by sammcj
- mcp-package-version
- src
- handlers
import axios from 'axios'
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js'
import {
PackageHandler,
GitHubActionQuery,
GitHubActionVersion,
GitHubActionInput
} from '../types/index.js'
export class GitHubActionsHandler implements PackageHandler {
private githubApiUrl = 'https://api.github.com'
private githubToken = process.env.GITHUB_TOKEN || ''
private getHeaders() {
const headers: Record<string, string> = {
'Accept': 'application/vnd.github.v3+json'
}
if (this.githubToken) {
headers['Authorization'] = `token ${this.githubToken}`
}
return headers
}
private async getLatestActionVersion(action: GitHubActionInput): Promise<GitHubActionVersion> {
try {
const { owner, repo, currentVersion } = action
const headers = this.getHeaders()
// First, try to get releases
const releasesUrl = `${this.githubApiUrl}/repos/${owner}/${repo}/releases`
const releasesResponse = await axios.get(releasesUrl, { headers })
if (releasesResponse.data && releasesResponse.data.length > 0) {
// Sort releases by published date (newest first)
const releases = releasesResponse.data.sort((a: any, b: any) =>
new Date(b.published_at).getTime() - new Date(a.published_at).getTime()
)
// Get the latest release
const latestRelease = releases[0]
// Extract version from tag name (remove 'v' prefix if present)
const latestVersion = latestRelease.tag_name.replace(/^v/, '')
// Parse version components
const versionParts = latestVersion.split('.')
const latestMajorVersion = versionParts[0] ? `${versionParts[0]}` : undefined
const latestMinorVersion = versionParts[1] ? `${versionParts[0]}.${versionParts[1]}` : undefined
const latestPatchVersion = latestVersion
return {
name: `${owner}/${repo}`,
owner,
repo,
currentVersion,
latestVersion,
latestMajorVersion,
latestMinorVersion,
latestPatchVersion,
publishedAt: latestRelease.published_at,
url: latestRelease.html_url
}
}
// If no releases, try to get tags
const tagsUrl = `${this.githubApiUrl}/repos/${owner}/${repo}/tags`
const tagsResponse = await axios.get(tagsUrl, { headers })
if (tagsResponse.data && tagsResponse.data.length > 0) {
// Get the first tag (usually the latest)
const latestTag = tagsResponse.data[0]
// Extract version from tag name (remove 'v' prefix if present)
const latestVersion = latestTag.name.replace(/^v/, '')
// Parse version components
const versionParts = latestVersion.split('.')
const latestMajorVersion = versionParts[0] ? `${versionParts[0]}` : undefined
const latestMinorVersion = versionParts[1] ? `${versionParts[0]}.${versionParts[1]}` : undefined
const latestPatchVersion = latestVersion
return {
name: `${owner}/${repo}`,
owner,
repo,
currentVersion,
latestVersion,
latestMajorVersion,
latestMinorVersion,
latestPatchVersion,
url: latestTag.commit.url
}
}
// If no releases or tags, check if the repo exists
const repoUrl = `${this.githubApiUrl}/repos/${owner}/${repo}`
await axios.get(repoUrl, { headers })
// If we get here, the repo exists but has no releases or tags
return {
name: `${owner}/${repo}`,
owner,
repo,
currentVersion,
latestVersion: 'unknown',
url: `https://github.com/${owner}/${repo}`
}
} catch (error: any) {
console.error(`Error fetching GitHub Action ${action.owner}/${action.repo}:`, error)
// Check if it's a 404 error (repo not found)
if (error.response && error.response.status === 404) {
return {
name: `${action.owner}/${action.repo}`,
owner: action.owner,
repo: action.repo,
currentVersion: action.currentVersion,
latestVersion: 'not found',
skipped: true,
skipReason: 'Repository not found'
} as GitHubActionVersion
}
// Check if it's a rate limit error
if (error.response && error.response.status === 403 && error.response.headers['x-ratelimit-remaining'] === '0') {
return {
name: `${action.owner}/${action.repo}`,
owner: action.owner,
repo: action.repo,
currentVersion: action.currentVersion,
latestVersion: 'unknown',
skipped: true,
skipReason: 'GitHub API rate limit exceeded'
} as GitHubActionVersion
}
// Other errors
return {
name: `${action.owner}/${action.repo}`,
owner: action.owner,
repo: action.repo,
currentVersion: action.currentVersion,
latestVersion: 'error',
skipped: true,
skipReason: `Error: ${error.message || 'Unknown error'}`
} as GitHubActionVersion
}
}
async getLatestVersion(args: GitHubActionQuery) {
if (!args.actions || !Array.isArray(args.actions) || args.actions.length === 0) {
throw new McpError(
ErrorCode.InvalidParams,
'At least one GitHub Action must be provided'
)
}
try {
const results: GitHubActionVersion[] = []
// Process each action in parallel
const promises = args.actions.map(action => this.getLatestActionVersion(action))
const actionVersions = await Promise.all(promises)
results.push(...actionVersions)
// Filter out unnecessary details if includeDetails is false
if (args.includeDetails !== true) {
results.forEach(result => {
delete result.publishedAt
delete result.url
})
}
return {
content: [
{
type: 'text',
text: JSON.stringify(results, null, 2),
},
],
}
} catch (error: any) {
console.error('Error checking GitHub Actions:', error)
throw new McpError(
ErrorCode.InternalError,
`Failed to fetch GitHub Actions information: ${error.message || 'Unknown error'}`
)
}
}
}