npm.go•6.03 kB
package handlers
import (
"context"
"encoding/json"
"fmt"
"net/url"
"sort"
"strings"
"sync"
"github.com/mark3labs/mcp-go/mcp"
"github.com/sirupsen/logrus"
)
const (
// NpmRegistryURL is the base URL for the npm registry
NpmRegistryURL = "https://registry.npmjs.org"
)
// NpmHandler handles npm package version checking
type NpmHandler struct {
client HTTPClient
cache *sync.Map
logger *logrus.Logger
}
// NewNpmHandler creates a new npm handler
func NewNpmHandler(logger *logrus.Logger, cache *sync.Map) *NpmHandler {
if cache == nil {
cache = &sync.Map{}
}
return &NpmHandler{
client: DefaultHTTPClient,
cache: cache,
logger: logger,
}
}
// NpmPackageInfo represents information about an npm package
type NpmPackageInfo struct {
Name string `json:"name"`
DistTags map[string]string `json:"dist-tags"`
Versions map[string]struct {
Version string `json:"version"`
} `json:"versions"`
}
// getPackageInfo gets information about an npm package
func (h *NpmHandler) getPackageInfo(packageName string) (*NpmPackageInfo, error) {
// Check cache first
if cachedInfo, ok := h.cache.Load(fmt.Sprintf("npm:%s", packageName)); ok {
h.logger.WithField("package", packageName).Debug("Using cached npm package info")
return cachedInfo.(*NpmPackageInfo), nil
}
// Construct URL
packageURL := fmt.Sprintf("%s/%s", NpmRegistryURL, url.PathEscape(packageName))
h.logger.WithFields(logrus.Fields{
"package": packageName,
"url": packageURL,
}).Debug("Fetching npm package info")
// Make request
body, err := MakeRequestWithLogger(h.client, h.logger, "GET", packageURL, nil)
if err != nil {
return nil, fmt.Errorf("failed to fetch npm package info: %w", err)
}
// Parse response
var info NpmPackageInfo
if err := json.Unmarshal(body, &info); err != nil {
return nil, fmt.Errorf("failed to parse npm package info: %w", err)
}
// Cache result
h.cache.Store(fmt.Sprintf("npm:%s", packageName), &info)
return &info, nil
}
// GetLatestVersion gets the latest version of npm packages
func (h *NpmHandler) GetLatestVersion(ctx context.Context, args map[string]interface{}) (*mcp.CallToolResult, error) {
h.logger.Debug("Getting latest npm package versions")
// Parse dependencies
depsRaw, ok := args["dependencies"]
if !ok {
return nil, fmt.Errorf("missing required parameter: dependencies")
}
// Convert to map[string]string
depsMap := make(map[string]string)
if deps, ok := depsRaw.(map[string]interface{}); ok {
for name, version := range deps {
if vStr, ok := version.(string); ok {
depsMap[name] = vStr
} else {
depsMap[name] = fmt.Sprintf("%v", version)
}
}
} else {
return nil, fmt.Errorf("invalid dependencies format: expected object")
}
// Parse constraints
var constraints VersionConstraints
if constraintsRaw, ok := args["constraints"]; ok {
if constraintsMap, ok := constraintsRaw.(map[string]interface{}); ok {
constraints = make(VersionConstraints)
for name, constraintRaw := range constraintsMap {
if constraintMap, ok := constraintRaw.(map[string]interface{}); ok {
var constraint VersionConstraint
if majorVersion, ok := constraintMap["majorVersion"].(float64); ok {
majorInt := int(majorVersion)
constraint.MajorVersion = &majorInt
}
if excludePackage, ok := constraintMap["excludePackage"].(bool); ok {
constraint.ExcludePackage = excludePackage
}
constraints[name] = constraint
}
}
}
}
// Process each dependency
results := make([]PackageVersion, 0, len(depsMap))
for name, version := range depsMap {
h.logger.WithFields(logrus.Fields{
"package": name,
"version": version,
}).Debug("Processing npm package")
// Check if package should be excluded
if constraint, ok := constraints[name]; ok && constraint.ExcludePackage {
results = append(results, PackageVersion{
Name: name,
Skipped: true,
SkipReason: "Package excluded by constraints",
})
continue
}
// Clean version string
currentVersion := CleanVersion(version)
// Get package info
info, err := h.getPackageInfo(name)
if err != nil {
h.logger.WithFields(logrus.Fields{
"package": name,
"error": err.Error(),
}).Error("Failed to get npm package info")
results = append(results, PackageVersion{
Name: name,
CurrentVersion: StringPtr(currentVersion),
LatestVersion: "unknown",
Registry: "npm",
Skipped: true,
SkipReason: fmt.Sprintf("Failed to fetch package info: %v", err),
})
continue
}
// Get latest version
latestVersion := info.DistTags["latest"]
if latestVersion == "" {
// If no latest tag, use the highest version
versions := make([]string, 0, len(info.Versions))
for v := range info.Versions {
versions = append(versions, v)
}
sort.Strings(versions)
if len(versions) > 0 {
latestVersion = versions[len(versions)-1]
}
}
// Apply major version constraint if specified
if constraint, ok := constraints[name]; ok && constraint.MajorVersion != nil {
targetMajor := *constraint.MajorVersion
latestMajor, _, _, err := ParseVersion(latestVersion)
if err == nil && latestMajor > targetMajor {
// Find the latest version with the target major version
versions := make([]string, 0, len(info.Versions))
for v := range info.Versions {
major, _, _, err := ParseVersion(v)
if err == nil && major == targetMajor {
versions = append(versions, v)
}
}
sort.Strings(versions)
if len(versions) > 0 {
latestVersion = versions[len(versions)-1]
}
}
}
// Add result
results = append(results, PackageVersion{
Name: name,
CurrentVersion: StringPtr(currentVersion),
LatestVersion: latestVersion,
Registry: "npm",
})
}
// Sort results by name
sort.Slice(results, func(i, j int) bool {
return strings.ToLower(results[i].Name) < strings.ToLower(results[j].Name)
})
return NewToolResultJSON(results)
}