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 |
```