#
tokens: 17742/50000 4/36 files (page 2/2)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 2 of 2. Use http://codebase.md/sammcj/mcp-package-version?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .clinerules
├── .github
│   ├── FUNDING.yml
│   └── workflows
│       └── publish.yml
├── .gitignore
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── docker-compose.yaml
├── Dockerfile
├── go.mod
├── go.sum
├── images
│   ├── cline-without.jpg
│   └── with-without.jpg
├── internal
│   ├── cache
│   │   └── cache.go
│   └── handlers
│       ├── bedrock.go
│       ├── docker_test.go
│       ├── docker.go
│       ├── github_actions.go
│       ├── go.go
│       ├── java.go
│       ├── npm.go
│       ├── python.go
│       ├── swift.go
│       ├── tests
│       │   ├── mcp_response_test.go
│       │   └── mock_client.go
│       ├── types.go
│       └── utils.go
├── LICENSE
├── main.go
├── Makefile
├── pkg
│   ├── server
│   │   ├── server_test.go
│   │   ├── server.go
│   │   ├── tests
│   │   │   ├── mcp_official_schema_test.go
│   │   │   └── mcp_schema_test.go
│   │   └── version.go
│   └── version
│       └── version.go
├── README.md
├── smithery.yaml
└── tests
    ├── handlers
    │   ├── docker_test.go
    │   └── mcp_response_test.go
    └── server
        ├── mcp_official_schema_test.go
        ├── mcp_schema_test.go
        └── server_test.go
```

# Files

--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------

```yaml
  1 | name: Build and Release
  2 | 
  3 | on:
  4 |   push:
  5 |     branches: [ main ]
  6 |     tags:
  7 |       - 'v*'
  8 |   pull_request:
  9 |     branches: [ main ]
 10 | 
 11 | jobs:
 12 |   bump-version:
 13 |     name: Bump Version
 14 |     runs-on: ubuntu-latest
 15 |     if: github.ref == 'refs/heads/main'
 16 |     outputs:
 17 |       new_tag: ${{ steps.tag_version.outputs.new_tag }}
 18 |       changelog: ${{ steps.tag_version.outputs.changelog }}
 19 |     permissions:
 20 |       contents: write
 21 |     steps:
 22 |       - name: Check out code
 23 |         uses: actions/checkout@v4
 24 |         with:
 25 |           fetch-depth: 0
 26 |           token: ${{ secrets.GITHUB_TOKEN }}
 27 | 
 28 |       - name: Bump version and push tag
 29 |         id: tag_version
 30 |         uses: mathieudutour/[email protected]
 31 |         with:
 32 |           github_token: ${{ secrets.GITHUB_TOKEN }}
 33 |           release_branches: main
 34 |           default_bump: patch
 35 |           tag_prefix: v
 36 |           create_annotated_tag: true
 37 | 
 38 |   build:
 39 |     name: Build and Test
 40 |     runs-on: ubuntu-latest
 41 |     needs: [bump-version]
 42 |     if: always() && (needs.bump-version.result == 'success' || needs.bump-version.result == 'skipped')
 43 |     steps:
 44 |       - name: Set up Go
 45 |         uses: actions/setup-go@v5
 46 |         with:
 47 |           go-version: '1.24'
 48 |           check-latest: true
 49 | 
 50 |       - name: Check out code
 51 |         uses: actions/checkout@v4
 52 | 
 53 |       - name: Set up Go cache
 54 |         uses: actions/cache@v4
 55 |         with:
 56 |           path: |
 57 |             ~/.cache/go-build
 58 |             ~/go/pkg/mod
 59 |           key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
 60 |           restore-keys: |
 61 |             ${{ runner.os }}-go-
 62 | 
 63 |       - name: Get dependencies
 64 |         run: go mod download
 65 | 
 66 |       - name: Build
 67 |         run: |
 68 |           # Get version from tag, bump-version job, or use SHA for non-tag builds
 69 |           if [[ $GITHUB_REF == refs/tags/v* ]]; then
 70 |             # If this is a tag build, use the tag version
 71 |             VERSION=${GITHUB_REF#refs/tags/v}
 72 |           elif [[ "${{ github.ref }}" == "refs/heads/main" && "${{ needs.bump-version.outputs.new_tag }}" != "" ]]; then
 73 |             # If this is a main branch build with a new tag from bump-version job
 74 |             VERSION="${{ needs.bump-version.outputs.new_tag }}"
 75 |             VERSION=${VERSION#v}  # Remove the 'v' prefix
 76 |           else
 77 |             # For PR builds, use the commit SHA
 78 |             VERSION="sha-$(git rev-parse --short HEAD)"
 79 |           fi
 80 | 
 81 |           echo "Building version: $VERSION"
 82 | 
 83 |           # Get commit hash
 84 |           COMMIT=$(git rev-parse --short HEAD)
 85 | 
 86 |           # Get build date
 87 |           BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
 88 | 
 89 |           # Build with ldflags to inject version info
 90 |           mkdir -p bin
 91 |           go build -v -o bin/mcp-package-version \
 92 |             -ldflags "-X github.com/sammcj/mcp-package-version/v2/pkg/version.Version=$VERSION -X github.com/sammcj/mcp-package-version/v2/pkg/version.Commit=$COMMIT -X github.com/sammcj/mcp-package-version/v2/pkg/version.BuildDate=$BUILD_DATE" \
 93 |             .
 94 | 
 95 |       - name: Test
 96 |         run: make test
 97 | 
 98 |       - name: Upload build artifacts
 99 |         uses: actions/upload-artifact@v4
100 |         with:
101 |           name: mcp-package-version
102 |           path: bin/mcp-package-version
103 |           retention-days: 7
104 | 
105 |   release:
106 |     name: Create Release
107 |     needs: [build, bump-version]
108 |     if: startsWith(github.ref, 'refs/tags/v') || (github.ref == 'refs/heads/main' && needs.bump-version.outputs.new_tag != '')
109 |     runs-on: ubuntu-latest
110 |     permissions:
111 |       contents: write
112 |     steps:
113 |       - name: Check out code
114 |         uses: actions/checkout@v4
115 |         with:
116 |           fetch-depth: 0
117 | 
118 |       - name: Set up Go
119 |         uses: actions/setup-go@v5
120 |         with:
121 |           go-version: '1.24'
122 |           check-latest: true
123 | 
124 |       - name: Download build artifacts
125 |         uses: actions/download-artifact@v4
126 |         with:
127 |           name: mcp-package-version
128 |           path: bin/
129 | 
130 |       - name: Make binary executable
131 |         run: chmod +x bin/mcp-package-version
132 | 
133 |       - name: Get version
134 |         id: get_version
135 |         run: |
136 |           if [[ $GITHUB_REF == refs/tags/v* ]]; then
137 |             # If this is a tag build, use the tag version
138 |             VERSION=${GITHUB_REF#refs/tags/v}
139 |           elif [[ "${{ github.ref }}" == "refs/heads/main" && "${{ needs.bump-version.outputs.new_tag }}" != "" ]]; then
140 |             # If this is a main branch build with a new tag from bump-version job
141 |             VERSION="${{ needs.bump-version.outputs.new_tag }}"
142 |             VERSION=${VERSION#v}  # Remove the 'v' prefix
143 |           else
144 |             # Fallback (should not happen due to job condition)
145 |             VERSION="0.0.0-unknown"
146 |           fi
147 | 
148 |           echo "Using version: $VERSION"
149 |           echo "VERSION=$VERSION" >> $GITHUB_ENV
150 |           echo "version=$VERSION" >> $GITHUB_OUTPUT
151 | 
152 |       - name: Generate changelog
153 |         id: changelog
154 |         run: |
155 |           if [[ "${{ github.ref }}" == "refs/heads/main" && "${{ needs.bump-version.outputs.changelog }}" != "" ]]; then
156 |             # If this is a main branch build with a changelog from bump-version job
157 |             CHANGELOG="${{ needs.bump-version.outputs.changelog }}"
158 |           else
159 |             # Generate changelog from git history
160 |             # Get the latest tag before this one
161 |             PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
162 | 
163 |             if [ -z "$PREVIOUS_TAG" ]; then
164 |               # If there's no previous tag, get all commits
165 |               CHANGELOG=$(git log --pretty=format:"* %s (%h)" --no-merges)
166 |             else
167 |               # Get commits between the previous tag and this one
168 |               CHANGELOG=$(git log --pretty=format:"* %s (%h)" --no-merges ${PREVIOUS_TAG}..HEAD)
169 |             fi
170 |           fi
171 | 
172 |           echo "CHANGELOG<<EOF" >> $GITHUB_ENV
173 |           echo "$CHANGELOG" >> $GITHUB_ENV
174 |           echo "EOF" >> $GITHUB_ENV
175 | 
176 |       - name: Create Release
177 |         uses: softprops/action-gh-release@v2
178 |         with:
179 |           name: Release v${{ steps.get_version.outputs.version }}
180 |           body: |
181 |             ## Changes in this Release
182 | 
183 |             ${{ env.CHANGELOG }}
184 | 
185 |             ## Installation
186 | 
187 |             Download the binary for your platform and run it.
188 |           files: |
189 |             bin/mcp-package-version
190 |           draft: false
191 |           prerelease: false
192 |           tag_name: ${{ github.ref == 'refs/heads/main' && needs.bump-version.outputs.new_tag || github.ref }}
193 | 
194 |   docker:
195 |     name: Build and Push Docker Image
196 |     needs: [build, bump-version]
197 |     # Only run for main branch and tag builds, not for PRs
198 |     if: (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') && github.event_name != 'pull_request'
199 |     runs-on: ubuntu-latest
200 |     permissions:
201 |       contents: read
202 |       packages: write
203 |     steps:
204 |       - name: Check out code
205 |         uses: actions/checkout@v4
206 | 
207 |       - name: Set up Docker Buildx
208 |         uses: docker/setup-buildx-action@v3
209 | 
210 |       - name: Login to GitHub Container Registry
211 |         uses: docker/login-action@v3
212 |         with:
213 |           registry: ghcr.io
214 |           username: ${{ github.actor }}
215 |           password: ${{ secrets.GITHUB_TOKEN }}
216 | 
217 |       - name: Extract metadata for Docker
218 |         id: meta
219 |         uses: docker/metadata-action@v5
220 |         with:
221 |           images: ghcr.io/${{ github.repository }}
222 |           tags: |
223 |             type=semver,pattern={{version}}
224 |             type=semver,pattern={{major}}.{{minor}}
225 |             type=semver,pattern={{major}}
226 |             type=ref,event=branch
227 |             type=sha
228 | 
229 |       - name: Get version information
230 |         id: version_info
231 |         run: |
232 |           # Get version from tag, bump-version job, or use SHA for non-tag builds
233 |           if [[ $GITHUB_REF == refs/tags/v* ]]; then
234 |             # If this is a tag build, use the tag version
235 |             VERSION=${GITHUB_REF#refs/tags/v}
236 |           elif [[ "${{ github.ref }}" == "refs/heads/main" && "${{ needs.bump-version.outputs.new_tag }}" != "" ]]; then
237 |             # If this is a main branch build with a new tag from bump-version job
238 |             VERSION="${{ needs.bump-version.outputs.new_tag }}"
239 |             VERSION=${VERSION#v}  # Remove the 'v' prefix
240 |           else
241 |             # For PR builds, use the commit SHA
242 |             VERSION="sha-$(git rev-parse --short HEAD)"
243 |           fi
244 | 
245 |           echo "Using version: $VERSION"
246 |           echo "VERSION=$VERSION" >> $GITHUB_ENV
247 | 
248 |           # Get commit hash
249 |           COMMIT=$(git rev-parse --short HEAD)
250 |           echo "COMMIT=$COMMIT" >> $GITHUB_ENV
251 | 
252 |           # Get build date
253 |           BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
254 |           echo "BUILD_DATE=$BUILD_DATE" >> $GITHUB_ENV
255 | 
256 |       - name: Build and push Docker image
257 |         uses: docker/build-push-action@v5
258 |         with:
259 |           context: .
260 |           push: true
261 |           tags: ${{ steps.meta.outputs.tags }}
262 |           labels: ${{ steps.meta.outputs.labels }}
263 |           build-args: |
264 |             VERSION=${{ env.VERSION }}
265 |             COMMIT=${{ env.COMMIT }}
266 |             BUILD_DATE=${{ env.BUILD_DATE }}
267 |           cache-from: type=gha
268 |           cache-to: type=gha,mode=max
269 | 
```

--------------------------------------------------------------------------------
/internal/handlers/python.go:
--------------------------------------------------------------------------------

```go
  1 | package handlers
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"encoding/json"
  6 | 	"fmt"
  7 | 	"regexp"
  8 | 	"sort"
  9 | 	"strings"
 10 | 	"sync"
 11 | 
 12 | 	"github.com/mark3labs/mcp-go/mcp"
 13 | 	"github.com/sirupsen/logrus"
 14 | )
 15 | 
 16 | const (
 17 | 	// PyPIURL is the base URL for the PyPI API
 18 | 	PyPIURL = "https://pypi.org/pypi"
 19 | )
 20 | 
 21 | // PythonHandler handles Python package version checking
 22 | type PythonHandler struct {
 23 | 	client HTTPClient
 24 | 	cache  *sync.Map
 25 | 	logger *logrus.Logger
 26 | }
 27 | 
 28 | // NewPythonHandler creates a new Python handler
 29 | func NewPythonHandler(logger *logrus.Logger, cache *sync.Map) *PythonHandler {
 30 | 	if cache == nil {
 31 | 		cache = &sync.Map{}
 32 | 	}
 33 | 	return &PythonHandler{
 34 | 		client: DefaultHTTPClient,
 35 | 		cache:  cache,
 36 | 		logger: logger,
 37 | 	}
 38 | }
 39 | 
 40 | // PyPIPackageInfo represents information about a PyPI package
 41 | type PyPIPackageInfo struct {
 42 | 	Info struct {
 43 | 		Name    string `json:"name"`
 44 | 		Version string `json:"version"`
 45 | 	} `json:"info"`
 46 | 	Releases map[string][]struct {
 47 | 		PackageType string `json:"packagetype"`
 48 | 	} `json:"releases"`
 49 | }
 50 | 
 51 | // getPackageInfo gets information about a PyPI package
 52 | func (h *PythonHandler) getPackageInfo(packageName string) (*PyPIPackageInfo, error) {
 53 | 	// Check cache first
 54 | 	if cachedInfo, ok := h.cache.Load(fmt.Sprintf("pypi:%s", packageName)); ok {
 55 | 		h.logger.WithField("package", packageName).Debug("Using cached PyPI package info")
 56 | 		return cachedInfo.(*PyPIPackageInfo), nil
 57 | 	}
 58 | 
 59 | 	// Construct URL
 60 | 	packageURL := fmt.Sprintf("%s/%s/json", PyPIURL, packageName)
 61 | 	h.logger.WithFields(logrus.Fields{
 62 | 		"package": packageName,
 63 | 		"url":     packageURL,
 64 | 	}).Debug("Fetching PyPI package info")
 65 | 
 66 | 	// Make request
 67 | 	body, err := MakeRequestWithLogger(h.client, h.logger, "GET", packageURL, nil)
 68 | 	if err != nil {
 69 | 		return nil, fmt.Errorf("failed to fetch PyPI package info: %w", err)
 70 | 	}
 71 | 
 72 | 	// Parse response
 73 | 	var info PyPIPackageInfo
 74 | 	if err := json.Unmarshal(body, &info); err != nil {
 75 | 		return nil, fmt.Errorf("failed to parse PyPI package info: %w", err)
 76 | 	}
 77 | 
 78 | 	// Cache result
 79 | 	h.cache.Store(fmt.Sprintf("pypi:%s", packageName), &info)
 80 | 
 81 | 	return &info, nil
 82 | }
 83 | 
 84 | // parseRequirement parses a Python requirement string
 85 | func parseRequirement(req string) (name string, version string, err error) {
 86 | 	// Extract package name and version constraint
 87 | 	re := regexp.MustCompile(`^([a-zA-Z0-9_.-]+)(?:\s*([<>=!~^].*)?)?$`)
 88 | 	matches := re.FindStringSubmatch(req)
 89 | 	if len(matches) < 2 {
 90 | 		return "", "", fmt.Errorf("invalid requirement format: %s", req)
 91 | 	}
 92 | 
 93 | 	name = matches[1]
 94 | 	if len(matches) > 2 && matches[2] != "" {
 95 | 		version = strings.TrimSpace(matches[2])
 96 | 	}
 97 | 
 98 | 	return name, version, nil
 99 | }
100 | 
101 | // GetLatestVersionFromRequirements gets the latest version of Python packages from requirements.txt
102 | func (h *PythonHandler) GetLatestVersionFromRequirements(ctx context.Context, args map[string]interface{}) (*mcp.CallToolResult, error) {
103 | 	h.logger.Debug("Getting latest Python package versions from requirements.txt")
104 | 
105 | 	// Parse requirements
106 | 	reqsRaw, ok := args["requirements"]
107 | 	if !ok {
108 | 		return nil, fmt.Errorf("missing required parameter: requirements")
109 | 	}
110 | 
111 | 	// Convert to []string
112 | 	var reqs []string
113 | 	if reqsArr, ok := reqsRaw.([]interface{}); ok {
114 | 		for _, req := range reqsArr {
115 | 			if reqStr, ok := req.(string); ok {
116 | 				reqs = append(reqs, reqStr)
117 | 			} else {
118 | 				reqs = append(reqs, fmt.Sprintf("%v", req))
119 | 			}
120 | 		}
121 | 	} else {
122 | 		return nil, fmt.Errorf("invalid requirements format: expected array")
123 | 	}
124 | 
125 | 	// Process each requirement
126 | 	results := make([]PackageVersion, 0, len(reqs))
127 | 	for _, req := range reqs {
128 | 		// Skip comments and empty lines
129 | 		req = strings.TrimSpace(req)
130 | 		if req == "" || strings.HasPrefix(req, "#") {
131 | 			continue
132 | 		}
133 | 
134 | 		// Parse requirement
135 | 		name, version, err := parseRequirement(req)
136 | 		if err != nil {
137 | 			h.logger.WithFields(logrus.Fields{
138 | 				"requirement": req,
139 | 				"error":       err.Error(),
140 | 			}).Error("Failed to parse Python requirement")
141 | 			results = append(results, PackageVersion{
142 | 				Name:       req,
143 | 				Skipped:    true,
144 | 				SkipReason: fmt.Sprintf("Failed to parse requirement: %v", err),
145 | 			})
146 | 			continue
147 | 		}
148 | 
149 | 		// Clean version string
150 | 		currentVersion := CleanVersion(version)
151 | 
152 | 		// Get package info
153 | 		info, err := h.getPackageInfo(name)
154 | 		if err != nil {
155 | 			h.logger.WithFields(logrus.Fields{
156 | 				"package": name,
157 | 				"error":   err.Error(),
158 | 			}).Error("Failed to get PyPI package info")
159 | 			results = append(results, PackageVersion{
160 | 				Name:           name,
161 | 				CurrentVersion: StringPtr(currentVersion),
162 | 				LatestVersion:  "unknown",
163 | 				Registry:       "pypi",
164 | 				Skipped:        true,
165 | 				SkipReason:     fmt.Sprintf("Failed to fetch package info: %v", err),
166 | 			})
167 | 			continue
168 | 		}
169 | 
170 | 		// Get latest version
171 | 		latestVersion := info.Info.Version
172 | 
173 | 		// Add result
174 | 		results = append(results, PackageVersion{
175 | 			Name:           name,
176 | 			CurrentVersion: StringPtr(currentVersion),
177 | 			LatestVersion:  latestVersion,
178 | 			Registry:       "pypi",
179 | 		})
180 | 	}
181 | 
182 | 	// Sort results by name
183 | 	sort.Slice(results, func(i, j int) bool {
184 | 		return strings.ToLower(results[i].Name) < strings.ToLower(results[j].Name)
185 | 	})
186 | 
187 | 	return NewToolResultJSON(results)
188 | }
189 | 
190 | // GetLatestVersionFromPyProject gets the latest version of Python packages from pyproject.toml
191 | func (h *PythonHandler) GetLatestVersionFromPyProject(ctx context.Context, args map[string]interface{}) (*mcp.CallToolResult, error) {
192 | 	h.logger.Debug("Getting latest Python package versions from pyproject.toml")
193 | 
194 | 	// Parse dependencies
195 | 	depsRaw, ok := args["dependencies"]
196 | 	if !ok {
197 | 		return nil, fmt.Errorf("missing required parameter: dependencies")
198 | 	}
199 | 
200 | 	// Convert to PyProjectDependencies
201 | 	var pyProjectDeps PyProjectDependencies
202 | 	if depsMap, ok := depsRaw.(map[string]interface{}); ok {
203 | 		// Parse main dependencies
204 | 		if mainDeps, ok := depsMap["dependencies"].(map[string]interface{}); ok {
205 | 			pyProjectDeps.Dependencies = make(map[string]string)
206 | 			for name, version := range mainDeps {
207 | 				if vStr, ok := version.(string); ok {
208 | 					pyProjectDeps.Dependencies[name] = vStr
209 | 				} else {
210 | 					pyProjectDeps.Dependencies[name] = fmt.Sprintf("%v", version)
211 | 				}
212 | 			}
213 | 		}
214 | 
215 | 		// Parse optional dependencies
216 | 		if optDeps, ok := depsMap["optional-dependencies"].(map[string]interface{}); ok {
217 | 			pyProjectDeps.OptionalDependencies = make(map[string]map[string]string)
218 | 			for group, deps := range optDeps {
219 | 				if depsMap, ok := deps.(map[string]interface{}); ok {
220 | 					pyProjectDeps.OptionalDependencies[group] = make(map[string]string)
221 | 					for name, version := range depsMap {
222 | 						if vStr, ok := version.(string); ok {
223 | 							pyProjectDeps.OptionalDependencies[group][name] = vStr
224 | 						} else {
225 | 							pyProjectDeps.OptionalDependencies[group][name] = fmt.Sprintf("%v", version)
226 | 						}
227 | 					}
228 | 				}
229 | 			}
230 | 		}
231 | 
232 | 		// Parse dev dependencies
233 | 		if devDeps, ok := depsMap["dev-dependencies"].(map[string]interface{}); ok {
234 | 			pyProjectDeps.DevDependencies = make(map[string]string)
235 | 			for name, version := range devDeps {
236 | 				if vStr, ok := version.(string); ok {
237 | 					pyProjectDeps.DevDependencies[name] = vStr
238 | 				} else {
239 | 					pyProjectDeps.DevDependencies[name] = fmt.Sprintf("%v", version)
240 | 				}
241 | 			}
242 | 		}
243 | 	} else {
244 | 		return nil, fmt.Errorf("invalid dependencies format: expected object")
245 | 	}
246 | 
247 | 	// Process all dependencies
248 | 	results := make([]PackageVersion, 0)
249 | 
250 | 	// Process main dependencies
251 | 	for name, version := range pyProjectDeps.Dependencies {
252 | 		result, err := h.processPackage(name, version)
253 | 		if err != nil {
254 | 			h.logger.WithFields(logrus.Fields{
255 | 				"package": name,
256 | 				"error":   err.Error(),
257 | 			}).Error("Failed to process Python package")
258 | 		} else {
259 | 			results = append(results, result)
260 | 		}
261 | 	}
262 | 
263 | 	// Process optional dependencies
264 | 	for group, deps := range pyProjectDeps.OptionalDependencies {
265 | 		for name, version := range deps {
266 | 			result, err := h.processPackage(name, version)
267 | 			if err != nil {
268 | 				h.logger.WithFields(logrus.Fields{
269 | 					"package": name,
270 | 					"group":   group,
271 | 					"error":   err.Error(),
272 | 				}).Error("Failed to process Python package")
273 | 			} else {
274 | 				// Add group info to result
275 | 				result.Name = fmt.Sprintf("%s (optional:%s)", name, group)
276 | 				results = append(results, result)
277 | 			}
278 | 		}
279 | 	}
280 | 
281 | 	// Process dev dependencies
282 | 	for name, version := range pyProjectDeps.DevDependencies {
283 | 		result, err := h.processPackage(name, version)
284 | 		if err != nil {
285 | 			h.logger.WithFields(logrus.Fields{
286 | 				"package": name,
287 | 				"error":   err.Error(),
288 | 			}).Error("Failed to process Python package")
289 | 		} else {
290 | 			// Add dev info to result
291 | 			result.Name = fmt.Sprintf("%s (dev)", name)
292 | 			results = append(results, result)
293 | 		}
294 | 	}
295 | 
296 | 	// Sort results by name
297 | 	sort.Slice(results, func(i, j int) bool {
298 | 		return strings.ToLower(results[i].Name) < strings.ToLower(results[j].Name)
299 | 	})
300 | 
301 | 	return NewToolResultJSON(results)
302 | }
303 | 
304 | // processPackage processes a single Python package
305 | func (h *PythonHandler) processPackage(name, version string) (PackageVersion, error) {
306 | 	// Clean version string
307 | 	currentVersion := CleanVersion(version)
308 | 
309 | 	// Get package info
310 | 	info, err := h.getPackageInfo(name)
311 | 	if err != nil {
312 | 		return PackageVersion{
313 | 			Name:           name,
314 | 			CurrentVersion: StringPtr(currentVersion),
315 | 			LatestVersion:  "unknown",
316 | 			Registry:       "pypi",
317 | 			Skipped:        true,
318 | 			SkipReason:     fmt.Sprintf("Failed to fetch package info: %v", err),
319 | 		}, err
320 | 	}
321 | 
322 | 	// Get latest version
323 | 	latestVersion := info.Info.Version
324 | 
325 | 	return PackageVersion{
326 | 		Name:           name,
327 | 		CurrentVersion: StringPtr(currentVersion),
328 | 		LatestVersion:  latestVersion,
329 | 		Registry:       "pypi",
330 | 	}, nil
331 | }
332 | 
```

--------------------------------------------------------------------------------
/internal/handlers/bedrock.go:
--------------------------------------------------------------------------------

```go
  1 | package handlers
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"encoding/json"
  6 | 	"fmt"
  7 | 	"sort"
  8 | 	"strings"
  9 | 	"sync"
 10 | 
 11 | 	"github.com/mark3labs/mcp-go/mcp"
 12 | 	"github.com/sirupsen/logrus"
 13 | )
 14 | 
 15 | // BedrockHandler handles AWS Bedrock model checking
 16 | type BedrockHandler struct {
 17 | 	client HTTPClient
 18 | 	cache  *sync.Map
 19 | 	logger *logrus.Logger
 20 | }
 21 | 
 22 | // NewBedrockHandler creates a new Bedrock handler
 23 | func NewBedrockHandler(logger *logrus.Logger, cache *sync.Map) *BedrockHandler {
 24 | 	if cache == nil {
 25 | 		cache = &sync.Map{}
 26 | 	}
 27 | 	return &BedrockHandler{
 28 | 		client: DefaultHTTPClient,
 29 | 		cache:  cache,
 30 | 		logger: logger,
 31 | 	}
 32 | }
 33 | 
 34 | // GetLatestVersion gets information about AWS Bedrock models
 35 | func (h *BedrockHandler) GetLatestVersion(ctx context.Context, args map[string]interface{}) (*mcp.CallToolResult, error) {
 36 | 	h.logger.Debug("Getting AWS Bedrock model information")
 37 | 
 38 | 	// Parse action
 39 | 	action := "list"
 40 | 	if actionRaw, ok := args["action"].(string); ok && actionRaw != "" {
 41 | 		action = actionRaw
 42 | 	}
 43 | 
 44 | 	// Handle different actions
 45 | 	switch action {
 46 | 	case "list":
 47 | 		return h.listModels()
 48 | 	case "search":
 49 | 		return h.searchModels(args)
 50 | 	case "get":
 51 | 		return h.getModel(args)
 52 | 	case "get_latest_claude_sonnet":
 53 | 		return h.getLatestClaudeSonnet()
 54 | 	default:
 55 | 		return nil, fmt.Errorf("invalid action: %s", action)
 56 | 	}
 57 | }
 58 | 
 59 | // listModels lists all available AWS Bedrock models
 60 | func (h *BedrockHandler) listModels() (*mcp.CallToolResult, error) {
 61 | 	// In a real implementation, this would fetch data from AWS Bedrock API
 62 | 	// For now, we'll return a static list of models
 63 | 	models := []BedrockModel{
 64 | 		{
 65 | 			Provider:           "anthropic",
 66 | 			ModelName:          "Claude 3 Opus",
 67 | 			ModelID:            "anthropic.claude-3-opus-20240229-v1:0",
 68 | 			RegionsSupported:   []string{"us-east-1", "us-west-2", "eu-central-1"},
 69 | 			InputModalities:    []string{"text", "image"},
 70 | 			OutputModalities:   []string{"text"},
 71 | 			StreamingSupported: true,
 72 | 		},
 73 | 		{
 74 | 			Provider:           "anthropic",
 75 | 			ModelName:          "Claude 3 Sonnet",
 76 | 			ModelID:            "anthropic.claude-3-sonnet-20240229-v1:0",
 77 | 			RegionsSupported:   []string{"us-east-1", "us-west-2", "eu-central-1"},
 78 | 			InputModalities:    []string{"text", "image"},
 79 | 			OutputModalities:   []string{"text"},
 80 | 			StreamingSupported: true,
 81 | 		},
 82 | 		{
 83 | 			Provider:           "anthropic",
 84 | 			ModelName:          "Claude 3 Haiku",
 85 | 			ModelID:            "anthropic.claude-3-haiku-20240307-v1:0",
 86 | 			RegionsSupported:   []string{"us-east-1", "us-west-2", "eu-central-1"},
 87 | 			InputModalities:    []string{"text", "image"},
 88 | 			OutputModalities:   []string{"text"},
 89 | 			StreamingSupported: true,
 90 | 		},
 91 | 		{
 92 | 			Provider:           "amazon",
 93 | 			ModelName:          "Titan Text G1 - Express",
 94 | 			ModelID:            "amazon.titan-text-express-v1",
 95 | 			RegionsSupported:   []string{"us-east-1", "us-west-2"},
 96 | 			InputModalities:    []string{"text"},
 97 | 			OutputModalities:   []string{"text"},
 98 | 			StreamingSupported: true,
 99 | 		},
100 | 		{
101 | 			Provider:           "amazon",
102 | 			ModelName:          "Titan Image Generator G1",
103 | 			ModelID:            "amazon.titan-image-generator-v1",
104 | 			RegionsSupported:   []string{"us-east-1", "us-west-2"},
105 | 			InputModalities:    []string{"text"},
106 | 			OutputModalities:   []string{"image"},
107 | 			StreamingSupported: false,
108 | 		},
109 | 		{
110 | 			Provider:           "cohere",
111 | 			ModelName:          "Command",
112 | 			ModelID:            "cohere.command-text-v14",
113 | 			RegionsSupported:   []string{"us-east-1", "us-west-2"},
114 | 			InputModalities:    []string{"text"},
115 | 			OutputModalities:   []string{"text"},
116 | 			StreamingSupported: true,
117 | 		},
118 | 		{
119 | 			Provider:           "meta",
120 | 			ModelName:          "Llama 2 Chat 13B",
121 | 			ModelID:            "meta.llama2-13b-chat-v1",
122 | 			RegionsSupported:   []string{"us-east-1", "us-west-2"},
123 | 			InputModalities:    []string{"text"},
124 | 			OutputModalities:   []string{"text"},
125 | 			StreamingSupported: true,
126 | 		},
127 | 		{
128 | 			Provider:           "meta",
129 | 			ModelName:          "Llama 2 Chat 70B",
130 | 			ModelID:            "meta.llama2-70b-chat-v1",
131 | 			RegionsSupported:   []string{"us-east-1", "us-west-2"},
132 | 			InputModalities:    []string{"text"},
133 | 			OutputModalities:   []string{"text"},
134 | 			StreamingSupported: true,
135 | 		},
136 | 		{
137 | 			Provider:           "stability",
138 | 			ModelName:          "Stable Diffusion XL 1.0",
139 | 			ModelID:            "stability.stable-diffusion-xl-v1",
140 | 			RegionsSupported:   []string{"us-east-1", "us-west-2"},
141 | 			InputModalities:    []string{"text"},
142 | 			OutputModalities:   []string{"image"},
143 | 			StreamingSupported: false,
144 | 		},
145 | 	}
146 | 
147 | 	// Sort models by provider and name
148 | 	sort.Slice(models, func(i, j int) bool {
149 | 		if models[i].Provider != models[j].Provider {
150 | 			return models[i].Provider < models[j].Provider
151 | 		}
152 | 		return models[i].ModelName < models[j].ModelName
153 | 	})
154 | 
155 | 	result := BedrockModelSearchResult{
156 | 		Models:     models,
157 | 		TotalCount: len(models),
158 | 	}
159 | 
160 | 	return NewToolResultJSON(result)
161 | }
162 | 
163 | // searchModels searches for AWS Bedrock models
164 | func (h *BedrockHandler) searchModels(args map[string]interface{}) (*mcp.CallToolResult, error) {
165 | 	// Get all models
166 | 	result, err := h.listModels()
167 | 	if err != nil {
168 | 		return nil, err
169 | 	}
170 | 
171 | 	// Convert result to JSON string
172 | 	resultJSON, err := json.Marshal(result)
173 | 	if err != nil {
174 | 		return nil, fmt.Errorf("failed to marshal result: %w", err)
175 | 	}
176 | 
177 | 	// Parse result
178 | 	var data map[string]interface{}
179 | 	if err := json.Unmarshal(resultJSON, &data); err != nil {
180 | 		return nil, fmt.Errorf("failed to parse model data: %w", err)
181 | 	}
182 | 
183 | 	// Get models
184 | 	modelsRaw, ok := data["models"].([]interface{})
185 | 	if !ok {
186 | 		return nil, fmt.Errorf("invalid model data format")
187 | 	}
188 | 
189 | 	// Parse query
190 | 	query := ""
191 | 	if queryRaw, ok := args["query"].(string); ok {
192 | 		query = strings.ToLower(queryRaw)
193 | 	}
194 | 
195 | 	// Parse provider
196 | 	provider := ""
197 | 	if providerRaw, ok := args["provider"].(string); ok {
198 | 		provider = strings.ToLower(providerRaw)
199 | 	}
200 | 
201 | 	// Parse region
202 | 	region := ""
203 | 	if regionRaw, ok := args["region"].(string); ok {
204 | 		region = strings.ToLower(regionRaw)
205 | 	}
206 | 
207 | 	// Filter models
208 | 	var filteredModels []BedrockModel
209 | 	for _, modelRaw := range modelsRaw {
210 | 		modelMap, ok := modelRaw.(map[string]interface{})
211 | 		if !ok {
212 | 			continue
213 | 		}
214 | 
215 | 		// Convert to BedrockModel
216 | 		var model BedrockModel
217 | 		modelJSON, err := json.Marshal(modelMap)
218 | 		if err != nil {
219 | 			continue
220 | 		}
221 | 		if err := json.Unmarshal(modelJSON, &model); err != nil {
222 | 			continue
223 | 		}
224 | 
225 | 		// Apply filters
226 | 		if query != "" {
227 | 			nameMatch := strings.Contains(strings.ToLower(model.ModelName), query)
228 | 			idMatch := strings.Contains(strings.ToLower(model.ModelID), query)
229 | 			providerMatch := strings.Contains(strings.ToLower(model.Provider), query)
230 | 			if !nameMatch && !idMatch && !providerMatch {
231 | 				continue
232 | 			}
233 | 		}
234 | 
235 | 		if provider != "" && !strings.Contains(strings.ToLower(model.Provider), provider) {
236 | 			continue
237 | 		}
238 | 
239 | 		if region != "" {
240 | 			var regionMatch bool
241 | 			for _, r := range model.RegionsSupported {
242 | 				if strings.Contains(strings.ToLower(r), region) {
243 | 					regionMatch = true
244 | 					break
245 | 				}
246 | 			}
247 | 			if !regionMatch {
248 | 				continue
249 | 			}
250 | 		}
251 | 
252 | 		filteredModels = append(filteredModels, model)
253 | 	}
254 | 
255 | 	// Sort models by provider and name
256 | 	sort.Slice(filteredModels, func(i, j int) bool {
257 | 		if filteredModels[i].Provider != filteredModels[j].Provider {
258 | 			return filteredModels[i].Provider < filteredModels[j].Provider
259 | 		}
260 | 		return filteredModels[i].ModelName < filteredModels[j].ModelName
261 | 	})
262 | 
263 | 	searchResult := BedrockModelSearchResult{
264 | 		Models:     filteredModels,
265 | 		TotalCount: len(filteredModels),
266 | 	}
267 | 
268 | 	return NewToolResultJSON(searchResult)
269 | }
270 | 
271 | // getModel gets a specific AWS Bedrock model
272 | func (h *BedrockHandler) getModel(args map[string]interface{}) (*mcp.CallToolResult, error) {
273 | 	// Parse model ID
274 | 	modelID, ok := args["modelId"].(string)
275 | 	if !ok || modelID == "" {
276 | 		return nil, fmt.Errorf("missing required parameter: modelId")
277 | 	}
278 | 
279 | 	// Get all models
280 | 	result, err := h.listModels()
281 | 	if err != nil {
282 | 		return nil, err
283 | 	}
284 | 
285 | 	// Convert result to JSON string
286 | 	resultJSON, err := json.Marshal(result)
287 | 	if err != nil {
288 | 		return nil, fmt.Errorf("failed to marshal result: %w", err)
289 | 	}
290 | 
291 | 	// Parse result
292 | 	var data map[string]interface{}
293 | 	if err := json.Unmarshal(resultJSON, &data); err != nil {
294 | 		return nil, fmt.Errorf("failed to parse model data: %w", err)
295 | 	}
296 | 
297 | 	// Get models
298 | 	modelsRaw, ok := data["models"].([]interface{})
299 | 	if !ok {
300 | 		return nil, fmt.Errorf("invalid model data format")
301 | 	}
302 | 
303 | 	// Find model
304 | 	for _, modelRaw := range modelsRaw {
305 | 		modelMap, ok := modelRaw.(map[string]interface{})
306 | 		if !ok {
307 | 			continue
308 | 		}
309 | 
310 | 		// Check model ID
311 | 		if id, ok := modelMap["modelId"].(string); ok && id == modelID {
312 | 			return NewToolResultJSON(modelMap)
313 | 		}
314 | 	}
315 | 
316 | 	return nil, fmt.Errorf("model not found: %s", modelID)
317 | }
318 | 
319 | // getLatestClaudeSonnet gets the latest Claude Sonnet model
320 | func (h *BedrockHandler) getLatestClaudeSonnet() (*mcp.CallToolResult, error) {
321 | 	// Get all models
322 | 	result, err := h.listModels()
323 | 	if err != nil {
324 | 		return nil, err
325 | 	}
326 | 
327 | 	// Convert result to JSON string
328 | 	resultJSON, err := json.Marshal(result)
329 | 	if err != nil {
330 | 		return nil, fmt.Errorf("failed to marshal result: %w", err)
331 | 	}
332 | 
333 | 	// Parse result
334 | 	var data map[string]interface{}
335 | 	if err := json.Unmarshal(resultJSON, &data); err != nil {
336 | 		return nil, fmt.Errorf("failed to parse model data: %w", err)
337 | 	}
338 | 
339 | 	// Get models
340 | 	modelsRaw, ok := data["models"].([]interface{})
341 | 	if !ok {
342 | 		return nil, fmt.Errorf("invalid model data format")
343 | 	}
344 | 
345 | 	// Find Claude Sonnet model
346 | 	for _, modelRaw := range modelsRaw {
347 | 		modelMap, ok := modelRaw.(map[string]interface{})
348 | 		if !ok {
349 | 			continue
350 | 		}
351 | 
352 | 		// Convert to BedrockModel
353 | 		var model BedrockModel
354 | 		modelJSON, err := json.Marshal(modelMap)
355 | 		if err != nil {
356 | 			continue
357 | 		}
358 | 		if err := json.Unmarshal(modelJSON, &model); err != nil {
359 | 			continue
360 | 		}
361 | 
362 | 		// Check if it's Claude Sonnet
363 | 		if model.Provider == "anthropic" && strings.Contains(model.ModelName, "Sonnet") {
364 | 			return NewToolResultJSON(model)
365 | 		}
366 | 	}
367 | 
368 | 	return nil, fmt.Errorf("claude sonnet model not found")
369 | }
370 | 
```

--------------------------------------------------------------------------------
/pkg/server/server.go:
--------------------------------------------------------------------------------

```go
  1 | package server
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"fmt"
  6 | 	"io"
  7 | 	"os"
  8 | 	"os/signal"
  9 | 	"path/filepath"
 10 | 	"strings"
 11 | 	"sync"
 12 | 	"syscall"
 13 | 	"time"
 14 | 
 15 | 	"github.com/mark3labs/mcp-go/mcp"
 16 | 	mcpserver "github.com/mark3labs/mcp-go/server"
 17 | 	"github.com/sammcj/mcp-package-version/v2/internal/cache"
 18 | 	"github.com/sammcj/mcp-package-version/v2/internal/handlers"
 19 | 	"github.com/sirupsen/logrus"
 20 | 	"gopkg.in/natefinch/lumberjack.v2"
 21 | )
 22 | 
 23 | const (
 24 | 	// CacheTTL is the time-to-live for cached data (12 hours)
 25 | 	CacheTTL = 12 * time.Hour
 26 | 	// MaxLogSize is the maximum size of the log file in megabytes before rotation
 27 | 	MaxLogSize = 1
 28 | 	// MaxLogBackups is the maximum number of old log files to retain
 29 | 	MaxLogBackups = 3
 30 | 	// MaxLogAge is the maximum number of days to retain old log files
 31 | 	MaxLogAge = 28
 32 | )
 33 | 
 34 | // PackageVersionServer implements the MCPServerHandler interface for the package version server
 35 | type PackageVersionServer struct {
 36 | 	logger      *logrus.Logger
 37 | 	cache       *cache.Cache
 38 | 	sharedCache *sync.Map
 39 | 	Version     string
 40 | 	Commit      string
 41 | 	BuildDate   string
 42 | }
 43 | 
 44 | // getLogFilePath returns the path to the log file
 45 | func getLogFilePath() string {
 46 | 	// Get user's home directory
 47 | 	homeDir, err := os.UserHomeDir()
 48 | 	if err != nil {
 49 | 		// Fallback to current directory if home directory can't be determined
 50 | 		return "mcp-package-version.log"
 51 | 	}
 52 | 
 53 | 	// Create logs directory in user's home directory if it doesn't exist
 54 | 	logsDir := filepath.Join(homeDir, ".mcp-package-version", "logs")
 55 | 	if err := os.MkdirAll(logsDir, 0755); err != nil {
 56 | 		// Fallback to current directory if logs directory can't be created
 57 | 		return "mcp-package-version.log"
 58 | 	}
 59 | 
 60 | 	return filepath.Join(logsDir, "mcp-package-version.log")
 61 | }
 62 | 
 63 | // NewPackageVersionServer creates a new package version server
 64 | func NewPackageVersionServer(version, commit, buildDate string) *PackageVersionServer {
 65 | 	logger := logrus.New()
 66 | 	logger.SetFormatter(&logrus.TextFormatter{
 67 | 		FullTimestamp: true,
 68 | 	})
 69 | 
 70 | 	// Set log level based on environment variable
 71 | 	logLevelStr := os.Getenv("LOG_LEVEL")
 72 | 	logLevel, err := logrus.ParseLevel(logLevelStr)
 73 | 	if err == nil {
 74 | 		logger.SetLevel(logLevel)
 75 | 	} else {
 76 | 		// Default to Info level if LOG_LEVEL is not set or invalid
 77 | 		logger.SetLevel(logrus.InfoLevel)
 78 | 	}
 79 | 	logger.WithField("log_level", logger.GetLevel().String()).Debug("Log level set")
 80 | 
 81 | 	logFilePath := getLogFilePath()
 82 | 
 83 | 	// Configure log rotation
 84 | 	logRotator := &lumberjack.Logger{
 85 | 		Filename:   logFilePath,
 86 | 		MaxSize:    MaxLogSize,    // megabytes
 87 | 		MaxBackups: MaxLogBackups, // number of backups
 88 | 		MaxAge:     MaxLogAge,     // days
 89 | 		Compress:   true,          // compress old log files
 90 | 	}
 91 | 
 92 | 	// Set logger output to the rotated log file initially
 93 | 	// We will add stdout later only if transport is SSE
 94 | 	logger.SetOutput(logRotator)
 95 | 
 96 | 	// Create a fallback logger that discards all output in case we can't open the log file
 97 | 	fallbackLogger := logrus.New()
 98 | 	fallbackLogger.SetOutput(io.Discard)
 99 | 
100 | 	return &PackageVersionServer{
101 | 		logger:      logger,
102 | 		cache:       cache.NewCache(CacheTTL),
103 | 		sharedCache: &sync.Map{},
104 | 		Version:     version,
105 | 		Commit:      commit,
106 | 		BuildDate:   buildDate,
107 | 	}
108 | }
109 | 
110 | // Name returns the display name of the server
111 | func (s *PackageVersionServer) Name() string {
112 | 	return "Package Version"
113 | }
114 | 
115 | // Capabilities returns the server capabilities
116 | func (s *PackageVersionServer) Capabilities() []mcpserver.ServerOption {
117 | 	return []mcpserver.ServerOption{
118 | 		mcpserver.WithToolCapabilities(true),
119 | 		mcpserver.WithResourceCapabilities(false, false),
120 | 		mcpserver.WithPromptCapabilities(false),
121 | 	}
122 | }
123 | 
124 | // Initialize sets up the server
125 | func (s *PackageVersionServer) Initialize(srv *mcpserver.MCPServer) error {
126 | 	// Set up the logger
127 | 	pid := os.Getpid()
128 | 	s.logger.WithFields(logrus.Fields{
129 | 		"pid": pid,
130 | 	}).Debug("Starting package-version MCP server")
131 | 
132 | 	s.logger.Debug("Initialising package version handlers")
133 | 
134 | 	// Register tools and handlers
135 | 	s.registerNpmTool(srv)
136 | 	s.registerPythonTools(srv)
137 | 	s.registerJavaTools(srv)
138 | 	s.registerGoTool(srv)
139 | 	s.registerBedrockTools(srv)
140 | 	s.registerDockerTool(srv)
141 | 	s.registerSwiftTool(srv)
142 | 	s.registerGitHubActionsTool(srv)
143 | 
144 | 	// Register empty resource and prompt handlers to handle resources/list and prompts/list requests
145 | 	s.registerEmptyResourceHandlers(srv)
146 | 	s.registerEmptyPromptHandlers(srv)
147 | 
148 | 	s.logger.Debug("All handlers registered successfully")
149 | 
150 | 	return nil
151 | }
152 | 
153 | // registerEmptyResourceHandlers registers empty resource handlers to respond to resources/list requests
154 | func (s *PackageVersionServer) registerEmptyResourceHandlers(srv *mcpserver.MCPServer) {
155 | 	s.logger.Debug("Registering empty resource handlers")
156 | 
157 | 	// The mcp-go library will automatically handle resources/list requests with an empty list
158 | 	// if no resources are registered, but we need to declare the capability
159 | 	// No need to add any actual resources since we don't have any
160 | }
161 | 
162 | // registerEmptyPromptHandlers registers empty prompt handlers to respond to prompts/list requests
163 | func (s *PackageVersionServer) registerEmptyPromptHandlers(srv *mcpserver.MCPServer) {
164 | 	s.logger.Debug("Registering empty prompt handlers")
165 | 
166 | 	// The mcp-go library will automatically handle prompts/list requests with an empty list
167 | 	// if no prompts are registered, but we need to declare the capability
168 | 	// No need to add any actual prompts since we don't have any
169 | }
170 | 
171 | // Start starts the MCP server with the specified transport
172 | func (s *PackageVersionServer) Start(transport, port, baseURL string) error {
173 | 	s.logger.WithFields(logrus.Fields{
174 | 		"transport": transport,
175 | 		"port":      port,
176 | 		"baseURL":   baseURL,
177 | 	}).Debug("Starting MCP server")
178 | 
179 | 	// Create a context with cancellation for graceful shutdown
180 | 	_, cancel := context.WithCancel(context.Background())
181 | 	defer cancel()
182 | 
183 | 	// Create a new server
184 | 	srv := mcpserver.NewMCPServer("package-version", "Package Version MCP Server")
185 | 
186 | 	// Initialize the server
187 | 	if err := s.Initialize(srv); err != nil {
188 | 		return fmt.Errorf("failed to initialize server: %w", err)
189 | 	}
190 | 
191 | 	// Set up signal handling for graceful shutdown
192 | 	sigCh := make(chan os.Signal, 1)
193 | 	signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
194 | 
195 | 	// Run the server based on the transport type
196 | 	errCh := make(chan error, 1)
197 | 
198 | 	if transport == "sse" {
199 | 		// Configure logger to also write to stdout for SSE mode
200 | 		logRotator := s.logger.Out.(*lumberjack.Logger) // Get the existing rotator
201 | 		multiWriter := io.MultiWriter(os.Stdout, logRotator)
202 | 		s.logger.SetOutput(multiWriter)
203 | 		s.logger.Debug("Configured logger for SSE mode (file + stdout)")
204 | 
205 | 		// Create an SSE server
206 | 		// Ensure the baseURL has the correct format: http://hostname:port
207 | 		// Remove trailing slash if present
208 | 		if baseURL[len(baseURL)-1] == '/' {
209 | 			baseURL = baseURL[:len(baseURL)-1]
210 | 		}
211 | 
212 | 		// Ensure the baseURL is correctly formatted for SSE
213 | 		// The mcp-go package expects the baseURL to be in the format: http://hostname:port
214 | 		// without any trailing slashes or paths
215 | 
216 | 		// First, check if baseURL already includes a port
217 | 		var sseBaseURL string
218 | 		if baseURL == "http://localhost" || baseURL == "https://localhost" {
219 | 			// If baseURL is just http://localhost or https://localhost, append the port
220 | 			sseBaseURL = fmt.Sprintf("%s:%s", baseURL, port)
221 | 		} else {
222 | 			// Otherwise, use the baseURL as is, assuming it already includes the port if needed
223 | 			// Otherwise, use the baseURL as is. It should contain the correct
224 | 			// scheme, hostname, and port (if non-standard) for external access.
225 | 			sseBaseURL = baseURL
226 | 		}
227 | 
228 | 		// The --base-url provided by the user is assumed to be the correct external URL.
229 | 		// We no longer attempt to modify it or append the internal port, except for the localhost default case handled above.
230 | 		s.logger.WithField("final_advertised_base_url", sseBaseURL).Debug("Using final base URL for SSE configuration")
231 | 
232 | 		// Create the SSE server with the correct base URL
233 | 		// The WithBaseURL option is critical for the client to connect properly
234 | 		// Try with different options to see what works
235 | 
236 | 		// Try with a specific path for the SSE endpoint
237 | 		// The client might be expecting a specific path like /mcp/sse
238 | 		// Let's try with just the base URL without any path
239 | 		sseBaseURL = strings.TrimSuffix(sseBaseURL, "/mcp")
240 | 
241 | 		// Add SSE server options
242 | 		sseOptions := []mcpserver.SSEOption{
243 | 			mcpserver.WithBaseURL(sseBaseURL),
244 | 			// Add any other relevant options here if discovered
245 | 		}
246 | 		s.logger.WithField("sse_options", fmt.Sprintf("%+v", sseOptions)).Debug("Configuring SSE server with options") // Log options
247 | 
248 | 		// Create the SSE server with the options
249 | 		sseServer := mcpserver.NewSSEServer(srv, sseOptions...)
250 | 
251 | 		// Start the SSE server in a goroutine
252 | 		go func() {
253 | 			// Start the SSE server on the specified port
254 | 			// The server will listen on all interfaces (0.0.0.0)
255 | 			listenAddr := ":" + port
256 | 			s.logger.WithFields(logrus.Fields{
257 | 				"listenAddr": listenAddr,
258 | 				"baseURL":    sseBaseURL,
259 | 				"serverName": "package-version",
260 | 			}).Info("Attempting to start SSE server") // Changed level to Info
261 | 
262 | 			// Log the final configuration being used for SSE
263 | 			s.logger.WithFields(logrus.Fields{
264 | 				"listen_address":      listenAddr,
265 | 				"advertised_base_url": sseBaseURL,
266 | 			}).Info("SSE server configured")
267 | 
268 | 			// Log the available routes for debugging
269 | 			s.logger.Debug("Expected SSE routes:")
270 | 			s.logger.Debug("- " + sseBaseURL + "/")
271 | 			s.logger.Debug("- " + sseBaseURL + "/sse")
272 | 			s.logger.Debug("- " + sseBaseURL + "/events")
273 | 			s.logger.Debug("- " + sseBaseURL + "/mcp")
274 | 			s.logger.Debug("- " + sseBaseURL + "/mcp/sse")
275 | 
276 | 			// Try accessing the routes to see if they're available
277 | 			s.logger.Debug("Checking routes availability:")
278 | 			s.logger.Debug("To test routes, run: curl " + sseBaseURL + "/sse")
279 | 
280 | 			if err := sseServer.Start(listenAddr); err != nil {
281 | 				// Log the error before sending it to the channel
282 | 				s.logger.WithError(err).Error("SSE server failed to start or encountered a runtime error")
283 | 				errCh <- fmt.Errorf("SSE server error: %w", err)
284 | 			} else {
285 | 				// This part might only be reached on graceful shutdown without error
286 | 				s.logger.Info("SSE server stopped gracefully")
287 | 			}
288 | 		}()
289 | 
290 | 		// Wait for signal to shut down
291 | 		<-sigCh
292 | 		s.logger.Debug("Shutting down SSE server...")
293 | 		cancel()
294 | 		errCh <- nil
295 | 	} else {
296 | 		// Default to stdio transport
297 | 		go func() {
298 | 
299 | 			s.logger.Debug("STDIO server is running. Press Ctrl+C to stop.")
300 | 
301 | 			if err := mcpserver.ServeStdio(srv); err != nil {
302 | 				errCh <- fmt.Errorf("STDIO server error: %w", err)
303 | 			}
304 | 		}()
305 | 
306 | 		// Wait for signal to shut down
307 | 		<-sigCh
308 | 		s.logger.Debug("Shutting down STDIO server...")
309 | 		cancel()
310 | 		errCh <- nil
311 | 	}
312 | 
313 | 	// Wait for server to exit or error
314 | 	return <-errCh
315 | }
316 | 
317 | // registerNpmTool registers the npm version checking tool
318 | func (s *PackageVersionServer) registerNpmTool(srv *mcpserver.MCPServer) {
319 | 	// Create NPM handler with a logger that doesn't output to stdout/stderr in stdio mode
320 | 	npmHandler := handlers.NewNpmHandler(s.logger, s.sharedCache)
321 | 
322 | 	// Add NPM tool
323 | 	npmTool := mcp.NewTool("check_npm_versions",
324 | 		mcp.WithDescription("Check latest stable versions for npm packages"),
325 | 		mcp.WithObject("dependencies",
326 | 			mcp.Required(),
327 | 			mcp.Description("Required: Dependencies object from package.json (e.g., { \"dependencies\": { \"express\": \"^4.17.1\" } })"),
328 | 		),
329 | 		mcp.WithObject("constraints",
330 | 			mcp.Description("Optional constraints for specific packages"),
331 | 		),
332 | 	)
333 | 
334 | 	// Add NPM handler
335 | 	srv.AddTool(npmTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
336 | 		s.logger.WithField("tool", "check_npm_versions").Debug("Received request")
337 | 		return npmHandler.GetLatestVersion(ctx, request.Params.Arguments)
338 | 	})
339 | }
340 | 
341 | // registerPythonTools registers the Python version checking tools
342 | func (s *PackageVersionServer) registerPythonTools(srv *mcpserver.MCPServer) {
343 | 	// Create Python handler with a logger that doesn't output to stdout/stderr in stdio mode
344 | 	pythonHandler := handlers.NewPythonHandler(s.logger, s.sharedCache)
345 | 
346 | 	// Tool for requirements.txt
347 | 	pythonTool := mcp.NewTool("check_python_versions",
348 | 		mcp.WithDescription("Get the current, up to date Python package versions to use when adding or updating Python packages for requirements.txt"),
349 | 		mcp.WithArray("requirements",
350 | 			mcp.Required(),
351 | 			mcp.Description("Required: Array of one or more requirements from requirements.txt"),
352 | 			mcp.Items(map[string]interface{}{"type": "string"}),
353 | 		),
354 | 	)
355 | 
356 | 	// Add Python requirements.txt handler
357 | 	srv.AddTool(pythonTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
358 | 		s.logger.WithField("tool", "check_python_versions").Debug("Received request")
359 | 		return pythonHandler.GetLatestVersionFromRequirements(ctx, request.Params.Arguments)
360 | 	})
361 | 
362 | 	// Tool for pyproject.toml
363 | 	pyprojectTool := mcp.NewTool("check_pyproject_versions",
364 | 		mcp.WithDescription("Get the current, up to date Python package versions to use when adding or updating Python packages for pyproject.toml"),
365 | 		mcp.WithObject("dependencies",
366 | 			mcp.Required(),
367 | 			mcp.Description("Required: Dependencies object from pyproject.toml"),
368 | 		),
369 | 	)
370 | 
371 | 	// Add Python pyproject.toml handler
372 | 	srv.AddTool(pyprojectTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
373 | 		s.logger.WithField("tool", "check_pyproject_versions").Debug("Received request")
374 | 		return pythonHandler.GetLatestVersionFromPyProject(ctx, request.Params.Arguments)
375 | 	})
376 | }
377 | 
378 | // registerJavaTools registers the Java version checking tools
379 | func (s *PackageVersionServer) registerJavaTools(srv *mcpserver.MCPServer) {
380 | 	// Create Java handler with a logger that doesn't output to stdout/stderr in stdio mode
381 | 	javaHandler := handlers.NewJavaHandler(s.logger, s.sharedCache)
382 | 
383 | 	// Tool for Maven
384 | 	mavenTool := mcp.NewTool("check_maven_versions",
385 | 		mcp.WithDescription("Check latest stable versions for Java packages in pom.xml"),
386 | 		mcp.WithArray("dependencies",
387 | 			mcp.Required(),
388 | 			mcp.Description("Array of Maven dependencies"),
389 | 			mcp.Items(map[string]interface{}{"type": "object"}),
390 | 		),
391 | 	)
392 | 
393 | 	// Add Maven handler
394 | 	srv.AddTool(mavenTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
395 | 		s.logger.WithField("tool", "check_maven_versions").Debug("Received request")
396 | 		return javaHandler.GetLatestVersionFromMaven(ctx, request.Params.Arguments)
397 | 	})
398 | 
399 | 	// Tool for Gradle
400 | 	gradleTool := mcp.NewTool("check_gradle_versions",
401 | 		mcp.WithDescription("Get latest stable versions for Java packages in build.gradle"),
402 | 		mcp.WithArray("dependencies",
403 | 			mcp.Required(),
404 | 			mcp.Description("Array of Gradle dependencies"),
405 | 			mcp.Items(map[string]interface{}{"type": "object"}),
406 | 		),
407 | 	)
408 | 
409 | 	// Add Gradle handler
410 | 	srv.AddTool(gradleTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
411 | 		s.logger.WithField("tool", "check_gradle_versions").Debug("Received request")
412 | 		return javaHandler.GetLatestVersionFromGradle(ctx, request.Params.Arguments)
413 | 	})
414 | }
415 | 
416 | // registerGoTool registers the Go version checking tool
417 | func (s *PackageVersionServer) registerGoTool(srv *mcpserver.MCPServer) {
418 | 	// Create Go handler with a logger that doesn't output to stdout/stderr in stdio mode
419 | 	goHandler := handlers.NewGoHandler(s.logger, s.sharedCache)
420 | 
421 | 	goTool := mcp.NewTool("check_go_versions",
422 | 		mcp.WithDescription("Get the current, up to date package versions to use when adding Go packages or updating go.mod"),
423 | 		mcp.WithObject("dependencies",
424 | 			mcp.Required(),
425 | 			mcp.Description("Required: Dependencies from go.mod"),
426 | 		),
427 | 	)
428 | 
429 | 	// Add Go handler
430 | 	srv.AddTool(goTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
431 | 		s.logger.WithField("tool", "check_go_versions").Debug("Received request")
432 | 		return goHandler.GetLatestVersion(ctx, request.Params.Arguments)
433 | 	})
434 | }
435 | 
436 | // registerBedrockTools registers the AWS Bedrock tools
437 | func (s *PackageVersionServer) registerBedrockTools(srv *mcpserver.MCPServer) {
438 | 	// Create Bedrock handler with a logger that doesn't output to stdout/stderr in stdio mode
439 | 	bedrockHandler := handlers.NewBedrockHandler(s.logger, s.sharedCache)
440 | 
441 | 	// Tool for searching Bedrock models
442 | 	bedrockTool := mcp.NewTool("check_bedrock_models",
443 | 		mcp.WithDescription("Search, list, and get information about Amazon Bedrock models"),
444 | 		mcp.WithString("action",
445 | 			mcp.Description("Action to perform: list all models, search for models, or get a specific model"),
446 | 			mcp.Enum("list", "search", "get"),
447 | 			mcp.DefaultString("list"),
448 | 		),
449 | 		mcp.WithString("query",
450 | 			mcp.Description("Search query for model name or ID (used with action: \"search\")"),
451 | 		),
452 | 		mcp.WithString("provider",
453 | 			mcp.Description("Filter by provider name (used with action: \"search\")"),
454 | 		),
455 | 		mcp.WithString("region",
456 | 			mcp.Description("Filter by AWS region (used with action: \"search\")"),
457 | 		),
458 | 		mcp.WithString("modelId",
459 | 			mcp.Description("Model ID to retrieve (used with action: \"get\")"),
460 | 		),
461 | 	)
462 | 
463 | 	// Add Bedrock handler
464 | 	srv.AddTool(bedrockTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
465 | 		s.logger.WithFields(logrus.Fields{
466 | 			"tool":   "check_bedrock_models",
467 | 			"action": request.Params.Arguments["action"],
468 | 		}).Debug("Received request")
469 | 		return bedrockHandler.GetLatestVersion(ctx, request.Params.Arguments)
470 | 	})
471 | 
472 | 	// Tool for getting the latest Claude Sonnet model
473 | 	sonnetTool := mcp.NewTool("get_latest_bedrock_model",
474 | 		mcp.WithDescription("Return the latest Claude Sonnet model available on Amazon Bedrock (best for coding tasks)"),
475 | 	)
476 | 
477 | 	// Add Bedrock Claude Sonnet handler
478 | 	srv.AddTool(sonnetTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
479 | 		s.logger.WithField("tool", "get_latest_bedrock_model").Debug("Received request")
480 | 		// Set the action to get_latest_claude_sonnet to use the specialised method
481 | 		return bedrockHandler.GetLatestVersion(ctx, map[string]interface{}{
482 | 			"action": "get_latest_claude_sonnet",
483 | 		})
484 | 	})
485 | }
486 | 
487 | // registerDockerTool registers the Docker version checking tool
488 | func (s *PackageVersionServer) registerDockerTool(srv *mcpserver.MCPServer) {
489 | 	// Create Docker handler with a logger that doesn't output to stdout/stderr in stdio mode
490 | 	dockerHandler := handlers.NewDockerHandler(s.logger, s.sharedCache)
491 | 
492 | 	dockerTool := mcp.NewTool("check_docker_tags",
493 | 		mcp.WithDescription("Get the latest, up to date tags for Docker container images from Docker Hub, GitHub Container Registry, or custom registries for use when writing Dockerfiles or docker-compose files"),
494 | 		mcp.WithString("image",
495 | 			mcp.Required(),
496 | 			mcp.Description("Required: Docker image name (e.g., \"nginx\", \"ubuntu\", \"ghcr.io/owner/repo\")"),
497 | 		),
498 | 		mcp.WithString("registry",
499 | 			mcp.Description("Registry to check (dockerhub, ghcr, or custom)"),
500 | 			mcp.Enum("dockerhub", "ghcr", "custom"),
501 | 			mcp.DefaultString("dockerhub"),
502 | 		),
503 | 		mcp.WithString("customRegistry",
504 | 			mcp.Description("URL for custom registry (required when registry is \"custom\")"),
505 | 		),
506 | 		mcp.WithNumber("limit",
507 | 			mcp.Description("Maximum number of tags to return"),
508 | 			mcp.DefaultNumber(10),
509 | 		),
510 | 		mcp.WithArray("filterTags",
511 | 			mcp.Description("Array of regex patterns to filter tags"),
512 | 			mcp.Items(map[string]interface{}{"type": "string"}),
513 | 		),
514 | 		// If the above doesn't work, maybe try a deeper structure like this:
515 | 		// mcp.WithArray("filterTags",
516 | 		//
517 | 		//	mcp.Description("Array of regex patterns to filter tags"),
518 | 		//	mcp.Items(map[string]interface{}{
519 | 		//
520 | 		// "type": "string",
521 | 		// "description": "Regex pattern to filter Docker tags",
522 | 		//
523 | 		//	}),
524 | 		//
525 | 		// ),
526 | 		mcp.WithBoolean("includeDigest",
527 | 			mcp.Description("Include image digest in results"),
528 | 			mcp.DefaultBool(false),
529 | 		),
530 | 	)
531 | 
532 | 	// Add Docker handler
533 | 	srv.AddTool(dockerTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
534 | 		s.logger.WithFields(logrus.Fields{
535 | 			"tool":     "check_docker_tags",
536 | 			"image":    request.Params.Arguments["image"],
537 | 			"registry": request.Params.Arguments["registry"],
538 | 		}).Debug("Received request")
539 | 		return dockerHandler.GetLatestVersion(ctx, request.Params.Arguments)
540 | 	})
541 | }
542 | 
543 | // registerSwiftTool registers the Swift version checking tool
544 | func (s *PackageVersionServer) registerSwiftTool(srv *mcpserver.MCPServer) {
545 | 	// Create Swift handler with a logger that doesn't output to stdout/stderr in stdio mode
546 | 	swiftHandler := handlers.NewSwiftHandler(s.logger, s.sharedCache)
547 | 
548 | 	swiftTool := mcp.NewTool("check_swift_versions",
549 | 		mcp.WithDescription("Check latest stable versions for Swift packages in Package.swift"),
550 | 		mcp.WithArray("dependencies",
551 | 			mcp.Required(),
552 | 			mcp.Description("Required: Array of Swift package dependencies"),
553 | 			mcp.Items(map[string]interface{}{"type": "object"}),
554 | 		),
555 | 		mcp.WithObject("constraints",
556 | 			mcp.Description("Optional constraints for specific packages"),
557 | 		),
558 | 	)
559 | 
560 | 	// Add Swift handler
561 | 	srv.AddTool(swiftTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
562 | 		s.logger.WithField("tool", "check_swift_versions").Debug("Received request")
563 | 		return swiftHandler.GetLatestVersion(ctx, request.Params.Arguments)
564 | 	})
565 | }
566 | 
567 | // registerGitHubActionsTool registers the GitHub Actions version checking tool
568 | func (s *PackageVersionServer) registerGitHubActionsTool(srv *mcpserver.MCPServer) {
569 | 	// Create GitHub Actions handler with a logger that doesn't output to stdout/stderr in stdio mode
570 | 	githubActionsHandler := handlers.NewGitHubActionsHandler(s.logger, s.sharedCache)
571 | 
572 | 	githubActionsTool := mcp.NewTool("check_github_actions",
573 | 		mcp.WithDescription("Get the current, up to date GitHub Actions versions to use when adding or updating GitHub Actions"),
574 | 		mcp.WithArray("actions",
575 | 			mcp.Required(),
576 | 			mcp.Description("Required: Array of GitHub Actions to check"),
577 | 			mcp.Items(map[string]interface{}{"type": "object"}),
578 | 		),
579 | 		mcp.WithBoolean("includeDetails",
580 | 			mcp.Description("Include additional details like published date and URL"),
581 | 			mcp.DefaultBool(false),
582 | 		),
583 | 	)
584 | 
585 | 	// Add GitHub Actions handler
586 | 	srv.AddTool(githubActionsTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
587 | 		s.logger.WithField("tool", "check_github_actions").Debug("Received request")
588 | 		return githubActionsHandler.GetLatestVersion(ctx, request.Params.Arguments)
589 | 	})
590 | }
591 | 
```
Page 2/2FirstPrevNextLast