#
tokens: 44286/50000 9/60 files (page 2/2)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 2 of 2. Use http://codebase.md/arvindand/maven-tools-mcp?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .dockerignore
├── .gitattributes
├── .github
│   └── workflows
│       ├── ci.yml
│       └── docker.yml
├── .gitignore
├── .mvn
│   └── wrapper
│       └── maven-wrapper.properties
├── assets
│   └── demo.gif
├── build
│   ├── build-docker.cmd
│   ├── build-docker.sh
│   ├── build.cmd
│   ├── build.sh
│   └── README.md
├── CHANGELOG.md
├── CORPORATE-CERTIFICATES.md
├── docker-compose.yml
├── LICENSE
├── mvnw
├── mvnw.cmd
├── pom.xml
├── README.md
└── src
    ├── main
    │   ├── java
    │   │   └── com
    │   │       └── arvindand
    │   │           └── mcp
    │   │               └── maven
    │   │                   ├── config
    │   │                   │   ├── CacheConfig.java
    │   │                   │   ├── CacheConstants.java
    │   │                   │   ├── Context7Properties.java
    │   │                   │   ├── HttpClientConfig.java
    │   │                   │   ├── JacksonConfig.java
    │   │                   │   ├── MavenCentralProperties.java
    │   │                   │   ├── McpToolsConfig.java
    │   │                   │   └── NativeImageConfiguration.java
    │   │                   ├── MavenMcpServerApplication.java
    │   │                   ├── model
    │   │                   │   ├── BulkCheckResult.java
    │   │                   │   ├── Context7Guidance.java
    │   │                   │   ├── DependencyAge.java
    │   │                   │   ├── DependencyAgeAnalysis.java
    │   │                   │   ├── DependencyInfo.java
    │   │                   │   ├── MavenArtifact.java
    │   │                   │   ├── MavenCoordinate.java
    │   │                   │   ├── MavenMetadata.java
    │   │                   │   ├── McpError.java
    │   │                   │   ├── ProjectHealthAnalysis.java
    │   │                   │   ├── ReleasePatternAnalysis.java
    │   │                   │   ├── StabilityFilter.java
    │   │                   │   ├── ToolResponse.java
    │   │                   │   ├── VersionComparison.java
    │   │                   │   ├── VersionInfo.java
    │   │                   │   ├── VersionsByType.java
    │   │                   │   └── VersionTimelineAnalysis.java
    │   │                   ├── service
    │   │                   │   ├── MavenCentralException.java
    │   │                   │   ├── MavenCentralService.java
    │   │                   │   └── MavenDependencyTools.java
    │   │                   └── util
    │   │                       ├── MavenCoordinateParser.java
    │   │                       └── VersionComparator.java
    │   └── resources
    │       ├── application-docker.yaml
    │       ├── application-no-context7.yaml
    │       ├── application.yaml
    │       ├── logback-spring.xml
    │       └── META-INF
    │           └── additional-spring-configuration-metadata.json
    └── test
        ├── java
        │   └── com
        │       └── arvindand
        │           └── mcp
        │               └── maven
        │                   ├── config
        │                   │   └── Context7PropertiesTest.java
        │                   ├── MavenMcpServerIT.java
        │                   ├── model
        │                   │   └── Context7GuidanceTest.java
        │                   ├── service
        │                   │   ├── MavenCentralServiceRepositoryIT.java
        │                   │   ├── MavenCentralServiceUnitTest.java
        │                   │   ├── MavenDependencyToolsContext7EnabledIT.java
        │                   │   ├── MavenDependencyToolsIT.java
        │                   │   └── MavenDependencyToolsPerformanceIT.java
        │                   ├── TestHelpers.java
        │                   └── util
        │                       ├── MavenCoordinateParserTest.java
        │                       └── VersionComparatorTest.java
        └── resources
            └── application-test.yaml
```

# Files

--------------------------------------------------------------------------------
/CORPORATE-CERTIFICATES.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Corporate Certificate Guide
  2 | 
  3 | This guide explains how to build custom Docker images with your corporate SSL certificates for environments with SSL inspection/MITM proxies.
  4 | 
  5 | ## Problem
  6 | 
  7 | Corporate networks often use SSL inspection (MITM proxies) that intercept HTTPS traffic. This requires applications to trust the corporate CA certificates. If your environment blocks outbound connections to `https://mcp.context7.com`, you have two options:
  8 | 
  9 | 1. **Use the `-noc7` image variant** (simplest - no Context7 integration)
 10 | 2. **Build a custom image with your corporate certificates** (includes Context7 with custom certs)
 11 | 
 12 | This guide covers option 2.
 13 | 
 14 | ## Solution: Custom Certificate Binding
 15 | 
 16 | Spring Boot's Maven plugin supports [certificate bindings](https://docs.spring.io/spring-boot/maven-plugin/build-image.html) that inject your corporate certificates during the native image build process. The Paketo buildpacks automatically configure the JVM truststore with your certificates, which are then compiled into the native image.
 17 | 
 18 | ## Prerequisites
 19 | 
 20 | - Docker installed and running
 21 | - Java 24
 22 | - Maven 3.9+
 23 | - Your corporate CA certificate(s) in `.crt` or `.pem` format
 24 | 
 25 | ## Step-by-Step Instructions
 26 | 
 27 | ### 1. Prepare Certificate Directory
 28 | 
 29 | Create a directory structure for your certificates:
 30 | 
 31 | ```bash
 32 | mkdir certs
 33 | cd certs
 34 | ```
 35 | 
 36 | Create a `type` file (required by Paketo buildpacks):
 37 | 
 38 | ```bash
 39 | echo "ca-certificates" > type
 40 | ```
 41 | 
 42 | Add your corporate certificate(s) to this directory:
 43 | 
 44 | ```bash
 45 | # Copy your corporate CA certificate(s)
 46 | cp /path/to/your/corporate-ca.crt .
 47 | # You can add multiple certificates
 48 | cp /path/to/your/corporate-ca2.crt .
 49 | ```
 50 | 
 51 | **Important:** Only include `.crt` or `.pem` certificate files. Do NOT include private key files (`.key`, `.pem` with private keys).
 52 | 
 53 | Your `certs/` directory should look like:
 54 | 
 55 | ``` plaintext
 56 | certs/
 57 | ├── type
 58 | ├── corporate-ca.crt
 59 | └── corporate-ca2.crt (optional)
 60 | ```
 61 | 
 62 | ### 2. Configure Maven Plugin
 63 | 
 64 | Edit your `pom.xml` to add certificate bindings to the Spring Boot Maven plugin:
 65 | 
 66 | ```xml
 67 | <plugin>
 68 |     <groupId>org.springframework.boot</groupId>
 69 |     <artifactId>spring-boot-maven-plugin</artifactId>
 70 |     <configuration>
 71 |         <image>
 72 |             <env>
 73 |                 <BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE>
 74 |             </env>
 75 |             <bindings>
 76 |                 <!-- Bind your certs directory to the buildpack's certificate location -->
 77 |                 <binding>${project.basedir}/certs:/platform/bindings/ca-certificates</binding>
 78 |             </bindings>
 79 |         </image>
 80 |     </configuration>
 81 | </plugin>
 82 | ```
 83 | 
 84 | ### 3. Build Native Image
 85 | 
 86 | Build your custom native image with certificates and Context7 enabled:
 87 | 
 88 | ```bash
 89 | ./mvnw clean package -DskipTests
 90 | SPRING_PROFILES_ACTIVE=docker ./mvnw -Pnative spring-boot:build-image \
 91 |   -Dspring-boot.build-image.imageName=my-maven-tools-mcp:corporate
 92 | ```
 93 | 
 94 | **Build time:** 10-15 minutes for native image compilation
 95 | 
 96 | **Note:** This builds an image WITH Context7 integration. The custom certificates allow Context7 to work through your corporate SSL inspection. If you don't need Context7 at all, use the pre-built `latest-noc7` image instead (no custom build needed).
 97 | 
 98 | ### 4. Verify Certificate Inclusion
 99 | 
100 | You can verify that the buildpack processed your certificates by checking the build output:
101 | 
102 | ``` plaintext
103 | [creator]     Paketo Buildpack for CA Certificates 3.10.4
104 | [creator]       https://github.com/paketo-buildpacks/ca-certificates
105 | [creator]       Launch Helper: Contributing to layer
106 | [creator]       CA Certificates: Contributing to layer
107 | [creator]         Added 1 additional CA certificate(s) to system truststore
108 | ```
109 | 
110 | ### 5. Configure Claude Desktop
111 | 
112 | Update your Claude Desktop configuration to use the custom image:
113 | 
114 | **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
115 | **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
116 | **Linux:** `~/.config/Claude/claude_desktop_config.json`
117 | 
118 | ```json
119 | {
120 |   "mcpServers": {
121 |     "maven-tools": {
122 |       "command": "docker",
123 |       "args": [
124 |         "run", "-i", "--rm",
125 |         "my-maven-tools-mcp:corporate"
126 |       ]
127 |     }
128 |   }
129 | }
130 | ```
131 | 
132 | ### 6. Test the Image
133 | 
134 | Test your custom image:
135 | 
136 | ```bash
137 | # Quick test - should show MCP initialization
138 | echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | \
139 |   docker run -i --rm my-maven-tools-mcp:corporate
140 | ```
141 | 
142 | You should see a JSON-RPC response without SSL errors.
143 | 
144 | ## How It Works
145 | 
146 | 1. **Build-time Injection:** The Maven plugin binds your `certs/` directory to `/platform/bindings/ca-certificates` inside the build container
147 | 2. **Buildpack Processing:** The Paketo CA Certificates buildpack detects the binding and adds your certificates to the JVM truststore
148 | 3. **Native Compilation:** GraalVM native-image compiles the application with the updated truststore
149 | 4. **Runtime:** The native image trusts your corporate certificates without any runtime configuration
150 | 
151 | ## Profiles Explained
152 | 
153 | - `docker`: Disables Spring Boot banner (required for MCP protocol)
154 | 
155 | The custom certificate build uses the `docker` profile and **enables Context7 integration**. This is the whole point - your corporate certificates allow Context7 to work through SSL inspection.
156 | 
157 | If you don't need Context7 at all, skip this custom build and use the pre-built `latest-noc7` image instead.
158 | 
159 | ## Troubleshooting
160 | 
161 | ### Build fails with "failed to parse certificate"
162 | 
163 | **Problem:** You likely included a private key file (`.key` or `.pem` with private keys) in the `certs/` directory.
164 | 
165 | **Solution:** Remove all private key files. Only include certificate files (`.crt` or certificate-only `.pem` files).
166 | 
167 | ### Image still fails to connect to Context7
168 | 
169 | **Problem:** Your corporate proxy blocks `mcp.context7.com` entirely (domain/IP blocking, not just SSL inspection).
170 | 
171 | **Solution:** If the domain is blocked, custom certificates won't help. Use the pre-built `-noc7` image variant instead:
172 | 
173 | ```bash
174 | docker pull arvindand/maven-tools-mcp:latest-noc7
175 | ```
176 | 
177 | This image has no Context7 integration and doesn't attempt any outbound connections.
178 | 
179 | ### Build takes longer than expected
180 | 
181 | **Normal:** Native image compilation takes 10-15 minutes. This is expected for GraalVM native images.
182 | 
183 | ### Certificate not being picked up
184 | 
185 | **Check:**
186 | 
187 | 1. Ensure `type` file contains exactly: `ca-certificates`
188 | 2. Verify certificate files are in `.crt` or `.pem` format
189 | 3. Check that the binding path in `pom.xml` is correct: `${project.basedir}/certs:/platform/bindings/ca-certificates`
190 | 4. Look for the CA Certificates buildpack output in the build logs
191 | 
192 | ## Alternative: Use Pre-built `-noc7` Image
193 | 
194 | If you don't need Context7 integration, the simplest solution is to use the pre-built `-noc7` image variant:
195 | 
196 | ```json
197 | {
198 |   "mcpServers": {
199 |     "maven-tools": {
200 |       "command": "docker",
201 |       "args": [
202 |         "run", "-i", "--rm",
203 |         "arvindand/maven-tools-mcp:latest-noc7"
204 |       ]
205 |     }
206 |   }
207 | }
208 | ```
209 | 
210 | This image:
211 | 
212 | - ✅ Has no Context7 integration (no outbound connections to `mcp.context7.com`)
213 | - ✅ Works in environments with SSL inspection
214 | - ✅ Requires no custom build
215 | - ✅ Provides all Maven dependency analysis tools
216 | 
217 | ## References
218 | 
219 | - [Spring Boot Maven Plugin - Build Image](https://docs.spring.io/spring-boot/maven-plugin/build-image.html)
220 | - [Paketo CA Certificates Buildpack](https://github.com/paketo-buildpacks/ca-certificates)
221 | - [Paketo Service Bindings Specification](https://github.com/buildpacks/spec/blob/main/extensions/bindings.md)
222 | 
```

--------------------------------------------------------------------------------
/.github/workflows/docker.yml:
--------------------------------------------------------------------------------

```yaml
  1 | name: Build and Publish Multi-Architecture Docker Image
  2 | 
  3 | on:
  4 |   push:
  5 |     branches: [main, develop]
  6 |     tags: ["v*"]
  7 |   pull_request:
  8 |     branches: [main]
  9 | 
 10 | env:
 11 |   DOCKER_HUB_REPOSITORY: arvindand/maven-tools-mcp
 12 | 
 13 | jobs:
 14 |   build-amd64:
 15 |     runs-on: ubuntu-latest
 16 |     if: github.event_name != 'pull_request'
 17 |     steps:
 18 |       - name: Checkout repository
 19 |         uses: actions/checkout@v4
 20 | 
 21 |       - name: Set up Java 24
 22 |         uses: actions/setup-java@v4
 23 |         with:
 24 |           java-version: "24"
 25 |           distribution: "temurin"
 26 | 
 27 |       - name: Cache Maven dependencies
 28 |         uses: actions/cache@v4
 29 |         with:
 30 |           path: ~/.m2
 31 |           key: ${{ runner.os }}-amd64-m2-${{ hashFiles('**/pom.xml') }}
 32 |           restore-keys: ${{ runner.os }}-amd64-m2
 33 | 
 34 |       - name: Make mvnw executable
 35 |         run: chmod +x ./mvnw
 36 | 
 37 |       - name: Build with Maven
 38 |         run: ./mvnw clean package -DskipTests
 39 | 
 40 |       - name: Log in to Docker Hub
 41 |         uses: docker/login-action@v3
 42 |         with:
 43 |           username: ${{ secrets.DOCKER_HUB_USERNAME }}
 44 |           password: ${{ secrets.DOCKER_HUB_TOKEN }}
 45 | 
 46 |       - name: Build and push AMD64 images (with and without Context7)
 47 |         run: |
 48 |           PROJECT_VERSION=$(./mvnw help:evaluate -Dexpression=project.version -q -DforceStdout 2>/dev/null || echo "1.4.0")
 49 | 
 50 |           # Build AMD64 native image WITH Context7
 51 |           SPRING_PROFILES_ACTIVE=docker ./mvnw -Pnative spring-boot:build-image \
 52 |             -Dspring-boot.build-image.imageName=arvindand/maven-tools-mcp:${PROJECT_VERSION}-amd64 \
 53 |             -Dspring-boot.build-image.env.BP_NATIVE_IMAGE_BUILD_ARGUMENTS="-march=x86-64-v2 --no-fallback" \
 54 |             -Dspring-boot.build-image.publish=true \
 55 |             -Ddocker.publishRegistry.username="${{ secrets.DOCKER_HUB_USERNAME }}" \
 56 |             -Ddocker.publishRegistry.password="${{ secrets.DOCKER_HUB_TOKEN }}"
 57 | 
 58 |           # Build AMD64 native image WITHOUT Context7 (noc7 variant)
 59 |           SPRING_PROFILES_ACTIVE=docker,no-context7 ./mvnw -Pnative spring-boot:build-image \
 60 |             -Dspring-boot.build-image.imageName=arvindand/maven-tools-mcp:${PROJECT_VERSION}-noc7-amd64 \
 61 |             -Dspring-boot.build-image.env.BP_NATIVE_IMAGE_BUILD_ARGUMENTS="-march=x86-64-v2 --no-fallback" \
 62 |             -Dspring-boot.build-image.publish=true \
 63 |             -Ddocker.publishRegistry.username="${{ secrets.DOCKER_HUB_USERNAME }}" \
 64 |             -Ddocker.publishRegistry.password="${{ secrets.DOCKER_HUB_TOKEN }}"
 65 | 
 66 |   build-arm64:
 67 |     runs-on: ubuntu-24.04-arm # Free ARM64 runners for public repos
 68 |     if: github.event_name != 'pull_request'
 69 |     steps:
 70 |       - name: Checkout repository
 71 |         uses: actions/checkout@v4
 72 | 
 73 |       - name: Set up Java 24
 74 |         uses: actions/setup-java@v4
 75 |         with:
 76 |           java-version: "24"
 77 |           distribution: "temurin"
 78 | 
 79 |       - name: Cache Maven dependencies
 80 |         uses: actions/cache@v4
 81 |         with:
 82 |           path: ~/.m2
 83 |           key: ${{ runner.os }}-arm64-m2-${{ hashFiles('**/pom.xml') }}
 84 |           restore-keys: ${{ runner.os }}-arm64-m2
 85 | 
 86 |       - name: Make mvnw executable
 87 |         run: chmod +x ./mvnw
 88 | 
 89 |       - name: Build with Maven
 90 |         run: ./mvnw clean package -DskipTests
 91 | 
 92 |       - name: Log in to Docker Hub
 93 |         uses: docker/login-action@v3
 94 |         with:
 95 |           username: ${{ secrets.DOCKER_HUB_USERNAME }}
 96 |           password: ${{ secrets.DOCKER_HUB_TOKEN }}
 97 | 
 98 |       - name: Build and push ARM64 images (with and without Context7)
 99 |         run: |
100 |           PROJECT_VERSION=$(./mvnw help:evaluate -Dexpression=project.version -q -DforceStdout 2>/dev/null || echo "1.4.0")
101 | 
102 |           # Build ARM64 native image WITH Context7
103 |           SPRING_PROFILES_ACTIVE=docker ./mvnw -Pnative spring-boot:build-image \
104 |             -Dspring-boot.build-image.imageName=arvindand/maven-tools-mcp:${PROJECT_VERSION}-arm64 \
105 |             -Dspring-boot.build-image.publish=true \
106 |             -Ddocker.publishRegistry.username="${{ secrets.DOCKER_HUB_USERNAME }}" \
107 |             -Ddocker.publishRegistry.password="${{ secrets.DOCKER_HUB_TOKEN }}"
108 | 
109 |           # Build ARM64 native image WITHOUT Context7 (noc7 variant)
110 |           SPRING_PROFILES_ACTIVE=docker,no-context7 ./mvnw -Pnative spring-boot:build-image \
111 |             -Dspring-boot.build-image.imageName=arvindand/maven-tools-mcp:${PROJECT_VERSION}-noc7-arm64 \
112 |             -Dspring-boot.build-image.publish=true \
113 |             -Ddocker.publishRegistry.username="${{ secrets.DOCKER_HUB_USERNAME }}" \
114 |             -Ddocker.publishRegistry.password="${{ secrets.DOCKER_HUB_TOKEN }}"
115 | 
116 |   create-manifest:
117 |     runs-on: ubuntu-latest
118 |     needs: [build-amd64, build-arm64]
119 |     if: github.event_name != 'pull_request'
120 |     steps:
121 |       - name: Checkout repository
122 |         uses: actions/checkout@v4
123 | 
124 |       - name: Make mvnw executable
125 |         run: chmod +x ./mvnw
126 | 
127 |       - name: Log in to Docker Hub
128 |         uses: docker/login-action@v3
129 |         with:
130 |           username: ${{ secrets.DOCKER_HUB_USERNAME }}
131 |           password: ${{ secrets.DOCKER_HUB_TOKEN }}
132 | 
133 |       - name: Create and push multi-architecture manifests
134 |         run: |
135 |           PROJECT_VERSION=$(./mvnw help:evaluate -Dexpression=project.version -q -DforceStdout 2>/dev/null || echo "1.4.0")
136 | 
137 |           # Create multi-architecture manifest for version tag (WITH Context7)
138 |           docker manifest create arvindand/maven-tools-mcp:${PROJECT_VERSION} \
139 |             --amend arvindand/maven-tools-mcp:${PROJECT_VERSION}-amd64 \
140 |             --amend arvindand/maven-tools-mcp:${PROJECT_VERSION}-arm64
141 |           docker manifest push arvindand/maven-tools-mcp:${PROJECT_VERSION}
142 | 
143 |           # Create multi-architecture manifest for latest tag (WITH Context7)
144 |           docker manifest create arvindand/maven-tools-mcp:latest \
145 |             --amend arvindand/maven-tools-mcp:${PROJECT_VERSION}-amd64 \
146 |             --amend arvindand/maven-tools-mcp:${PROJECT_VERSION}-arm64
147 |           docker manifest push arvindand/maven-tools-mcp:latest
148 | 
149 |           # Create multi-architecture manifest for noc7 version tag (WITHOUT Context7)
150 |           docker manifest create arvindand/maven-tools-mcp:${PROJECT_VERSION}-noc7 \
151 |             --amend arvindand/maven-tools-mcp:${PROJECT_VERSION}-noc7-amd64 \
152 |             --amend arvindand/maven-tools-mcp:${PROJECT_VERSION}-noc7-arm64
153 |           docker manifest push arvindand/maven-tools-mcp:${PROJECT_VERSION}-noc7
154 | 
155 |           # Create multi-architecture manifest for latest-noc7 tag (WITHOUT Context7)
156 |           docker manifest create arvindand/maven-tools-mcp:latest-noc7 \
157 |             --amend arvindand/maven-tools-mcp:${PROJECT_VERSION}-noc7-amd64 \
158 |             --amend arvindand/maven-tools-mcp:${PROJECT_VERSION}-noc7-arm64
159 |           docker manifest push arvindand/maven-tools-mcp:latest-noc7
160 | 
161 |   test-pr:
162 |     runs-on: ubuntu-latest
163 |     if: github.event_name == 'pull_request'
164 |     steps:
165 |       - name: Checkout repository
166 |         uses: actions/checkout@v4
167 | 
168 |       - name: Set up Java 24
169 |         uses: actions/setup-java@v4
170 |         with:
171 |           java-version: "24"
172 |           distribution: "temurin"
173 | 
174 |       - name: Cache Maven dependencies
175 |         uses: actions/cache@v4
176 |         with:
177 |           path: ~/.m2
178 |           key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
179 |           restore-keys: ${{ runner.os }}-m2
180 | 
181 |       - name: Make mvnw executable
182 |         run: chmod +x ./mvnw
183 | 
184 |       - name: Build and test PR
185 |         run: |
186 |           ./mvnw clean package -DskipTests
187 | 
188 |           # Build with Context7 (default)
189 |           SPRING_PROFILES_ACTIVE=docker ./mvnw -Pnative spring-boot:build-image \
190 |             -Dspring-boot.build-image.imageName=maven-tools-mcp:pr-${{ github.event.number }}
191 | 
192 |           # Build without Context7 (noc7 variant)
193 |           SPRING_PROFILES_ACTIVE=docker,no-context7 ./mvnw -Pnative spring-boot:build-image \
194 |             -Dspring-boot.build-image.imageName=maven-tools-mcp:pr-${{ github.event.number }}-noc7
195 | 
196 |           # Test both images
197 |           echo "Testing image with Context7..."
198 |           timeout 30s docker run --rm maven-tools-mcp:pr-${{ github.event.number }} --help || true
199 | 
200 |           echo "Testing image without Context7..."
201 |           timeout 30s docker run --rm maven-tools-mcp:pr-${{ github.event.number }}-noc7 --help || true
202 | 
```

--------------------------------------------------------------------------------
/src/main/java/com/arvindand/mcp/maven/service/MavenCentralService.java:
--------------------------------------------------------------------------------

```java
  1 | package com.arvindand.mcp.maven.service;
  2 | 
  3 | import static com.arvindand.mcp.maven.config.CacheConstants.*;
  4 | 
  5 | import com.arvindand.mcp.maven.config.MavenCentralProperties;
  6 | import com.arvindand.mcp.maven.model.MavenArtifact;
  7 | import com.arvindand.mcp.maven.model.MavenCoordinate;
  8 | import com.arvindand.mcp.maven.model.MavenMetadata;
  9 | import com.arvindand.mcp.maven.util.VersionComparator;
 10 | import com.fasterxml.jackson.dataformat.xml.XmlMapper;
 11 | import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
 12 | import io.github.resilience4j.ratelimiter.annotation.RateLimiter;
 13 | import io.github.resilience4j.retry.annotation.Retry;
 14 | import java.util.Collections;
 15 | import java.util.List;
 16 | import java.util.Optional;
 17 | import java.util.concurrent.CompletableFuture;
 18 | import java.util.concurrent.ExecutorService;
 19 | import java.util.concurrent.Executors;
 20 | import org.slf4j.Logger;
 21 | import org.slf4j.LoggerFactory;
 22 | import org.springframework.cache.annotation.Cacheable;
 23 | import org.springframework.stereotype.Service;
 24 | import org.springframework.web.client.RestClient;
 25 | 
 26 | /**
 27 |  * Service for interacting with Maven Central via direct repository metadata access. Fetches
 28 |  * maven-metadata.xml files directly for accurate version information.
 29 |  *
 30 |  * @author Arvind Menon
 31 |  * @since 0.1.0
 32 |  */
 33 | @Service
 34 | public class MavenCentralService {
 35 | 
 36 |   private static final Logger logger = LoggerFactory.getLogger(MavenCentralService.class);
 37 |   private static final String METADATA_FETCH_ERROR_MSG =
 38 |       "Repository metadata fetch failed for {}:{}";
 39 |   private static final int ACCURATE_TIMESTAMP_VERSION_LIMIT = 30;
 40 |   private final RestClient restClient;
 41 |   private final XmlMapper xmlMapper;
 42 |   private final MavenCentralProperties properties;
 43 |   private final VersionComparator versionComparator;
 44 |   private final ExecutorService virtualThreadExecutor = Executors.newVirtualThreadPerTaskExecutor();
 45 | 
 46 |   public MavenCentralService(MavenCentralProperties properties, RestClient mavenCentralRestClient) {
 47 |     this.properties = properties;
 48 |     this.restClient = mavenCentralRestClient;
 49 |     this.xmlMapper = new XmlMapper();
 50 |     this.versionComparator = new VersionComparator();
 51 |   }
 52 | 
 53 |   /**
 54 |    * Gets the latest version for a Maven coordinate. Leverages cached results from getAllVersions()
 55 |    * for efficiency.
 56 |    *
 57 |    * @param coordinate the Maven coordinate
 58 |    * @return the latest version or null if not found
 59 |    */
 60 |   public String getLatestVersion(MavenCoordinate coordinate) {
 61 |     List<String> versions = getAllVersions(coordinate);
 62 |     return versions.isEmpty() ? null : versions.get(0);
 63 |   }
 64 | 
 65 |   /**
 66 |    * Checks if a specific version exists for a Maven coordinate.
 67 |    *
 68 |    * @param coordinate the Maven coordinate
 69 |    * @param version the version to check
 70 |    * @return true if the version exists
 71 |    */
 72 |   @Cacheable(
 73 |       value = MAVEN_VERSION_CHECKS,
 74 |       key =
 75 |           "#coordinate.groupId() + ':' + #coordinate.artifactId() + ':' + #version + ':' +"
 76 |               + " (#coordinate.packaging() ?: 'jar')")
 77 |   public boolean checkVersionExists(MavenCoordinate coordinate, String version) {
 78 |     try {
 79 |       Optional<MavenMetadata> metadata = fetchRepositoryMetadata(coordinate);
 80 |       if (metadata.isPresent() && metadata.get().hasValidVersioning()) {
 81 |         return metadata.get().versioning().getVersionStrings().contains(version);
 82 |       }
 83 |       return false;
 84 |     } catch (Exception e) {
 85 |       logger.debug(METADATA_FETCH_ERROR_MSG, coordinate.groupId(), coordinate.artifactId(), e);
 86 |       return false;
 87 |     }
 88 |   }
 89 | 
 90 |   /**
 91 |    * Gets all available versions for a Maven coordinate.
 92 |    *
 93 |    * @param coordinate the Maven coordinate
 94 |    * @return list of all versions, sorted by version descending
 95 |    */
 96 |   @Cacheable(
 97 |       value = MAVEN_ALL_VERSIONS,
 98 |       key =
 99 |           "#coordinate.groupId() + ':' + #coordinate.artifactId() + ':' + (#coordinate.packaging()"
100 |               + " ?: 'jar')")
101 |   @CircuitBreaker(name = "maven-central", fallbackMethod = "getAllVersionsFallback")
102 |   @Retry(name = "maven-central")
103 |   @RateLimiter(name = "maven-central")
104 |   public List<String> getAllVersions(MavenCoordinate coordinate) {
105 |     return fetchAllVersionsInternal(coordinate);
106 |   }
107 | 
108 |   @SuppressWarnings("unused") // Used via @CircuitBreaker fallbackMethod
109 |   private List<String> getAllVersionsFallback(MavenCoordinate coordinate, Exception ex) {
110 |     logger.warn(
111 |         "Circuit breaker fallback for {}:{} - {}",
112 |         coordinate.groupId(),
113 |         coordinate.artifactId(),
114 |         ex.getMessage());
115 |     return Collections.emptyList();
116 |   }
117 | 
118 |   /**
119 |    * Gets all available versions with accurate timestamps for the most recent versions.
120 |    *
121 |    * @param coordinate the Maven coordinate
122 |    * @return list of artifacts with accurate timestamp information for recent versions
123 |    */
124 |   public List<MavenArtifact> getAllVersionsWithTimestamps(MavenCoordinate coordinate) {
125 |     return getRecentVersionsWithAccurateTimestamps(coordinate, ACCURATE_TIMESTAMP_VERSION_LIMIT);
126 |   }
127 | 
128 |   /**
129 |    * Gets version information with accurate timestamps for the specified number of recent versions.
130 |    *
131 |    * @param coordinate the Maven coordinate
132 |    * @param maxVersions maximum number of versions to retrieve
133 |    * @return list of recent artifacts with accurate timestamp information
134 |    */
135 |   @Cacheable(
136 |       value = MAVEN_ACCURATE_HISTORICAL_DATA,
137 |       key =
138 |           "#coordinate.groupId() + ':' + #coordinate.artifactId() + ':' + #maxVersions + ':' +"
139 |               + " (#coordinate.packaging() ?: 'jar')")
140 |   public List<MavenArtifact> getRecentVersionsWithAccurateTimestamps(
141 |       MavenCoordinate coordinate, int maxVersions) {
142 |     List<String> allVersions = getAllVersions(coordinate);
143 |     List<String> recentVersions = allVersions.stream().limit(maxVersions).toList();
144 | 
145 |     List<CompletableFuture<MavenArtifact>> futures =
146 |         recentVersions.stream()
147 |             .map(
148 |                 version ->
149 |                     CompletableFuture.supplyAsync(
150 |                         () -> fetchArtifactWithTimestamp(coordinate, version),
151 |                         virtualThreadExecutor))
152 |             .toList();
153 | 
154 |     return futures.stream()
155 |         .map(CompletableFuture::join)
156 |         .filter(java.util.Objects::nonNull)
157 |         .sorted((a, b) -> versionComparator.reversed().compare(a.version(), b.version()))
158 |         .toList();
159 |   }
160 | 
161 |   private MavenArtifact fetchArtifactWithTimestamp(MavenCoordinate coordinate, String version) {
162 |     String pomUrl = buildPomUrl(coordinate, version);
163 |     try {
164 |       long timestamp =
165 |           restClient
166 |               .head()
167 |               .uri(pomUrl)
168 |               .retrieve()
169 |               .toBodilessEntity()
170 |               .getHeaders()
171 |               .getLastModified();
172 | 
173 |       return new MavenArtifact(
174 |           coordinate.groupId() + ":" + coordinate.artifactId() + ":" + version,
175 |           coordinate.groupId(),
176 |           coordinate.artifactId(),
177 |           version,
178 |           coordinate.packaging() != null ? coordinate.packaging() : "jar",
179 |           timestamp);
180 |     } catch (Exception e) {
181 |       logger.debug("Failed to fetch timestamp for {}:{}", pomUrl, e.getMessage());
182 |       return null;
183 |     }
184 |   }
185 | 
186 |   /**
187 |    * Internal method to fetch all versions without caching (used by cacheable public methods).
188 |    *
189 |    * @param coordinate the Maven coordinate
190 |    * @return list of all versions, sorted by version descending
191 |    */
192 |   private List<String> fetchAllVersionsInternal(MavenCoordinate coordinate) {
193 |     try {
194 |       Optional<MavenMetadata> metadata = fetchRepositoryMetadata(coordinate);
195 |       if (metadata.isPresent() && metadata.get().hasValidVersioning()) {
196 |         List<String> versions = metadata.get().versioning().getVersionStrings();
197 |         return versions.stream()
198 |             .sorted(versionComparator.reversed())
199 |             .limit(properties.maxResults())
200 |             .toList();
201 |       }
202 |       return Collections.emptyList();
203 |     } catch (Exception e) {
204 |       logger.debug(METADATA_FETCH_ERROR_MSG, coordinate.groupId(), coordinate.artifactId(), e);
205 |       return Collections.emptyList();
206 |     }
207 |   }
208 | 
209 |   /**
210 |    * Fetches maven-metadata.xml from the repository for the given coordinate.
211 |    *
212 |    * @param coordinate the Maven coordinate
213 |    * @return optional containing metadata if found and parseable
214 |    */
215 |   @CircuitBreaker(name = "maven-central")
216 |   @Retry(name = "maven-central")
217 |   @RateLimiter(name = "maven-central")
218 |   private Optional<MavenMetadata> fetchRepositoryMetadata(MavenCoordinate coordinate) {
219 |     try {
220 |       String metadataUrl = buildMetadataUrl(coordinate);
221 |       logger.debug("Fetching metadata from: {}", metadataUrl);
222 | 
223 |       String xmlContent = restClient.get().uri(metadataUrl).retrieve().body(String.class);
224 | 
225 |       if (xmlContent != null && !xmlContent.trim().isEmpty()) {
226 |         MavenMetadata metadata = xmlMapper.readValue(xmlContent, MavenMetadata.class);
227 |         return Optional.of(metadata);
228 |       }
229 | 
230 |       return Optional.empty();
231 |     } catch (Exception e) {
232 |       logger.debug(
233 |           "Failed to fetch metadata for {}:{}: {}",
234 |           coordinate.groupId(),
235 |           coordinate.artifactId(),
236 |           e.getMessage());
237 |       return Optional.empty();
238 |     }
239 |   }
240 | 
241 |   /**
242 |    * Builds the URL for maven-metadata.xml for the given coordinate.
243 |    *
244 |    * @param coordinate the Maven coordinate
245 |    * @return the metadata URL
246 |    */
247 |   private String buildMetadataUrl(MavenCoordinate coordinate) {
248 |     String groupPath = coordinate.groupId().replace('.', '/');
249 |     return String.format(
250 |         "%s/%s/%s/maven-metadata.xml",
251 |         properties.repositoryBaseUrl(), groupPath, coordinate.artifactId());
252 |   }
253 | 
254 |   private String buildPomUrl(MavenCoordinate coordinate, String version) {
255 |     String groupPath = coordinate.groupId().replace('.', '/');
256 |     return String.format(
257 |         "%s/%s/%s/%s/%s-%s.pom",
258 |         properties.repositoryBaseUrl(),
259 |         groupPath,
260 |         coordinate.artifactId(),
261 |         version,
262 |         coordinate.artifactId(),
263 |         version);
264 |   }
265 | }
266 | 
```

--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------

```
  1 | <?xml version="1.0" encoding="UTF-8"?>
  2 | <project xmlns="http://maven.apache.org/POM/4.0.0"
  3 |     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4 |     xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  5 |     <modelVersion>4.0.0</modelVersion>
  6 |     <parent>
  7 |         <groupId>org.springframework.boot</groupId>
  8 |         <artifactId>spring-boot-starter-parent</artifactId>
  9 |         <version>3.5.6</version>
 10 |         <relativePath /> <!-- lookup parent from repository -->
 11 |     </parent>
 12 |     <groupId>com.arvindand.mcp</groupId>
 13 |     <artifactId>maven-tools-mcp</artifactId>
 14 |     <version>1.5.1</version>
 15 |     <name>maven-tools-mcp</name>
 16 |     <description>Maven Tools MCP server with universal JVM dependency intelligence. Provides comprehensive dependency analysis for any build tool (Maven, Gradle, SBT, Mill) using Maven Central Repository. Works with Claude Desktop, GitHub Copilot, and other MCP clients.</description>
 17 |     <url>https://github.com/arvindand/maven-tools-mcp</url>
 18 |     <licenses>
 19 |         <license>
 20 |             <name>MIT License</name>
 21 |             <url>https://opensource.org/licenses/MIT</url>
 22 |         </license>
 23 |     </licenses>
 24 |     <developers>
 25 |         <developer>
 26 |             <name>Arvind Menon</name>
 27 |             <email>[email protected]</email>
 28 |         </developer>
 29 |     </developers>
 30 |     <scm>
 31 |         <connection>scm:git:git://github.com/arvindand/maven-tools-mcp.git</connection>
 32 |         <developerConnection>scm:git:ssh://github.com:arvindand/maven-tools-mcp.git</developerConnection>
 33 |         <tag>HEAD</tag>
 34 |         <url>https://github.com/arvindand/maven-tools-mcp</url>
 35 |     </scm>
 36 |     <properties>
 37 |         <java.version>24</java.version>
 38 |         <spring-ai.version>1.1.0-M3</spring-ai.version>
 39 |         <fmt-maven-plugin.version>2.29</fmt-maven-plugin.version>
 40 |         <maven-artifact.version>3.9.11</maven-artifact.version>
 41 |         <okhttp.version>5.2.1</okhttp.version>
 42 |         <resilience4j.version>2.3.0</resilience4j.version>
 43 |         <!-- Test execution properties -->
 44 |         <skipUTs>false</skipUTs>
 45 |         <skipITs>false</skipITs>
 46 |     </properties>
 47 |     <dependencies>
 48 |         <!-- MCP Server -->
 49 |         <dependency>
 50 |             <groupId>org.springframework.ai</groupId>
 51 |             <artifactId>spring-ai-starter-mcp-server</artifactId>
 52 |         </dependency>
 53 |         <dependency>
 54 |             <groupId>org.springframework.ai</groupId>
 55 |             <artifactId>spring-ai-starter-mcp-client-webflux</artifactId>
 56 |         </dependency>
 57 |         <!-- Caching support -->
 58 |         <dependency>
 59 |             <groupId>org.springframework.boot</groupId>
 60 |             <artifactId>spring-boot-starter-cache</artifactId>
 61 |         </dependency>
 62 | 
 63 |         <!-- Caffeine cache implementation -->
 64 |         <dependency>
 65 |             <groupId>com.github.ben-manes.caffeine</groupId>
 66 |             <artifactId>caffeine</artifactId>
 67 |         </dependency>
 68 | 
 69 |         <!-- Maven version comparison library -->
 70 |         <dependency>
 71 |             <groupId>org.apache.maven</groupId>
 72 |             <artifactId>maven-artifact</artifactId>
 73 |             <version>${maven-artifact.version}</version>
 74 |         </dependency>
 75 | 
 76 |         <!-- Jackson for JSON processing -->
 77 |         <dependency>
 78 |             <groupId>org.springframework.boot</groupId>
 79 |             <artifactId>spring-boot-starter-json</artifactId>
 80 |         </dependency>
 81 | 
 82 |         <!-- Jackson JDK8 module for Optional support -->
 83 |         <dependency>
 84 |             <groupId>com.fasterxml.jackson.datatype</groupId>
 85 |             <artifactId>jackson-datatype-jdk8</artifactId>
 86 |         </dependency>
 87 | 
 88 |         <!-- Jackson JSR310 module for Java 8 time types -->
 89 |         <dependency>
 90 |             <groupId>com.fasterxml.jackson.datatype</groupId>
 91 |             <artifactId>jackson-datatype-jsr310</artifactId>
 92 |         </dependency>
 93 | 
 94 |         <!-- Jackson XML module for maven-metadata.xml parsing -->
 95 |         <dependency>
 96 |             <groupId>com.fasterxml.jackson.dataformat</groupId>
 97 |             <artifactId>jackson-dataformat-xml</artifactId>
 98 |         </dependency>
 99 | 
100 |         <!-- OkHttp for HTTP/2 and connection pooling -->
101 |         <dependency>
102 |             <groupId>com.squareup.okhttp3</groupId>
103 |             <artifactId>okhttp-jvm</artifactId>
104 |             <version>${okhttp.version}</version>
105 |         </dependency>
106 | 
107 |         <!-- Resilience4j for circuit breaker, retry, rate limiter -->
108 |         <dependency>
109 |             <groupId>io.github.resilience4j</groupId>
110 |             <artifactId>resilience4j-spring-boot3</artifactId>
111 |             <version>${resilience4j.version}</version>
112 |         </dependency>
113 | 
114 |         <!-- AOP for @CircuitBreaker, @Retry, @RateLimiter annotations -->
115 |         <dependency>
116 |             <groupId>org.springframework.boot</groupId>
117 |             <artifactId>spring-boot-starter-aop</artifactId>
118 |         </dependency>
119 | 
120 |         <!-- Test dependencies -->
121 |         <dependency>
122 |             <groupId>org.springframework.boot</groupId>
123 |             <artifactId>spring-boot-starter-test</artifactId>
124 |             <scope>test</scope>
125 |         </dependency>
126 | 
127 |     </dependencies>
128 |     <dependencyManagement>
129 |         <dependencies>
130 |             <dependency>
131 |                 <groupId>org.springframework.ai</groupId>
132 |                 <artifactId>spring-ai-bom</artifactId>
133 |                 <version>${spring-ai.version}</version>
134 |                 <type>pom</type>
135 |                 <scope>import</scope>
136 |             </dependency>
137 |         </dependencies>
138 |     </dependencyManagement>
139 | 
140 |     <profiles>
141 |         <!-- CI Profile: Only unit tests, no integration tests -->
142 |         <profile>
143 |             <id>ci</id>
144 |             <properties>
145 |                 <skipITs>true</skipITs>
146 |                 <skipUTs>false</skipUTs>
147 |             </properties>
148 |         </profile>
149 | 
150 |         <!-- Integration Profile: Run integration tests only -->
151 |         <profile>
152 |             <id>integration</id>
153 |             <properties>
154 |                 <skipITs>false</skipITs>
155 |                 <skipUTs>true</skipUTs>
156 |             </properties>
157 |         </profile>
158 | 
159 |         <!-- Full Profile: Run all tests -->
160 |         <profile>
161 |             <id>full</id>
162 |             <properties>
163 |                 <skipITs>false</skipITs>
164 |                 <skipUTs>false</skipUTs>
165 |             </properties>
166 |         </profile>
167 |     </profiles>
168 | 
169 |     <build>
170 |         <plugins>
171 |             <plugin>
172 |                 <groupId>org.graalvm.buildtools</groupId>
173 |                 <artifactId>native-maven-plugin</artifactId>
174 |             </plugin>
175 |             <plugin>
176 |                 <groupId>org.springframework.boot</groupId>
177 |                 <artifactId>spring-boot-maven-plugin</artifactId>
178 |                 <configuration>
179 |                     <image>
180 |                         <env>
181 |                             <BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE>
182 |                         </env>
183 |                     </image>
184 |                     <docker>
185 |                         <publishRegistry>
186 |                             <username>${docker.publishRegistry.username}</username>
187 |                             <password>${docker.publishRegistry.password}</password>
188 |                             <url>https://index.docker.io/v1/</url>
189 |                         </publishRegistry>
190 |                     </docker>
191 |                 </configuration>
192 |             </plugin>
193 | 
194 |             <!-- Surefire for Unit Tests -->
195 |             <plugin>
196 |                 <groupId>org.apache.maven.plugins</groupId>
197 |                 <artifactId>maven-surefire-plugin</artifactId>
198 |                 <configuration>
199 |                     <!-- Run only unit tests (exclude integration tests) -->
200 |                     <excludes>
201 |                         <exclude>**/*IntegrationTest.java</exclude>
202 |                         <exclude>**/*IT.java</exclude>
203 |                     </excludes>
204 |                     <!-- Prevent hanging issues in CI -->
205 |                     <forkedProcessTimeoutInSeconds>60</forkedProcessTimeoutInSeconds>
206 |                     <forkedProcessExitTimeoutInSeconds>30</forkedProcessExitTimeoutInSeconds>
207 |                 </configuration>
208 |             </plugin>
209 |             <!-- Failsafe for Integration Tests -->
210 |             <plugin>
211 |                 <groupId>org.apache.maven.plugins</groupId>
212 |                 <artifactId>maven-failsafe-plugin</artifactId>
213 |                 <configuration>
214 |                     <!-- Run only integration tests -->
215 |                     <includes>
216 |                         <include>**/*IntegrationTest.java</include>
217 |                         <include>**/*IT.java</include>
218 |                     </includes>
219 |                 </configuration>
220 |                 <executions>
221 |                     <execution>
222 |                         <goals>
223 |                             <goal>integration-test</goal>
224 |                             <goal>verify</goal>
225 |                         </goals>
226 |                     </execution>
227 |                 </executions>
228 |             </plugin>
229 |             <!-- Spotify fmt plugin for Google Java Format -->
230 |             <plugin>
231 |                 <groupId>com.spotify.fmt</groupId>
232 |                 <artifactId>fmt-maven-plugin</artifactId>
233 |                 <version>${fmt-maven-plugin.version}</version>
234 |                 <configuration>
235 |                     <!-- Format source and test files -->
236 |                     <sourceDirectory>src/main/java</sourceDirectory>
237 |                     <testSourceDirectory>src/test/java</testSourceDirectory>
238 |                     <!-- Use Google Java Format style -->
239 |                     <style>google</style>
240 |                 </configuration>
241 |                 <executions>
242 |                     <execution>
243 |                         <goals>
244 |                             <goal>check</goal>
245 |                         </goals>
246 |                     </execution>
247 |                 </executions>
248 |             </plugin>
249 |         </plugins>
250 |     </build>
251 |     <repositories>
252 |         <repository>
253 |             <id>spring-snapshots</id>
254 |             <name>Spring Snapshots</name>
255 |             <url>https://repo.spring.io/snapshot</url>
256 |             <snapshots>
257 |                 <enabled>true</enabled>
258 |             </snapshots>
259 |             <releases>
260 |                 <enabled>false</enabled>
261 |             </releases>
262 |         </repository>
263 |         <repository>
264 |             <id>spring-milestones</id>
265 |             <name>Spring Milestones</name>
266 |             <url>https://repo.spring.io/milestone</url>
267 |             <snapshots>
268 |                 <enabled>false</enabled>
269 |             </snapshots>
270 |         </repository>
271 |     </repositories>
272 | 
273 | </project>
```

--------------------------------------------------------------------------------
/src/test/java/com/arvindand/mcp/maven/util/MavenCoordinateParserTest.java:
--------------------------------------------------------------------------------

```java
  1 | package com.arvindand.mcp.maven.util;
  2 | 
  3 | import static org.assertj.core.api.Assertions.assertThat;
  4 | import static org.assertj.core.api.Assertions.assertThatThrownBy;
  5 | 
  6 | import com.arvindand.mcp.maven.model.MavenCoordinate;
  7 | import java.util.stream.Stream;
  8 | import org.junit.jupiter.api.Test;
  9 | import org.junit.jupiter.params.ParameterizedTest;
 10 | import org.junit.jupiter.params.provider.Arguments;
 11 | import org.junit.jupiter.params.provider.MethodSource;
 12 | import org.junit.jupiter.params.provider.ValueSource;
 13 | 
 14 | /**
 15 |  * Comprehensive unit tests for MavenCoordinateParser.
 16 |  *
 17 |  * @author Arvind Menon
 18 |  * @since 0.1.0
 19 |  */
 20 | class MavenCoordinateParserTest {
 21 | 
 22 |   /** Test data for valid coordinate parsing. */
 23 |   private static Stream<Arguments> validCoordinateTestData() {
 24 |     return Stream.of(
 25 |         // Basic groupId:artifactId
 26 |         Arguments.of(
 27 |             "org.springframework:spring-core",
 28 |             "org.springframework",
 29 |             "spring-core",
 30 |             null,
 31 |             null,
 32 |             null),
 33 | 
 34 |         // With version
 35 |         Arguments.of(
 36 |             "org.springframework:spring-core:6.1.4",
 37 |             "org.springframework",
 38 |             "spring-core",
 39 |             "6.1.4",
 40 |             null,
 41 |             null),
 42 | 
 43 |         // With version and packaging
 44 |         Arguments.of(
 45 |             "org.springframework:spring-core:6.1.4:jar",
 46 |             "org.springframework",
 47 |             "spring-core",
 48 |             "6.1.4",
 49 |             "jar",
 50 |             null),
 51 | 
 52 |         // With version, packaging, and classifier
 53 |         Arguments.of(
 54 |             "org.springframework:spring-core:6.1.4:jar:sources",
 55 |             "org.springframework",
 56 |             "spring-core",
 57 |             "6.1.4",
 58 |             "jar",
 59 |             "sources"),
 60 | 
 61 |         // POM packaging
 62 |         Arguments.of(
 63 |             "org.springframework.boot:spring-boot-starter-parent:3.2.0:pom",
 64 |             "org.springframework.boot",
 65 |             "spring-boot-starter-parent",
 66 |             "3.2.0",
 67 |             "pom",
 68 |             null),
 69 | 
 70 |         // With classifier but no explicit packaging (empty packaging becomes null)
 71 |         Arguments.of(
 72 |             "org.springframework:spring-core:6.1.4::sources",
 73 |             "org.springframework",
 74 |             "spring-core",
 75 |             "6.1.4",
 76 |             null,
 77 |             "sources"),
 78 | 
 79 |         // Complex version strings
 80 |         Arguments.of(
 81 |             "com.example:test-artifact:1.0.0-RC1",
 82 |             "com.example",
 83 |             "test-artifact",
 84 |             "1.0.0-RC1",
 85 |             null,
 86 |             null),
 87 | 
 88 |         // Long groupId with many segments
 89 |         Arguments.of(
 90 |             "com.company.department.team:artifact-name:2.5.1",
 91 |             "com.company.department.team",
 92 |             "artifact-name",
 93 |             "2.5.1",
 94 |             null,
 95 |             null),
 96 | 
 97 |         // Artifact with numbers and hyphens
 98 |         Arguments.of(
 99 |             "org.apache.commons:commons-lang3:3.12.0",
100 |             "org.apache.commons",
101 |             "commons-lang3",
102 |             "3.12.0",
103 |             null,
104 |             null),
105 | 
106 |         // Snapshot version
107 |         Arguments.of(
108 |             "com.example:test:1.0.0-SNAPSHOT:jar",
109 |             "com.example",
110 |             "test",
111 |             "1.0.0-SNAPSHOT",
112 |             "jar",
113 |             null));
114 |   }
115 | 
116 |   @ParameterizedTest(name = "Parse {0}")
117 |   @MethodSource("validCoordinateTestData")
118 |   void testParse_ValidCoordinates(
119 |       String coordinateString,
120 |       String expectedGroupId,
121 |       String expectedArtifactId,
122 |       String expectedVersion,
123 |       String expectedPackaging,
124 |       String expectedClassifier) {
125 | 
126 |     // When
127 |     MavenCoordinate result = MavenCoordinateParser.parse(coordinateString);
128 | 
129 |     // Then
130 |     assertThat(result.groupId()).isEqualTo(expectedGroupId);
131 |     assertThat(result.artifactId()).isEqualTo(expectedArtifactId);
132 |     assertThat(result.version()).isEqualTo(expectedVersion);
133 |     assertThat(result.packaging()).isEqualTo(expectedPackaging);
134 |     assertThat(result.classifier()).isEqualTo(expectedClassifier);
135 |   }
136 | 
137 |   @ParameterizedTest
138 |   @ValueSource(
139 |       strings = {
140 |         "invalid", // Too few parts
141 |         ":", // Empty parts
142 |         ":::", // Only separators
143 |         "group:", // Missing artifactId
144 |         ":artifact", // Missing groupId
145 |         "", // Empty string
146 |         "   ", // Whitespace only
147 |         "group:artifact:version:packaging:classifier:extra", // Too many parts
148 |         "group::version", // Missing artifactId (empty)
149 |         ":artifact:version" // Missing groupId (empty)
150 |       })
151 |   void testParse_InvalidCoordinates(String invalidCoordinate) {
152 |     // When & Then - Expect either invalid format or empty groupId/artifactId messages
153 |     assertThatThrownBy(() -> MavenCoordinateParser.parse(invalidCoordinate))
154 |         .isInstanceOf(IllegalArgumentException.class)
155 |         .hasMessageMatching(
156 |             ".*(Invalid Maven coordinate format|Dependency string cannot be null or empty|GroupId and artifactId cannot be empty).*");
157 |   }
158 | 
159 |   @Test
160 |   void testParse_NullInput() {
161 |     // When & Then
162 |     assertThatThrownBy(() -> MavenCoordinateParser.parse(null))
163 |         .isInstanceOf(IllegalArgumentException.class)
164 |         .hasMessageContaining("Dependency string cannot be null or empty");
165 |   }
166 | 
167 |   @Test
168 |   void testParse_WithWhitespace() {
169 |     // Given
170 |     String coordinateWithSpaces = "  org.springframework : spring-core : 6.1.4  ";
171 | 
172 |     // When
173 |     MavenCoordinate result = MavenCoordinateParser.parse(coordinateWithSpaces);
174 | 
175 |     // Then - Should trim whitespace from each part
176 |     assertThat(result.groupId()).isEqualTo("org.springframework");
177 |     assertThat(result.artifactId()).isEqualTo("spring-core");
178 |     assertThat(result.version()).isEqualTo("6.1.4");
179 |   }
180 | 
181 |   @Test
182 |   void testParse_EmptyVersionString() {
183 |     // Given - Empty version part
184 |     String coordinate = "org.springframework:spring-core:";
185 | 
186 |     // When
187 |     MavenCoordinate result = MavenCoordinateParser.parse(coordinate);
188 | 
189 |     // Then - Empty version should be converted to null
190 |     assertThat(result.groupId()).isEqualTo("org.springframework");
191 |     assertThat(result.artifactId()).isEqualTo("spring-core");
192 |     assertThat(result.version()).isNull();
193 |   }
194 | 
195 |   @Test
196 |   void testParse_EmptyPackagingString() {
197 |     // Given - Empty packaging part
198 |     String coordinate = "org.springframework:spring-core:6.1.4:";
199 | 
200 |     // When
201 |     MavenCoordinate result = MavenCoordinateParser.parse(coordinate);
202 | 
203 |     // Then - Empty packaging should be converted to null
204 |     assertThat(result.groupId()).isEqualTo("org.springframework");
205 |     assertThat(result.artifactId()).isEqualTo("spring-core");
206 |     assertThat(result.version()).isEqualTo("6.1.4");
207 |     assertThat(result.packaging()).isNull();
208 |   }
209 | 
210 |   @Test
211 |   void testParse_EmptyClassifierString() {
212 |     // Given - Empty classifier part
213 |     String coordinate = "org.springframework:spring-core:6.1.4:jar:";
214 | 
215 |     // When
216 |     MavenCoordinate result = MavenCoordinateParser.parse(coordinate);
217 | 
218 |     // Then - Empty classifier should be converted to null
219 |     assertThat(result.groupId()).isEqualTo("org.springframework");
220 |     assertThat(result.artifactId()).isEqualTo("spring-core");
221 |     assertThat(result.version()).isEqualTo("6.1.4");
222 |     assertThat(result.packaging()).isEqualTo("jar");
223 |     assertThat(result.classifier()).isNull();
224 |   }
225 | 
226 |   @Test
227 |   void testParse_SpecialCharactersInNames() {
228 |     // Given - Coordinates with special characters that should be valid
229 |     String coordinate = "com.example-company:my-artifact_name:1.0.0-beta.1:jar";
230 | 
231 |     // When
232 |     MavenCoordinate result = MavenCoordinateParser.parse(coordinate);
233 | 
234 |     // Then
235 |     assertThat(result.groupId()).isEqualTo("com.example-company");
236 |     assertThat(result.artifactId()).isEqualTo("my-artifact_name");
237 |     assertThat(result.version()).isEqualTo("1.0.0-beta.1");
238 |     assertThat(result.packaging()).isEqualTo("jar");
239 |   }
240 | 
241 |   @Test
242 |   void testParse_NumericVersions() {
243 |     // Given - Various numeric version formats
244 |     String coordinate1 = "com.example:test:1:jar";
245 |     String coordinate2 = "com.example:test:1.0:jar";
246 |     String coordinate3 = "com.example:test:1.0.0:jar";
247 | 
248 |     // When
249 |     MavenCoordinate result1 = MavenCoordinateParser.parse(coordinate1);
250 |     MavenCoordinate result2 = MavenCoordinateParser.parse(coordinate2);
251 |     MavenCoordinate result3 = MavenCoordinateParser.parse(coordinate3);
252 | 
253 |     // Then
254 |     assertThat(result1.version()).isEqualTo("1");
255 |     assertThat(result2.version()).isEqualTo("1.0");
256 |     assertThat(result3.version()).isEqualTo("1.0.0");
257 |   }
258 | 
259 |   @Test
260 |   void testParse_ConsistencyWithToCoordinateString() {
261 |     // Given - A coordinate string
262 |     String originalCoordinate = "org.springframework:spring-core:6.1.4:jar:sources";
263 | 
264 |     // When - Parse and convert back to string
265 |     MavenCoordinate parsed = MavenCoordinateParser.parse(originalCoordinate);
266 |     String reconstructed = parsed.toCoordinateString();
267 | 
268 |     // Then - Should be identical
269 |     assertThat(reconstructed).isEqualTo(originalCoordinate);
270 |   }
271 | 
272 |   @Test
273 |   void testParse_MinimalCoordinate() {
274 |     // Given - Minimal valid coordinate
275 |     String coordinate = "g:a";
276 | 
277 |     // When
278 |     MavenCoordinate result = MavenCoordinateParser.parse(coordinate);
279 | 
280 |     // Then
281 |     assertThat(result.groupId()).isEqualTo("g");
282 |     assertThat(result.artifactId()).isEqualTo("a");
283 |     assertThat(result.version()).isNull();
284 |     assertThat(result.packaging()).isNull();
285 |     assertThat(result.classifier()).isNull();
286 |   }
287 | 
288 |   @Test
289 |   void testParse_RealWorldExamples() {
290 |     // Test with real-world Maven coordinates
291 | 
292 |     // Spring Boot Starter
293 |     MavenCoordinate springBoot =
294 |         MavenCoordinateParser.parse("org.springframework.boot:spring-boot-starter:3.2.0");
295 |     assertThat(springBoot.groupId()).isEqualTo("org.springframework.boot");
296 |     assertThat(springBoot.artifactId()).isEqualTo("spring-boot-starter");
297 |     assertThat(springBoot.version()).isEqualTo("3.2.0");
298 | 
299 |     // Jackson with classifier
300 |     MavenCoordinate jackson =
301 |         MavenCoordinateParser.parse("com.fasterxml.jackson.core:jackson-core:2.15.2:jar:sources");
302 |     assertThat(jackson.groupId()).isEqualTo("com.fasterxml.jackson.core");
303 |     assertThat(jackson.artifactId()).isEqualTo("jackson-core");
304 |     assertThat(jackson.version()).isEqualTo("2.15.2");
305 |     assertThat(jackson.packaging()).isEqualTo("jar");
306 |     assertThat(jackson.classifier()).isEqualTo("sources");
307 | 
308 |     // Apache Commons
309 |     MavenCoordinate commons =
310 |         MavenCoordinateParser.parse("org.apache.commons:commons-lang3:3.12.0");
311 |     assertThat(commons.groupId()).isEqualTo("org.apache.commons");
312 |     assertThat(commons.artifactId()).isEqualTo("commons-lang3");
313 |     assertThat(commons.version()).isEqualTo("3.12.0");
314 |   }
315 | 
316 |   @Test
317 |   void testParse_EdgeCaseVersionFormats() {
318 |     // Test various version formats that should be valid
319 |     String[] validVersions = {
320 |       "1",
321 |       "1.0",
322 |       "1.0.0",
323 |       "1.0.0-SNAPSHOT",
324 |       "1.0.0-RC1",
325 |       "1.0.0.RELEASE",
326 |       "1.0.0.Final",
327 |       "2021.0.0",
328 |       "1.0.0-alpha.1",
329 |       "1.0.0+build.1"
330 |     };
331 | 
332 |     for (String version : validVersions) {
333 |       String coordinate = "com.example:test:" + version;
334 |       MavenCoordinate result = MavenCoordinateParser.parse(coordinate);
335 |       assertThat(result.version()).isEqualTo(version);
336 |     }
337 |   }
338 | }
339 | 
```

--------------------------------------------------------------------------------
/src/main/java/com/arvindand/mcp/maven/util/VersionComparator.java:
--------------------------------------------------------------------------------

```java
  1 | package com.arvindand.mcp.maven.util;
  2 | 
  3 | import com.arvindand.mcp.maven.model.VersionInfo.VersionType;
  4 | import java.util.Arrays;
  5 | import java.util.Comparator;
  6 | import java.util.Objects;
  7 | import java.util.Set;
  8 | import org.apache.maven.artifact.versioning.ComparableVersion;
  9 | import org.springframework.stereotype.Component;
 10 | 
 11 | /**
 12 |  * Utility for comparing and analyzing Maven version strings.
 13 |  *
 14 |  * @author Arvind Menon
 15 |  * @since 0.1.0
 16 |  */
 17 | @Component
 18 | public final class VersionComparator implements Comparator<String> {
 19 | 
 20 |   private static final String UNKNOWN = "unknown";
 21 |   private static final String ALPHA = "alpha";
 22 |   private static final String BETA = "beta";
 23 |   private static final String MILESTONE = "milestone";
 24 |   private static final String PATCH = "patch";
 25 |   private static final Set<String> STABLE_QUALIFIERS = Set.of("final", "ga", "release");
 26 |   private static final Set<String> ALPHA_QUALIFIERS = Set.of(ALPHA, "a");
 27 |   private static final Set<String> BETA_QUALIFIERS = Set.of(BETA, "b");
 28 |   private static final Set<String> MILESTONE_QUALIFIERS = Set.of(MILESTONE, "m");
 29 |   private static final Set<String> RC_QUALIFIERS = Set.of("rc", "cr");
 30 | 
 31 |   /**
 32 |    * Compares two version strings using Maven's ComparableVersion.
 33 |    *
 34 |    * @param version1 first version to compare
 35 |    * @param version2 second version to compare
 36 |    * @return negative if version1 < version2, 0 if equal, positive if version1 > version2
 37 |    */
 38 |   @Override
 39 |   public int compare(String version1, String version2) {
 40 |     return switch ((version1 == null ? 1 : 0) + (version2 == null ? 2 : 0)) {
 41 |       case 0 ->
 42 |           new ComparableVersion(version1)
 43 |               .compareTo(new ComparableVersion(version2)); // both non-null
 44 |       case 1 -> -1; // version1 is null, version2 is not
 45 |       case 2 -> 1; // version2 is null, version1 is not
 46 |       case 3 -> 0; // both null
 47 |       default -> throw new IllegalStateException("Unexpected comparison state");
 48 |     };
 49 |   }
 50 | 
 51 |   /**
 52 |    * Gets the latest version from an array of version strings.
 53 |    *
 54 |    * @param versions array of version strings
 55 |    * @return the latest version or null if array is empty
 56 |    */
 57 |   public static String getLatest(String[] versions) {
 58 |     if (versions == null || versions.length == 0) return null;
 59 |     return Arrays.stream(versions).max(new VersionComparator()).orElse(null);
 60 |   }
 61 | 
 62 |   /**
 63 |    * Determines the type of update between current and latest versions.
 64 |    *
 65 |    * @param currentVersion the current version
 66 |    * @param latestVersion the latest available version
 67 |    * @return update type: "major", "minor", "patch", "none", or "unknown"
 68 |    */
 69 |   public String determineUpdateType(String currentVersion, String latestVersion) {
 70 |     return switch (validateVersions(currentVersion, latestVersion)) {
 71 |       case INVALID -> UNKNOWN;
 72 |       case EQUAL -> "none";
 73 |       case VALID -> {
 74 |         int comparison = compare(currentVersion, latestVersion);
 75 |         if (comparison >= 0) {
 76 |           yield comparison == 0 ? "none" : UNKNOWN;
 77 |         } else {
 78 |           yield determineUpdateType(parseVersion(currentVersion), parseVersion(latestVersion));
 79 |         }
 80 |       }
 81 |     };
 82 |   }
 83 | 
 84 |   /**
 85 |    * Checks if a version is considered stable (not pre-release).
 86 |    *
 87 |    * @param version the version to check
 88 |    * @return true if the version is stable
 89 |    */
 90 |   public boolean isStableVersion(String version) {
 91 |     if (version == null) return false;
 92 | 
 93 |     return switch (classifyQualifier(extractQualifier(version))) {
 94 |       case STABLE -> true;
 95 |       case PRE_RELEASE -> false;
 96 |     };
 97 |   }
 98 | 
 99 |   /**
100 |    * Gets the version type enum for a version string.
101 |    *
102 |    * @param version the version to analyze
103 |    * @return the version type enum
104 |    */
105 |   public VersionType getVersionType(String version) {
106 |     if (version == null) return VersionType.STABLE;
107 | 
108 |     return switch (classifyQualifier(extractQualifier(version))) {
109 |       case STABLE -> VersionType.STABLE;
110 |       case PRE_RELEASE -> determinePreReleaseVersionType(extractQualifier(version));
111 |     };
112 |   }
113 | 
114 |   /**
115 |    * Gets the version type as a display string.
116 |    *
117 |    * @param version the version to analyze
118 |    * @return the version type as a string
119 |    */
120 |   public String getVersionTypeString(String version) {
121 |     if (version == null) return UNKNOWN;
122 |     return getVersionType(version).getDisplayName();
123 |   }
124 | 
125 |   /**
126 |    * Parses a version string into numeric components and qualifier.
127 |    *
128 |    * @param version the version string to parse
129 |    * @return parsed version components
130 |    */
131 |   public VersionComponents parseVersion(String version) {
132 |     if (version == null || version.trim().isEmpty()) {
133 |       return new VersionComponents(new int[0], "");
134 |     }
135 | 
136 |     String trimmed = version.trim();
137 | 
138 |     // Split on first hyphen to separate numeric part from qualifier
139 |     int hyphenIndex = findFirstQualifierSeparator(trimmed);
140 |     String numericPart = hyphenIndex != -1 ? trimmed.substring(0, hyphenIndex) : trimmed;
141 |     String qualifier = hyphenIndex != -1 ? trimmed.substring(hyphenIndex + 1).toLowerCase() : "";
142 | 
143 |     // Parse numeric components (major.minor.patch.etc)
144 |     String[] segments = numericPart.split("\\.");
145 |     int[] numericParts = new int[segments.length];
146 | 
147 |     for (int i = 0; i < segments.length; i++) {
148 |       try {
149 |         // Handle cases like "1.0.0-SNAPSHOT" where hyphen is within a segment
150 |         String segment = segments[i];
151 |         int segmentHyphen = segment.indexOf('-');
152 |         if (segmentHyphen != -1) {
153 |           segment = segment.substring(0, segmentHyphen);
154 |           // If this is the first time we see a qualifier, capture it
155 |           if (qualifier.isEmpty()) {
156 |             qualifier = segments[i].substring(segmentHyphen + 1).toLowerCase();
157 |           }
158 |         }
159 |         numericParts[i] = Integer.parseInt(segment);
160 |       } catch (NumberFormatException _) {
161 |         numericParts[i] = 0;
162 |       }
163 |     }
164 | 
165 |     return new VersionComponents(numericParts, qualifier);
166 |   }
167 | 
168 |   private int findFirstQualifierSeparator(String version) {
169 |     // Look for common qualifier separators: - or _
170 |     // But be smart about it - avoid separating dates (2023-01-15) or similar patterns
171 |     int hyphenIndex = version.indexOf('-');
172 |     int underscoreIndex = version.indexOf('_');
173 | 
174 |     // Return the first separator found, preferring hyphen
175 |     if (hyphenIndex == -1 && underscoreIndex == -1) return -1;
176 |     if (hyphenIndex == -1) return underscoreIndex;
177 |     if (underscoreIndex == -1) return hyphenIndex;
178 |     return Math.min(hyphenIndex, underscoreIndex);
179 |   }
180 | 
181 |   private String extractQualifier(String version) {
182 |     return parseVersion(version).qualifier();
183 |   }
184 | 
185 |   private ValidationResult validateVersions(String current, String latest) {
186 |     if (current == null || latest == null) return ValidationResult.INVALID;
187 |     if (current.equals(latest)) return ValidationResult.EQUAL;
188 |     return ValidationResult.VALID;
189 |   }
190 | 
191 |   private QualifierType classifyQualifier(String qualifier) {
192 |     if (qualifier.isEmpty() || STABLE_QUALIFIERS.contains(qualifier)) {
193 |       return QualifierType.STABLE;
194 |     }
195 | 
196 |     String lower = qualifier.toLowerCase();
197 | 
198 |     // Treat service packs as stable (e.g., 1.0.0-SP1)
199 |     if (lower.startsWith("sp")) {
200 |       return QualifierType.STABLE;
201 |     }
202 | 
203 |     // Common pre-release markers
204 |     if (lower.contains("snapshot")
205 |         || lower.contains("rc")
206 |         || lower.contains("cr")
207 |         || lower.contains("m")
208 |         || lower.contains(BETA)
209 |         || lower.contains(ALPHA)
210 |         || lower.contains("preview")
211 |         || lower.contains("dev")) {
212 |       return QualifierType.PRE_RELEASE;
213 |     }
214 | 
215 |     // Unknown qualifier: conservatively treat as pre-release rather than stable
216 |     return QualifierType.PRE_RELEASE;
217 |   }
218 | 
219 |   private VersionType determinePreReleaseVersionType(String qualifier) {
220 |     if (isAlphaQualifier(qualifier)) return VersionType.ALPHA;
221 |     if (isBetaQualifier(qualifier)) return VersionType.BETA;
222 |     if (isMilestoneQualifier(qualifier)) return VersionType.MILESTONE;
223 |     if (isRcQualifier(qualifier)) return VersionType.RC;
224 |     return VersionType.ALPHA; // Default unknown pre-releases to alpha
225 |   }
226 | 
227 |   private boolean isAlphaQualifier(String qualifier) {
228 |     return ALPHA_QUALIFIERS.contains(qualifier)
229 |         || qualifier.startsWith(ALPHA)
230 |         || qualifier.startsWith("a")
231 |         || qualifier.contains("dev")
232 |         || qualifier.contains("preview");
233 |   }
234 | 
235 |   private boolean isBetaQualifier(String qualifier) {
236 |     return BETA_QUALIFIERS.contains(qualifier)
237 |         || qualifier.startsWith(BETA)
238 |         || qualifier.startsWith("b");
239 |   }
240 | 
241 |   private boolean isMilestoneQualifier(String qualifier) {
242 |     return MILESTONE_QUALIFIERS.contains(qualifier)
243 |         || qualifier.startsWith(MILESTONE)
244 |         || qualifier.startsWith("m");
245 |   }
246 | 
247 |   private boolean isRcQualifier(String qualifier) {
248 |     return RC_QUALIFIERS.contains(qualifier)
249 |         || qualifier.startsWith("rc")
250 |         || qualifier.startsWith("cr")
251 |         || qualifier.contains("candidate");
252 |   }
253 | 
254 |   private String determineUpdateType(VersionComponents current, VersionComponents latest) {
255 |     // Handle case where numeric parts are identical but qualifiers differ
256 |     boolean sameNumericVersion = areNumericVersionsEqual(current, latest);
257 | 
258 |     if (sameNumericVersion) {
259 |       // If numeric versions are the same, check qualifiers
260 |       return determineQualifierUpdate(current.qualifier(), latest.qualifier());
261 |     }
262 | 
263 |     // Compare numeric parts to determine update type
264 |     int maxLength = Math.max(current.numericParts().length, latest.numericParts().length);
265 | 
266 |     for (int i = 0; i < maxLength; i++) {
267 |       int currentPart = i < current.numericParts().length ? current.numericParts()[i] : 0;
268 |       int latestPart = i < latest.numericParts().length ? latest.numericParts()[i] : 0;
269 | 
270 |       if (latestPart > currentPart) {
271 |         return switch (i) {
272 |           case 0 -> "major";
273 |           case 1 -> "minor";
274 |           default -> PATCH;
275 |         };
276 |       } else if (currentPart > latestPart) {
277 |         // Current version is higher than "latest" - this is a downgrade scenario
278 |         return UNKNOWN;
279 |       }
280 |     }
281 |     return "none";
282 |   }
283 | 
284 |   private boolean areNumericVersionsEqual(VersionComponents current, VersionComponents latest) {
285 |     int maxLength = Math.max(current.numericParts().length, latest.numericParts().length);
286 | 
287 |     for (int i = 0; i < maxLength; i++) {
288 |       int currentPart = i < current.numericParts().length ? current.numericParts()[i] : 0;
289 |       int latestPart = i < latest.numericParts().length ? latest.numericParts()[i] : 0;
290 | 
291 |       if (currentPart != latestPart) {
292 |         return false;
293 |       }
294 |     }
295 |     return true;
296 |   }
297 | 
298 |   private String determineQualifierUpdate(String currentQualifier, String latestQualifier) {
299 |     // If both are empty, versions are identical
300 |     if (currentQualifier.isEmpty() && latestQualifier.isEmpty()) {
301 |       return "none";
302 |     }
303 | 
304 |     // If current has qualifier but latest doesn't, it's upgrading to stable
305 |     if (!currentQualifier.isEmpty() && latestQualifier.isEmpty()) {
306 |       return PATCH; // Treat pre-release to stable as patch update
307 |     }
308 | 
309 |     // If current is stable but latest has qualifier, this is unusual (downgrade to pre-release)
310 |     if (currentQualifier.isEmpty() && !latestQualifier.isEmpty()) {
311 |       return UNKNOWN;
312 |     }
313 | 
314 |     // Both have qualifiers - compare stability levels
315 |     return compareQualifierStability(currentQualifier, latestQualifier);
316 |   }
317 | 
318 |   private String compareQualifierStability(String currentQualifier, String latestQualifier) {
319 |     // Define stability order (lower index = less stable)
320 |     String[] stabilityOrder = {ALPHA, BETA, MILESTONE, "rc", "snapshot"};
321 | 
322 |     int currentStability = getQualifierStability(currentQualifier, stabilityOrder);
323 |     int latestStability = getQualifierStability(latestQualifier, stabilityOrder);
324 | 
325 |     if (latestStability > currentStability) {
326 |       return PATCH; // Upgrading to more stable pre-release
327 |     } else if (latestStability < currentStability) {
328 |       return UNKNOWN; // Downgrading to less stable
329 |     } else {
330 |       // Same stability level - might be version number change within qualifier
331 |       return currentQualifier.equals(latestQualifier) ? "none" : PATCH;
332 |     }
333 |   }
334 | 
335 |   private int getQualifierStability(String qualifier, String[] stabilityOrder) {
336 |     String lowerQualifier = qualifier.toLowerCase();
337 | 
338 |     for (int i = 0; i < stabilityOrder.length; i++) {
339 |       if (lowerQualifier.contains(stabilityOrder[i])) {
340 |         return i;
341 |       }
342 |     }
343 | 
344 |     // Unknown qualifier types get medium stability
345 |     return stabilityOrder.length / 2;
346 |   }
347 | 
348 |   /**
349 |    * Represents parsed version components.
350 |    *
351 |    * @param numericParts the numeric parts of the version
352 |    * @param qualifier the qualifier part (e.g., "alpha", "beta")
353 |    */
354 |   public record VersionComponents(int[] numericParts, String qualifier) {
355 |     @Override
356 |     public boolean equals(Object obj) {
357 |       if (this == obj) return true;
358 |       if (obj == null || getClass() != obj.getClass()) return false;
359 |       VersionComponents that = (VersionComponents) obj;
360 |       return Arrays.equals(numericParts, that.numericParts)
361 |           && Objects.equals(qualifier, that.qualifier);
362 |     }
363 | 
364 |     @Override
365 |     public int hashCode() {
366 |       return Objects.hash(Arrays.hashCode(numericParts), qualifier);
367 |     }
368 | 
369 |     @Override
370 |     public String toString() {
371 |       return "VersionComponents{numericParts="
372 |           + Arrays.toString(numericParts)
373 |           + ", qualifier='"
374 |           + qualifier
375 |           + "'}";
376 |     }
377 |   }
378 | 
379 |   private enum ValidationResult {
380 |     INVALID,
381 |     EQUAL,
382 |     VALID
383 |   }
384 | 
385 |   private enum QualifierType {
386 |     STABLE,
387 |     PRE_RELEASE
388 |   }
389 | }
390 | 
```

--------------------------------------------------------------------------------
/src/test/java/com/arvindand/mcp/maven/util/VersionComparatorTest.java:
--------------------------------------------------------------------------------

```java
  1 | package com.arvindand.mcp.maven.util;
  2 | 
  3 | import static org.assertj.core.api.Assertions.assertThat;
  4 | 
  5 | import java.util.Arrays;
  6 | import java.util.List;
  7 | import java.util.stream.Stream;
  8 | import org.junit.jupiter.api.BeforeEach;
  9 | import org.junit.jupiter.api.Test;
 10 | import org.junit.jupiter.params.ParameterizedTest;
 11 | import org.junit.jupiter.params.provider.Arguments;
 12 | import org.junit.jupiter.params.provider.MethodSource;
 13 | 
 14 | /**
 15 |  * Comprehensive unit tests for VersionComparator.
 16 |  *
 17 |  * @author Arvind Menon
 18 |  * @since 0.1.0
 19 |  */
 20 | class VersionComparatorTest {
 21 | 
 22 |   private VersionComparator versionComparator;
 23 | 
 24 |   @BeforeEach
 25 |   void setUp() {
 26 |     versionComparator = new VersionComparator();
 27 |   }
 28 | 
 29 |   /** Test data for version comparison. */
 30 |   private static Stream<Arguments> versionComparisonTestData() {
 31 |     return Stream.of(
 32 |         // Basic numeric comparisons
 33 |         Arguments.of("1.0.0", "2.0.0", -1, "1.0.0 < 2.0.0"),
 34 |         Arguments.of("2.0.0", "1.0.0", 1, "2.0.0 > 1.0.0"),
 35 |         Arguments.of("1.0.0", "1.0.0", 0, "1.0.0 = 1.0.0"),
 36 | 
 37 |         // Minor version comparisons
 38 |         Arguments.of("1.0.0", "1.1.0", -1, "1.0.0 < 1.1.0"),
 39 |         Arguments.of("1.5.0", "1.2.0", 1, "1.5.0 > 1.2.0"),
 40 | 
 41 |         // Patch version comparisons
 42 |         Arguments.of("1.0.0", "1.0.1", -1, "1.0.0 < 1.0.1"),
 43 |         Arguments.of("1.0.5", "1.0.2", 1, "1.0.5 > 1.0.2"),
 44 | 
 45 |         // Different length versions - should be treated as equal
 46 |         Arguments.of("1.0", "1.0.0", 0, "1.0 = 1.0.0 (equivalent versions)"),
 47 |         Arguments.of("1.0.0", "1.0", 0, "1.0.0 = 1.0 (equivalent versions)"),
 48 |         Arguments.of("1", "1.0.0", 0, "1 = 1.0.0 (equivalent versions)"),
 49 |         Arguments.of("1.0", "1.0.1", -1, "1.0 < 1.0.1"),
 50 |         Arguments.of("1.1", "1.0.1", 1, "1.1 > 1.0.1"),
 51 | 
 52 |         // Large numbers
 53 |         Arguments.of("10.0.0", "9.9.9", 1, "10.0.0 > 9.9.9"),
 54 |         Arguments.of("1.10.0", "1.9.0", 1, "1.10.0 > 1.9.0"),
 55 |         Arguments.of("1.0.10", "1.0.9", 1, "1.0.10 > 1.0.9"),
 56 | 
 57 |         // Multi-digit versions
 58 |         Arguments.of("1.2.3", "1.2.10", -1, "1.2.3 < 1.2.10"),
 59 |         Arguments.of(
 60 |             "11.0.0",
 61 |             "2.0.0",
 62 |             1,
 63 |             "11.0.0 > 2.0.0"), // Qualifier comparisons (release > rc > milestone > beta > alpha)
 64 |         Arguments.of("1.0.0", "1.0.0-RC1", 1, "release > rc"),
 65 |         Arguments.of("1.0.0-RC1", "1.0.0-M1", 1, "rc > milestone"),
 66 |         Arguments.of("1.0.0-M1", "1.0.0-BETA1", 1, "milestone > beta"),
 67 |         Arguments.of("1.0.0-BETA1", "1.0.0-ALPHA1", 1, "beta > alpha"),
 68 | 
 69 |         // Same qualifier with different numbers
 70 |         Arguments.of("1.0.0-RC1", "1.0.0-RC2", -1, "RC1 < RC2"),
 71 |         Arguments.of("1.0.0-BETA2", "1.0.0-BETA1", 1, "BETA2 > BETA1"),
 72 |         Arguments.of("1.0.0-M2", "1.0.0-M1", 1, "M2 > M1"), // Different separators
 73 |         Arguments.of("1.0.0", "1-0-0", 0, "Different separators treated equally"),
 74 |         Arguments.of("1.0.0", "1_0_0", -1, "Underscore separators (Maven treats differently)"),
 75 |         Arguments.of(
 76 |             "1.0.0-RC1",
 77 |             "1.0.0.RC1",
 78 |             0,
 79 |             "Different qualifier separators (Maven treats as equivalent)"),
 80 | 
 81 |         // Case insensitive qualifiers
 82 |         Arguments.of("1.0.0-rc1", "1.0.0-RC1", 0, "Case insensitive RC"),
 83 |         Arguments.of("1.0.0-beta", "1.0.0-BETA", 0, "Case insensitive BETA"),
 84 |         Arguments.of("1.0.0-alpha", "1.0.0-ALPHA", 0, "Case insensitive ALPHA"),
 85 | 
 86 |         // Special qualifiers - Maven's official behavior
 87 |         Arguments.of("1.0.0.RELEASE", "1.0.0", 0, "RELEASE qualifier equivalent to plain"),
 88 |         Arguments.of("1.0.0.Final", "1.0.0", 0, "Final qualifier equivalent to plain"),
 89 |         Arguments.of("1.0.0.GA", "1.0.0", 0, "GA qualifier equivalent to plain"),
 90 | 
 91 |         // Complex real-world examples
 92 |         Arguments.of("3.2.0", "3.2.0-RC1", 1, "Spring Boot style"),
 93 |         Arguments.of("2.15.2", "2.16.0-SNAPSHOT", -1, "Jackson style with snapshot"),
 94 |         Arguments.of("6.1.4", "6.1.4.RELEASE", 0, "Spring Framework style"));
 95 |   }
 96 | 
 97 |   @ParameterizedTest(name = "{3}")
 98 |   @MethodSource("versionComparisonTestData")
 99 |   void testCompare(String version1, String version2, int expectedSign, String description) {
100 |     // When
101 |     int result = versionComparator.compare(version1, version2);
102 | 
103 |     // Then - Check the sign of the result
104 |     if (expectedSign == 0) {
105 |       assertThat(result).isZero();
106 |     } else if (expectedSign < 0) {
107 |       assertThat(result).isNegative();
108 |     } else {
109 |       assertThat(result).isPositive();
110 |     }
111 |   }
112 | 
113 |   @Test
114 |   void testCompare_NullAndEmptyVersions() {
115 |     // Null versions should be handled gracefully or throw appropriate exceptions
116 |     // The exact behavior depends on implementation, but it should be consistent
117 | 
118 |     try {
119 |       int result1 = versionComparator.compare(null, "1.0.0");
120 |       int result2 = versionComparator.compare("1.0.0", null);
121 |       int result3 = versionComparator.compare(null, null);
122 | 
123 |       // If no exception, results should be consistent
124 |       assertThat(result1).isNotEqualTo(result2); // Should be opposites
125 |       assertThat(result3).isZero(); // null equals null
126 |     } catch (NullPointerException e) {
127 |       // This is also acceptable behavior
128 |       assertThat(e).isInstanceOf(NullPointerException.class);
129 |     }
130 |   }
131 | 
132 |   @Test
133 |   void testCompare_SameVersionsAreEqual() {
134 |     String[] versions = {"1.0.0", "2.5.1", "1.0.0-RC1", "1.0.0-BETA", "1.0.0-ALPHA", "1.0.0-M1"};
135 | 
136 |     for (String version : versions) {
137 |       int result = versionComparator.compare(version, version);
138 |       assertThat(result).isZero();
139 |     }
140 |   }
141 | 
142 |   @Test
143 |   void testGetLatest_BasicVersions() {
144 |     // Given
145 |     String[] versions = {"1.0.0", "2.0.0", "1.5.0", "2.1.0", "1.0.1"};
146 | 
147 |     // When
148 |     String latest = VersionComparator.getLatest(versions);
149 | 
150 |     // Then
151 |     assertThat(latest).isEqualTo("2.1.0");
152 |   }
153 | 
154 |   @Test
155 |   void testGetLatest_WithQualifiers() {
156 |     // Given
157 |     String[] versions = {"1.0.0", "1.0.0-RC1", "1.0.0-BETA", "1.0.0-ALPHA", "1.0.0-M1"};
158 | 
159 |     // When
160 |     String latest = VersionComparator.getLatest(versions);
161 | 
162 |     // Then - Release version should be latest
163 |     assertThat(latest).isEqualTo("1.0.0");
164 |   }
165 | 
166 |   @Test
167 |   void testGetLatest_RealWorldExample() {
168 |     // Given - Realistic Spring Boot versions
169 |     String[] versions = {
170 |       "3.1.0", "3.1.1", "3.1.2", "3.2.0-RC1", "3.2.0-SNAPSHOT", "3.2.0", "3.1.5", "3.0.12"
171 |     };
172 | 
173 |     // When
174 |     String latest = VersionComparator.getLatest(versions);
175 | 
176 |     // Then - Should be the highest stable release
177 |     assertThat(latest).isEqualTo("3.2.0");
178 |   }
179 | 
180 |   @Test
181 |   void testGetLatest_EmptyArray() {
182 |     // Given
183 |     String[] versions = {};
184 | 
185 |     // When
186 |     String latest = VersionComparator.getLatest(versions);
187 | 
188 |     // Then
189 |     assertThat(latest).isNull();
190 |   }
191 | 
192 |   @Test
193 |   void testGetLatest_NullArray() {
194 |     // Given
195 |     String[] versions = null;
196 | 
197 |     // When
198 |     String latest = VersionComparator.getLatest(versions);
199 | 
200 |     // Then
201 |     assertThat(latest).isNull();
202 |   }
203 | 
204 |   @Test
205 |   void testGetLatest_SingleVersion() {
206 |     // Given
207 |     String[] versions = {"1.0.0"};
208 | 
209 |     // When
210 |     String latest = VersionComparator.getLatest(versions);
211 | 
212 |     // Then
213 |     assertThat(latest).isEqualTo("1.0.0");
214 |   }
215 | 
216 |   @Test
217 |   void testSorting_DescendingOrder() {
218 |     // Given
219 |     List<String> versions = Arrays.asList("1.0.0", "3.0.0", "2.0.0", "1.5.0", "2.1.0");
220 | 
221 |     // When - Sort in descending order
222 |     List<String> sorted = versions.stream().sorted(versionComparator.reversed()).toList();
223 | 
224 |     // Then
225 |     assertThat(sorted).containsExactly("3.0.0", "2.1.0", "2.0.0", "1.5.0", "1.0.0");
226 |   }
227 | 
228 |   @Test
229 |   void testSorting_AscendingOrder() {
230 |     // Given
231 |     List<String> versions = Arrays.asList("3.0.0", "1.0.0", "2.1.0", "1.5.0", "2.0.0");
232 | 
233 |     // When - Sort in ascending order
234 |     List<String> sorted = versions.stream().sorted(versionComparator).toList();
235 | 
236 |     // Then
237 |     assertThat(sorted).containsExactly("1.0.0", "1.5.0", "2.0.0", "2.1.0", "3.0.0");
238 |   }
239 | 
240 |   @Test
241 |   void testQualifierPriority() {
242 |     // Given - Same base version with different qualifiers
243 |     List<String> versions =
244 |         Arrays.asList("1.0.0-M1", "1.0.0-ALPHA", "1.0.0-BETA", "1.0.0-RC1", "1.0.0");
245 | 
246 |     // When - Sort in ascending order
247 |     List<String> sorted =
248 |         versions.stream()
249 |             .sorted(versionComparator)
250 |             .toList(); // Then - Should be in Maven order: alpha < beta < milestone < rc < release
251 |     assertThat(sorted)
252 |         .containsExactly("1.0.0-ALPHA", "1.0.0-BETA", "1.0.0-M1", "1.0.0-RC1", "1.0.0");
253 |   }
254 | 
255 |   @Test
256 |   void testMixedVersionFormats() {
257 |     // Given - Versions with different formats
258 |     List<String> versions = Arrays.asList("1", "1.0", "1.0.0", "1.0.1", "1.1", "2");
259 | 
260 |     // When - Sort in ascending order
261 |     List<String> sorted = versions.stream().sorted(versionComparator).toList();
262 | 
263 |     // Then - Should handle different formats correctly
264 |     assertThat(sorted).containsExactly("1", "1.0", "1.0.0", "1.0.1", "1.1", "2");
265 |   }
266 | 
267 |   @Test
268 |   void testConsistencyWithEquals() {
269 |     // Given - Versions that should be equal
270 |     String[][] equalVersionPairs = {
271 |       {"1.0.0", "1.0.0"},
272 |       {"1.0", "1.0.0"}, // Assuming this should be equal
273 |       {"1.0.0-RC1", "1.0.0-rc1"}
274 |     };
275 | 
276 |     for (String[] pair : equalVersionPairs) {
277 |       // When
278 |       int result1 = versionComparator.compare(pair[0], pair[1]);
279 |       int result2 = versionComparator.compare(pair[1], pair[0]);
280 | 
281 |       // Then - If equal, both comparisons should return 0
282 |       if (result1 == 0) {
283 |         assertThat(result2).isZero();
284 |       } else {
285 |         // If not equal, they should be opposites
286 |         assertThat(result1 * result2).isNegative();
287 |       }
288 |     }
289 |   }
290 | 
291 |   @Test
292 |   void testTransitivity() {
293 |     // Given - Three versions
294 |     String version1 = "1.0.0";
295 |     String version2 = "1.5.0";
296 |     String version3 = "2.0.0";
297 | 
298 |     // When
299 |     int compare12 = versionComparator.compare(version1, version2);
300 |     int compare23 = versionComparator.compare(version2, version3);
301 |     int compare13 = versionComparator.compare(version1, version3);
302 | 
303 |     // Then - If version1 < version2 and version2 < version3, then version1 < version3
304 |     if (compare12 < 0 && compare23 < 0) {
305 |       assertThat(compare13).isNegative();
306 |     }
307 |   }
308 | 
309 |   @Test
310 |   void testLargeVersionNumbers() {
311 |     // Given - Versions with large numbers
312 |     String[] versions = {"999.999.999", "1000.0.0", "1000.1.0"};
313 | 
314 |     // When
315 |     String latest = VersionComparator.getLatest(versions);
316 | 
317 |     // Then
318 |     assertThat(latest).isEqualTo("1000.1.0");
319 |   }
320 | 
321 |   @Test
322 |   void testVersionsWithLeadingZeros() {
323 |     // Given - Versions that might have leading zeros
324 |     String version1 = "1.01.0";
325 |     String version2 = "1.1.0";
326 | 
327 |     // When
328 |     int result = versionComparator.compare(version1, version2);
329 | 
330 |     // Then - Should handle leading zeros correctly (01 should equal 1)
331 |     assertThat(result).isZero();
332 |   }
333 | 
334 |   @ParameterizedTest
335 |   @MethodSource("updateTypeTestData")
336 |   void testDetermineUpdateType(
337 |       String current, String latest, String expectedUpdateType, String description) {
338 |     // When
339 |     String updateType = versionComparator.determineUpdateType(current, latest);
340 | 
341 |     // Then
342 |     assertThat(updateType).as(description).isEqualTo(expectedUpdateType);
343 |   }
344 | 
345 |   @Test
346 |   void testCriticalVersionParsingScenarios() {
347 |     // Test the critical parsing scenarios that were failing before the fix
348 | 
349 |     // Spring Boot milestone versions (previously corrupted by ComparableVersion.toString())
350 |     assertThat(versionComparator.determineUpdateType("2.0.0-M1", "2.0.0")).isEqualTo("patch");
351 |     assertThat(versionComparator.determineUpdateType("1.5.0", "2.0.0-M1")).isEqualTo("major");
352 | 
353 |     // Complex pre-release versions
354 |     assertThat(versionComparator.determineUpdateType("3.1.0-RC1", "3.1.0")).isEqualTo("patch");
355 |     assertThat(versionComparator.determineUpdateType("1.0.0-alpha", "1.0.0-beta"))
356 |         .isEqualTo("patch");
357 | 
358 |     // Verify version parsing doesn't corrupt the original strings
359 |     VersionComparator.VersionComponents parsed1 = versionComparator.parseVersion("2.0.0-M1");
360 |     assertThat(parsed1.qualifier()).isEqualTo("m1");
361 |     assertThat(parsed1.numericParts()).containsExactly(2, 0, 0);
362 | 
363 |     VersionComparator.VersionComponents parsed2 = versionComparator.parseVersion("1.0.0-SNAPSHOT");
364 |     assertThat(parsed2.qualifier()).isEqualTo("snapshot");
365 |     assertThat(parsed2.numericParts()).containsExactly(1, 0, 0);
366 |   }
367 | 
368 |   private static Stream<Arguments> updateTypeTestData() {
369 |     return Stream.of(
370 |         // Major updates
371 |         Arguments.of("1.0.0", "2.0.0", "major", "Major version update"),
372 |         Arguments.of("1.5.3", "2.0.0", "major", "Major version update with minor/patch"),
373 |         Arguments.of("1.0.0-alpha", "2.0.0", "major", "Major update from alpha"),
374 | 
375 |         // Minor updates
376 |         Arguments.of("1.0.0", "1.1.0", "minor", "Minor version update"),
377 |         Arguments.of("1.0.5", "1.2.0", "minor", "Minor update with patch difference"),
378 |         Arguments.of("1.0.0-beta", "1.1.0", "minor", "Minor update from beta"),
379 | 
380 |         // Patch updates
381 |         Arguments.of("1.0.0", "1.0.1", "patch", "Patch version update"),
382 |         Arguments.of("1.2.3", "1.2.4", "patch", "Simple patch update"),
383 |         Arguments.of("1.0.0-rc", "1.0.1", "patch", "Patch update from RC"),
384 | 
385 |         // No updates / equal versions
386 |         Arguments.of("1.0.0", "1.0.0", "none", "Same version"),
387 |         Arguments.of("1.0.0-alpha", "1.0.0-alpha", "none", "Same alpha version"),
388 | 
389 |         // Unknown/downgrade scenarios
390 |         Arguments.of("2.0.0", "1.0.0", "unknown", "Downgrade scenario"),
391 |         Arguments.of("1.1.0", "1.0.0", "unknown", "Minor downgrade"),
392 |         Arguments.of(null, "1.0.0", "unknown", "Null current version"),
393 |         Arguments.of("1.0.0", null, "unknown", "Null latest version"));
394 |   }
395 | 
396 |   @ParameterizedTest
397 |   @MethodSource("stableVersionTestData")
398 |   void testIsStableVersion(String version, boolean expectedStable, String description) {
399 |     // When
400 |     boolean isStable = versionComparator.isStableVersion(version);
401 | 
402 |     // Then
403 |     assertThat(isStable).as(description).isEqualTo(expectedStable);
404 |   }
405 | 
406 |   private static Stream<Arguments> stableVersionTestData() {
407 |     return Stream.of(
408 |         // Stable versions
409 |         Arguments.of("1.0.0", true, "Plain numeric version is stable"),
410 |         Arguments.of("2.1.5", true, "Multi-component numeric version is stable"),
411 |         Arguments.of("1.0.0-final", true, "Final qualifier is stable"),
412 |         Arguments.of("1.0.0-ga", true, "GA qualifier is stable"),
413 |         Arguments.of("1.0.0-release", true, "Release qualifier is stable"),
414 |         Arguments.of("1.0.0-sp1", true, "Service pack is stable"),
415 | 
416 |         // Pre-release versions
417 |         Arguments.of("1.0.0-alpha", false, "Alpha version is not stable"),
418 |         Arguments.of("1.0.0-beta", false, "Beta version is not stable"),
419 |         Arguments.of("1.0.0-rc", false, "RC version is not stable"),
420 |         Arguments.of("1.0.0-snapshot", false, "Snapshot version is not stable"),
421 |         Arguments.of("1.0.0-milestone", false, "Milestone version is not stable"),
422 | 
423 |         // Edge cases
424 |         Arguments.of(null, false, "Null version is not stable"));
425 |   }
426 | 
427 |   @ParameterizedTest
428 |   @MethodSource("versionTypeTestData")
429 |   void testGetVersionType(String version, String expectedType, String description) {
430 |     // When
431 |     String versionType = versionComparator.getVersionTypeString(version);
432 | 
433 |     // Then
434 |     assertThat(versionType).as(description).isEqualTo(expectedType);
435 |   }
436 | 
437 |   private static Stream<Arguments> versionTypeTestData() {
438 |     return Stream.of(
439 |         // Stable versions
440 |         Arguments.of("1.0.0", "stable", "Plain numeric version is stable"),
441 |         Arguments.of("1.0.0-final", "stable", "Final qualifier is stable"),
442 |         Arguments.of("1.0.0-ga", "stable", "GA qualifier is stable"),
443 |         Arguments.of("1.0.0-release", "stable", "Release qualifier is stable"),
444 | 
445 |         // Alpha versions
446 |         Arguments.of("1.0.0-alpha", "alpha", "Alpha version"),
447 |         Arguments.of("1.0.0-a1", "alpha", "Alpha with number"),
448 |         Arguments.of("1.0.0-dev", "alpha", "Dev version treated as alpha"),
449 |         Arguments.of("1.0.0-preview", "alpha", "Preview version treated as alpha"),
450 | 
451 |         // Beta versions
452 |         Arguments.of("1.0.0-beta", "beta", "Beta version"),
453 |         Arguments.of("1.0.0-b1", "beta", "Beta with number"),
454 | 
455 |         // RC versions
456 |         Arguments.of("1.0.0-rc", "rc", "RC version"),
457 |         Arguments.of("1.0.0-cr", "rc", "CR version treated as RC"),
458 |         Arguments.of("1.0.0-candidate", "rc", "Candidate version treated as RC"),
459 | 
460 |         // Milestone versions
461 |         Arguments.of("1.0.0-milestone", "milestone", "Milestone version"),
462 |         Arguments.of("1.0.0-m1", "milestone", "Milestone with number"),
463 | 
464 |         // Edge cases
465 |         Arguments.of(null, "unknown", "Null version returns unknown"),
466 |         Arguments.of("1.0.0-custom", "alpha", "Unknown qualifier defaults to alpha for safety"));
467 |   }
468 | }
469 | 
```

--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Changelog
  2 | 
  3 | All notable changes to this project will be documented in this file.
  4 | 
  5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
  6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
  7 | 
  8 | ## [Unreleased]
  9 | 
 10 | ### Added (Unreleased)
 11 | 
 12 | ### Changed (Unreleased)
 13 | 
 14 | ### Fixed (Unreleased)
 15 | 
 16 | ### Removed (Unreleased)
 17 | 
 18 | ## [1.5.1] - 2025-10-22
 19 | 
 20 | **Corporate Environment Support Release** - Adds dual-image build strategy and comprehensive documentation for corporate networks with SSL inspection/MITM proxies.
 21 | 
 22 | ### Added (1.5.1)
 23 | 
 24 | - **Corporate Certificate Guide**: Complete documentation (`CORPORATE-CERTIFICATES.md`) for building custom native images with corporate SSL certificates using Paketo buildpack certificate bindings
 25 | - **Dual-Image Build Strategy**: CI/CD now builds 4 image variants (amd64/arm64, with/without Context7) to support different deployment scenarios
 26 | - **Spring Profiles for Context7 Control**:
 27 |   - `application-no-context7.yaml` - Disables Context7 integration for corporate environments
 28 |   - `application-docker.yaml` - Controls Spring Boot banner for clean MCP protocol compliance
 29 | - **Image Variants Documentation**: Added comprehensive table in README explaining when to use each image tag (`latest`, `latest-noc7`, `<version>`, `<version>-noc7`)
 30 | 
 31 | ### Changed (1.5.1)
 32 | 
 33 | - **Build Scripts**: Updated all Unix and Windows build scripts to support `-noc7` image builds with Spring profile activation
 34 | - **README**: Added troubleshooting section for corporate SSL environments and link to certificate guide
 35 | - **CI/CD Workflow**: Enhanced to build and publish 4 multi-architecture image manifests
 36 | - **Updated Dependencies**:
 37 |   - Resilience4j updated to 2.3.0 (from 2.2.0)
 38 | 
 39 | ### Fixed (1.5.1)
 40 | 
 41 | - **SSL Handshake Issues**: Resolved Context7 connection failures in corporate environments with SSL inspection by providing `-noc7` image variants and custom certificate build solution
 42 | - **MCP Protocol Interference**: Fixed Spring Boot banner appearing before logback initialization by using `application-docker.yaml` profile
 43 | 
 44 | ## [1.5.0] - 2025-10-19
 45 | 
 46 | **Performance & Resilience Release** - Introduces OkHttp 5 for HTTP/2 support, circuit breaker patterns, and improved reliability. Includes code quality improvements and dependency updates.
 47 | 
 48 | ### Added (1.5.0)
 49 | 
 50 | - **OkHttp5 Integration**: Direct HTTP/2 support with connection pooling for improved performance and resource efficiency (5.2.1 - latest stable)
 51 | - **Resilience4j Patterns**: Added circuit breaker, retry, and rate limiter patterns to `MavenCentralService` for improved reliability
 52 | - **Connection Pool Configuration**: New `maven.central.connection-pool-size` property (default: 50) for tuning OkHttp connection pooling
 53 | - **Spring Configuration Metadata**: Added `maven.central.connection-pool-size` property documentation for IDE autocomplete support
 54 | 
 55 | ### Changed (1.5.0)
 56 | 
 57 | - **HTTP Client Architecture**: Introduced OkHttp 5.2.1 as the primary HTTP client (replacing SimpleClientHttpRequestFactory) for HTTP/2 support and improved connection pooling
 58 | - **Updated Dependencies**:
 59 |   - Spring Boot parent updated to 3.5.6 (from 3.5.4)
 60 |   - fmt-maven-plugin updated to 2.29 (from 2.27)
 61 | - **MCP Client Transport**: Changed from SYNC SSE client to ASYNC streamable-http transport for better performance and compatibility
 62 | 
 63 | ### Removed (1.5.0)
 64 | 
 65 | - **Test Utilities**: Removed unused `ClientStdio` test class (62 lines)
 66 | - **Legacy REST Configuration**: Removed direct `SimpleClientHttpRequestFactory` bean in favor of OkHttp5-backed RestClient
 67 | 
 68 | ## [1.4.0] - 2025-08-17
 69 | 
 70 | **Direct Maven Repository Access Release** - All existing MCP tools and features are fully retained with significantly improved accuracy and performance by reading maven-metadata.xml files directly from Maven Central instead of using the search API.
 71 | 
 72 | ### Added (1.4.0)
 73 | 
 74 | - **Maven Metadata XML Parsing:** Now reads maven-metadata.xml files directly from Maven Central repository for accurate version information
 75 | - **Jackson XML Support:** Added XML parsing capabilities for universal JVM build tool support
 76 | - **Direct Repository Tests:** Comprehensive test coverage for maven-metadata.xml access functionality
 77 | - **Improved Version Accuracy:** Eliminates search API delays and version ordering quirks
 78 | 
 79 | ### Changed (1.4.0)
 80 | 
 81 | - **Data Source:** Now uses `https://repo1.maven.org/maven2` maven-metadata.xml files instead of `search.maven.org` Solr API
 82 | - **Timestamp Accuracy:** Implemented a more accurate timestamp retrieval method that fetches real timestamps for recent versions via HTTP HEAD requests.
 83 | - **Enhanced Performance:** Smaller XML metadata files provide faster response times than large JSON search results
 84 | - **Simplified Configuration:** Removed complex strategy patterns for cleaner, more maintainable codebase
 85 | - **Updated Dependencies:** Added Jackson XML module for maven-metadata.xml parsing support
 86 | 
 87 | ### Fixed (1.4.0)
 88 | 
 89 | - **Version Ordering Issues:** Direct repository access provides accurate version ordering without Solr search index delays
 90 | - **Date-like Version Anomalies:** Fixed incorrect "latest" results for artifacts like commons-io with date-like versions
 91 | - **JGit Release Classification:** Service pack releases with `-r` suffix now correctly classified as stable
 92 | - **Timestamp Analysis:** Repository metadata provides authoritative version information for analytical features
 93 | - **Error Handling:** Graceful null returns for non-existent artifacts instead of exceptions for better API consistency
 94 | - **Date/Time Processing:** Improved timestamp parsing using proper Java time APIs for more accurate analytical features
 95 | 
 96 | ### Removed (1.4.0)
 97 | 
 98 | - **Search API Dependency:** Eliminated reliance on `search.maven.org/solrsearch/select` for core functionality
 99 | - **Unused Models:** Removed the obsolete `MavenSearchResponse` model after refactoring.
100 | - **Strategy Configuration:** Removed complex strategy patterns for simplified architecture  
101 | - **Legacy Properties:** Deprecated `maven.central.base-url` in favor of `maven.central.repository-base-url`
102 | 
103 | ## [1.3.0] - 2025-08-09
104 | 
105 | ### Added (1.3.0)
106 | 
107 | - **Type-safe ToolResponse architecture**: Unified response wrapper for all MCP tools with sealed interface pattern
108 | - **Performance optimizations**: Early-exit algorithms with 50-80% performance improvements
109 | - **Enhanced test coverage**: Critical version parsing test scenarios for complex pre-release versions
110 | - **Simplified Context7 orchestration**: Clear step-by-step tool usage instructions with web search fallback
111 | 
112 | ### Changed (1.3.0)
113 | 
114 | - **BREAKING**: All MCP tool methods now return `ToolResponse` instead of JSON strings for better type safety
115 | - **Context7Guidance model**: Simplified from 5 fields to single `orchestrationInstructions` field for better clarity
116 | - **Library name resolution**: Now uses Maven artifactId directly instead of ecosystem-specific mapping for universal coverage
117 | - **Response format**: Consistent response structure with `ToolResponse.Success<T>` and `ToolResponse.Error`
118 | - **User-Agent Header**: Updated to Maven-Tools-MCP/1.3.0
119 | - **Native image configuration**: Updated reflection hints to include ToolResponse and nested records
120 | - **Algorithm optimizations**: Implemented early-exit optimization in version type classification
121 | - **Stream operations**: Replaced manual loops with optimized stream operations where beneficial
122 | - **Time calculations**: Deduplicated redundant timestamp arithmetic operations
123 | 
124 | ### Fixed (1.3.0)
125 | 
126 | - **Critical version parsing bug**: Fixed corruption of pre-release versions (e.g., "2.0.0-M1" was becoming "2-milestone-1")
127 | - **Cache configuration**: Resolved type collision issues by using separate cache instances per region
128 | - **SonarQube warnings**: Resolved string literal duplication, cognitive complexity, and primitive null comparison issues
129 | 
130 | ### Removed (1.3.0)
131 | 
132 | - **Unused code**: Removed duplicate ToolResponseOperation interface and associated error handling method
133 | - **Obsolete dependencies**: Cleaned up JsonResponseService references and inline imports
134 | - **Context7 complexity**: Eliminated redundant guidance fields (suggestedSearch, searchHints, complexity, documentationFocus)
135 | - **Ecosystem-specific logic**: Removed 70+ lines of hardcoded Spring/Hibernate/Jackson library mapping for maintainability
136 | 
137 | ## [1.2.0] - 2025-07-24
138 | 
139 | ### Added (1.2.0)
140 | 
141 | - **Guided Delegation Architecture**: Context7 guidance hints in Maven tool responses for intelligent LLM orchestration
142 | - `Context7Properties` configuration with `context7.enabled` setting (defaults to true)
143 | - `Context7Guidance` model with smart search suggestions and ecosystem-specific hints
144 | - Context7 guidance integration in response models (when context7.enabled=true):
145 |   - `VersionComparisonResponse` includes migration guidance hints for updates
146 |   - `DependencyAgeResponse` includes modernization guidance for aging/stale dependencies  
147 |   - `ProjectHealthAnalysis` includes upgrade guidance hints for health issues
148 | - Raw Context7 MCP tools automatically exposed via Spring AI MCP client:
149 |   - `resolve-library-id` - Library search and resolution (when context7.enabled=true)
150 |   - `get-library-docs` - Documentation retrieval with topic queries (when context7.enabled=true)
151 | - Enhanced tool descriptions with Context7 guidance references
152 | - Type-safe record models: `ProjectHealthAnalysis`, `VersionsByType` replacing HashMap usage
153 | - `JacksonConfig` with JDK8 and JSR310 module registration for serialization support
154 | 
155 | ### Changed (1.2.0)
156 | 
157 | - **Simplified Architecture**: Eliminated complex internal Context7 integration (688 lines removed)
158 | - **Tool Consolidation**: Reduced from 10 to 8 tools for better usability:
159 |   - Enhanced `get_latest_version` to replace `get_stable_version` with `preferStable` parameter
160 |   - Enhanced `check_multiple_dependencies` to replace `check_multiple_stable_versions` with `stableOnly` parameter
161 | - **Guided Delegation**: Maven tools now provide Context7 guidance hints instead of internal documentation calls
162 | - **Conditional Context7 Guidance**: Guidance hints only included when `context7.enabled=true` (enabled by default)
163 | - Removed Context7 parameters from Maven tools (includeMigrationGuidance, includeUpgradeStrategy, includeModernizationGuidance)
164 | - Context7 integration enabled by default (context7.enabled=true) with clean responses when disabled
165 | - Updated tool descriptions to reference separate Context7 tool usage for documentation needs
166 | 
167 | ### Fixed (1.2.0)
168 | 
169 | - Eliminated Context7 data quality issues through guided delegation approach
170 | - Simplified maintenance by removing complex MCP client orchestration
171 | - Enhanced transparency - LLMs can adapt when Context7 returns incorrect results
172 | 
173 | ### Removed (1.2.0)
174 | 
175 | - **Complex Context7 Integration**: Removed 688 lines of internal Context7 integration code:
176 |   - `DocumentationEnrichmentService` (replaced with guided delegation)
177 |   - `DocumentationEnrichmentProperties` configuration
178 |   - Complex MCP client orchestration and error handling
179 | - **Context7 Tool Parameters**: Removed from Maven tool method signatures:
180 |   - `includeMigrationGuidance` parameter
181 |   - `includeUpgradeStrategy` parameter  
182 |   - `includeModernizationGuidance` parameter
183 | - **Deprecated Tools**: Removed redundant tools to reduce cognitive overload:
184 |   - `get_stable_version` (functionality moved to `get_latest_version` with `preferStable=true`)
185 |   - `check_multiple_stable_versions` (functionality moved to `check_multiple_dependencies` with `stableOnly=true`)
186 | 
187 | ## [1.1.1] - 2025-07-23
188 | 
189 | ### Fixed (1.1.1)
190 | 
191 | - Native image configuration for analytical intelligence models
192 | - Added reflection hints for all v1.1.0 analytical model classes (DependencyAgeAnalysis, ReleasePatternAnalysis, VersionTimelineAnalysis)
193 | - Proper native image compatibility for new analytical features
194 | 
195 | ## [1.1.0] - 2025-07-23
196 | 
197 | ### Added (1.1.0)
198 | 
199 | - **Analytical Intelligence Tools**: Four new MCP tools for advanced dependency analysis
200 |   - `analyze_dependency_age` - Classify dependencies as fresh/current/aging/stale with actionable insights
201 |   - `analyze_release_patterns` - Analyze maintenance activity, release velocity, and predict next releases
202 |   - `get_version_timeline` - Enhanced version timeline with temporal analysis and release gap detection
203 |   - `analyze_project_health` - Comprehensive health scoring for multiple dependencies with risk assessment
204 | - **Enhanced MavenCentralService**: Added timestamp-aware methods for age analysis (`getAllVersionsWithTimestamps`, `getRecentVersionsWithTimestamps`)
205 | - **New Model Classes**: Added comprehensive analytical data structures (`DependencyAgeAnalysis`, `ReleasePatternAnalysis`, `VersionTimelineAnalysis`)
206 | - **Virtual Thread Support**: Concurrent bulk analysis for improved performance
207 | - **New Parameters**: Added analytical parameters (`maxAgeInDays`, `monthsToAnalyze`, `versionCount`)
208 | 
209 | ### Changed (1.1.0)
210 | 
211 | - **Enhanced Tool Descriptions**: Updated existing tool descriptions for better clarity and universal JVM build tool support
212 | - **Improved Documentation**: Updated README.md and CLAUDE.md with analytical intelligence examples and scope decisions
213 | - **User-Agent Header**: Updated to Maven-Tools-MCP/1.1.0
214 | 
215 | ### Performance
216 | 
217 | - **Bulk Analysis**: Added concurrent processing for multiple dependency health analysis
218 | - **Caching**: Analytical tools leverage existing 24-hour cache infrastructure
219 | - **Memory Optimization**: Efficient data structures for timeline and pattern analysis
220 | 
221 | ## [1.0.0] - 2025-07-23
222 | 
223 | ### Breaking Changes
224 | 
225 | This major release updates tool names and adds stability parameters while maintaining compatibility with all JVM build tools.
226 | 
227 | ### ⚠️ BREAKING CHANGES
228 | 
229 | **Tool Renaming for Universal Appeal:**
230 | 
231 | - `maven_get_latest` → `get_latest_version`
232 | - `maven_get_stable` → `get_stable_version`
233 | - `maven_check_exists` → `check_version_exists`
234 | - `maven_bulk_check_latest` → `check_multiple_dependencies`
235 | - `maven_bulk_check_stable` → `check_multiple_stable_versions`
236 | - `maven_compare_versions` → `compare_dependency_versions`
237 | 
238 | ### Added (1.0.0)
239 | 
240 | **New Tool Parameters:**
241 | 
242 | - `preferStable` parameter for `get_latest_version` - prioritizes stable versions in comprehensive analysis
243 | - `stableOnly` parameter for `check_multiple_dependencies` - filters to production-ready versions only
244 | - `onlyStableTargets` parameter for `compare_dependency_versions` - only suggests stable upgrades for production safety
245 | 
246 | **JVM Build Tool Support:**
247 | 
248 | - Support for Maven, Gradle, SBT, Mill, and any JVM build tool
249 | - Standard Maven coordinate format for all tools
250 | - Cross-platform examples and documentation
251 | 
252 | **Stability Controls:**
253 | 
254 | - Stability preference controls across all tools
255 | - Filtering options for production deployments
256 | - Upgrade safety controls
257 | 
258 | ### Changed (1.0.0)
259 | 
260 | **Documentation Updates:**
261 | 
262 | - Application name remains `maven-tools-mcp` for consistency
263 | - Tool descriptions updated for JVM ecosystem support
264 | - Multi-build tool examples and scenarios
265 | - Examples include Kotlin, Scala, Retrofit, Spark dependencies
266 | 
267 | **README Updates:**
268 | 
269 | - Build tool support matrix
270 | - Usage examples with stability controls
271 | - Multi-build tool use cases
272 | - Production deployment examples
273 | 
274 | **Technical:**
275 | 
276 | - Updated tool method names and parameters
277 | - Version updated to 1.0.0
278 | - Test suite updated for new signatures
279 | 
280 | ## [0.1.3] - 2025-06-30
281 | 
282 | ### Added (0.1.3)
283 | 
284 | - Comprehensive version info in dependency check results and improved JSON serialization
285 | - Support for multiple builder platforms in Docker configuration (native AMD64 and ARM64 builds)
286 | - Helpful hints and format examples in tool descriptions for better LLM guidance
287 | - Demo GIF and improved documentation for setup and usage
288 | 
289 | ### Changed (0.1.3)
290 | 
291 | - Refactored and clarified README with detailed command descriptions, examples, and improved user guidance
292 | - Upgraded Spring Boot to 3.5.3
293 | - Internal refactoring for maintainability and clarity
294 | - Improved formatting and readability of tool descriptions
295 | 
296 | ### Fixed (0.1.3)
297 | 
298 | - Docker build and manifest creation for multi-architecture images
299 | - Build scripts and Docker image configuration for reliability and compatibility
300 | - Response structure for bulk and compare tools
301 | 
302 | ## [0.1.2] - 2025-06-09
303 | 
304 | ### Added (0.1.2)
305 | 
306 | - Docker support for MCP server deployment with pre-built images and Docker Compose
307 | 
308 | ### Changed (0.1.2)
309 | 
310 | - Enhanced build tooling and documentation for Docker deployment
311 | - Use maven-artifact for version comparisons
312 | 
313 | ## [0.1.1] - 2025-06-08
314 | 
315 | ### Added (0.1.1)
316 | 
317 | - Virtual thread support for optimal I/O-bound performance in bulk operations
318 | 
319 | ### Changed (0.1.1)
320 | 
321 | - Extended cache TTL from 1 hour to 24 hours for optimal performance
322 | 
323 | ### Removed (0.1.1)
324 | 
325 | - Support for 'snapshot' version type in all tools, API responses, and documentation. Only stable, rc, beta, alpha, and milestone are now supported.
326 |   - Reason: The underlying Maven Central API used for dependency search does not manage or return snapshot versions, so accurate and reliable snapshot support is not possible.
327 | - All code, documentation, and tests for the `maven_analyze_pom` tool (POM file analysis) have been removed.
328 |   - Reason: Full POM analysis is not efficient or LLM-friendly, and is out of scope for this project. Only direct dependency/version tools are supported.
329 | 
330 | ## [0.1.0] - 2025-06-07
331 | 
332 | ### Added (0.1.0)
333 | 
334 | - Initial release
335 | - MCP tools for Maven dependency management:
336 |   - `maven_get_latest` - Get latest version (stable, rc, beta, alpha, milestone)
337 |   - `maven_check_exists` - Check if version exists
338 |   - `maven_get_stable` - Get latest stable version
339 |   - `maven_bulk_check_latest` - Bulk version checking
340 |   - `maven_bulk_check_stable` - Bulk stable version checking
341 |   - `maven_analyze_pom` - POM file analysis
342 |   - `maven_compare_versions` - Version comparison
343 | - Caching with 1 hour TTL
344 | - Version classification (stable, rc, beta, alpha, milestone)
345 | - Works with Claude Desktop and GitHub Copilot
346 | 
347 | ### Technical (0.1.0)
348 | 
349 | - Java 24, Spring Boot 3.5.4, Spring AI
350 | - MCP Protocol 2024-11-05
351 | - Unit and integration tests
352 | - Maven Central API integration
353 | 
354 | [Unreleased]: https://github.com/arvindand/maven-tools-mcp/compare/v1.4.0...HEAD
355 | [1.4.0]: https://github.com/arvindand/maven-tools-mcp/compare/v1.3.0...v1.4.0
356 | [1.3.0]: https://github.com/arvindand/maven-tools-mcp/compare/v1.2.0...v1.3.0
357 | [1.2.0]: https://github.com/arvindand/maven-tools-mcp/compare/v1.1.1...v1.2.0
358 | [1.1.1]: https://github.com/arvindand/maven-tools-mcp/compare/v1.1.0...v1.1.1
359 | [1.1.0]: https://github.com/arvindand/maven-tools-mcp/compare/v1.0.0...v1.1.0
360 | [1.0.0]: https://github.com/arvindand/maven-tools-mcp/compare/v0.1.3...v1.0.0
361 | [0.1.3]: https://github.com/arvindand/maven-tools-mcp/compare/v0.1.2...v0.1.3
362 | [0.1.2]: https://github.com/arvindand/maven-tools-mcp/compare/v0.1.1...v0.1.2
363 | [0.1.1]: https://github.com/arvindand/maven-tools-mcp/compare/v0.1.0...v0.1.1
364 | [0.1.0]: https://github.com/arvindand/maven-tools-mcp/releases/tag/v0.1.0
365 | 
```

--------------------------------------------------------------------------------
/src/main/java/com/arvindand/mcp/maven/service/MavenDependencyTools.java:
--------------------------------------------------------------------------------

```java
   1 | package com.arvindand.mcp.maven.service;
   2 | 
   3 | import com.arvindand.mcp.maven.config.Context7Properties;
   4 | import com.arvindand.mcp.maven.model.BulkCheckResult;
   5 | import com.arvindand.mcp.maven.model.DependencyAge;
   6 | import com.arvindand.mcp.maven.model.DependencyAgeAnalysis;
   7 | import com.arvindand.mcp.maven.model.DependencyInfo;
   8 | import com.arvindand.mcp.maven.model.MavenArtifact;
   9 | import com.arvindand.mcp.maven.model.MavenCoordinate;
  10 | import com.arvindand.mcp.maven.model.ProjectHealthAnalysis;
  11 | import com.arvindand.mcp.maven.model.ReleasePatternAnalysis;
  12 | import com.arvindand.mcp.maven.model.StabilityFilter;
  13 | import com.arvindand.mcp.maven.model.ToolResponse;
  14 | import com.arvindand.mcp.maven.model.VersionComparison;
  15 | import com.arvindand.mcp.maven.model.VersionInfo;
  16 | import com.arvindand.mcp.maven.model.VersionInfo.VersionType;
  17 | import com.arvindand.mcp.maven.model.VersionTimelineAnalysis;
  18 | import com.arvindand.mcp.maven.model.VersionTimelineAnalysis.RecentActivity.ActivityLevel;
  19 | import com.arvindand.mcp.maven.model.VersionTimelineAnalysis.TimelineEntry.ReleaseGap;
  20 | import com.arvindand.mcp.maven.model.VersionTimelineAnalysis.VelocityTrend.TrendDirection;
  21 | import com.arvindand.mcp.maven.model.VersionsByType;
  22 | import com.arvindand.mcp.maven.util.MavenCoordinateParser;
  23 | import com.arvindand.mcp.maven.util.VersionComparator;
  24 | import java.time.Duration;
  25 | import java.time.Instant;
  26 | import java.time.temporal.ChronoUnit;
  27 | import java.util.ArrayList;
  28 | import java.util.Arrays;
  29 | import java.util.EnumSet;
  30 | import java.util.HashMap;
  31 | import java.util.List;
  32 | import java.util.Map;
  33 | import java.util.Optional;
  34 | import java.util.Set;
  35 | import java.util.concurrent.CompletableFuture;
  36 | import java.util.concurrent.Executors;
  37 | import java.util.stream.Collectors;
  38 | import org.slf4j.Logger;
  39 | import org.slf4j.LoggerFactory;
  40 | import org.springframework.ai.tool.annotation.Tool;
  41 | import org.springframework.ai.tool.annotation.ToolParam;
  42 | import org.springframework.lang.Nullable;
  43 | import org.springframework.stereotype.Service;
  44 | 
  45 | /**
  46 |  * Main service providing MCP tools for Maven dependency management.
  47 |  *
  48 |  * @author Arvind Menon
  49 |  * @since 0.1.0
  50 |  */
  51 | @Service
  52 | public class MavenDependencyTools {
  53 | 
  54 |   private static final String UNEXPECTED_ERROR = "Unexpected error";
  55 |   private static final String MAVEN_CENTRAL_ERROR = "Maven Central error: ";
  56 |   private static final String INVALID_MAVEN_COORDINATE_FORMAT = "Invalid Maven coordinate format: ";
  57 |   private static final String SUCCESS_STATUS = "success";
  58 |   private static final String ACTIVE_MAINTENANCE = "active";
  59 | 
  60 |   // Health level constants
  61 |   private static final String EXCELLENT_HEALTH = "excellent";
  62 |   private static final String GOOD_HEALTH = "good";
  63 |   private static final String FAIR_HEALTH = "fair";
  64 |   private static final String POOR_HEALTH = "poor";
  65 | 
  66 |   // Age classification constants
  67 |   private static final String FRESH_AGE = "fresh";
  68 |   private static final String CURRENT_AGE = "current";
  69 |   private static final String AGING_AGE = "aging";
  70 |   private static final String STALE_AGE = "stale";
  71 |   private static final Logger logger = LoggerFactory.getLogger(MavenDependencyTools.class);
  72 | 
  73 |   // Analysis constants
  74 |   private static final int DEFAULT_ANALYSIS_MONTHS = 24;
  75 |   private static final int DEFAULT_VERSION_COUNT = 20;
  76 |   private static final int ACCURATE_TIMESTAMP_VERSION_LIMIT = 30;
  77 |   private static final int RECENT_VERSIONS_LIMIT = 10;
  78 |   private static final int MILLISECONDS_TO_DAYS = 1000 * 60 * 60 * 24;
  79 |   private static final int DAYS_IN_MONTH = 30;
  80 | 
  81 |   // Health scoring constants
  82 |   private static final int PERFECT_HEALTH_SCORE = 100;
  83 |   private static final int CURRENT_VERSION_PENALTY = 10;
  84 |   private static final int AGING_VERSION_PENALTY = 30;
  85 |   private static final int STALE_VERSION_PENALTY = 60;
  86 |   private static final int MODERATE_MAINTENANCE_PENALTY = 15;
  87 |   private static final int SLOW_MAINTENANCE_PENALTY = 40;
  88 |   private static final int AGE_THRESHOLD_PENALTY = 20;
  89 | 
  90 |   // Health classification thresholds
  91 |   private static final int EXCELLENT_HEALTH_THRESHOLD = 80;
  92 |   private static final int GOOD_HEALTH_THRESHOLD = 65;
  93 |   private static final int FAIR_HEALTH_THRESHOLD = 50;
  94 | 
  95 |   // Stability analysis constants
  96 |   private static final int VERY_HIGH_STABILITY_THRESHOLD = 80;
  97 |   private static final int LOW_STABILITY_THRESHOLD = 50;
  98 |   private final MavenCentralService mavenCentralService;
  99 |   private final VersionComparator versionComparator;
 100 |   private final Context7Properties context7Properties;
 101 | 
 102 |   public MavenDependencyTools(
 103 |       MavenCentralService mavenCentralService,
 104 |       VersionComparator versionComparator,
 105 |       Context7Properties context7Properties) {
 106 |     this.mavenCentralService = mavenCentralService;
 107 |     this.versionComparator = versionComparator;
 108 |     this.context7Properties = context7Properties;
 109 |   }
 110 | 
 111 |   /**
 112 |    * Common error handling for tool operations that return data objects.
 113 |    *
 114 |    * @param operation the operation to execute
 115 |    * @param <T> the return type
 116 |    * @return ToolResponse containing either success result or error
 117 |    */
 118 |   private <T> ToolResponse executeToolOperation(ToolOperation<T> operation) {
 119 |     try {
 120 |       T result = operation.execute();
 121 |       return ToolResponse.Success.of(result);
 122 |     } catch (IllegalArgumentException e) {
 123 |       return ToolResponse.Error.of(INVALID_MAVEN_COORDINATE_FORMAT + e.getMessage());
 124 |     } catch (MavenCentralException e) {
 125 |       return ToolResponse.Error.of(MAVEN_CENTRAL_ERROR + e.getMessage());
 126 |     } catch (Exception e) {
 127 |       logger.error(UNEXPECTED_ERROR, e);
 128 |       return ToolResponse.Error.of(UNEXPECTED_ERROR + ": " + e.getMessage());
 129 |     }
 130 |   }
 131 | 
 132 |   @FunctionalInterface
 133 |   private interface ToolOperation<T> {
 134 |     T execute() throws IllegalArgumentException, MavenCentralException;
 135 |   }
 136 | 
 137 |   /**
 138 |    * Get the latest version of any dependency from Maven Central (works with Maven, Gradle, SBT,
 139 |    * Mill).
 140 |    *
 141 |    * @param dependency the dependency coordinate (groupId:artifactId)
 142 |    * @param stabilityFilter controls version filtering: ALL (default), STABLE_ONLY, or PREFER_STABLE
 143 |    * @return JSON response with latest versions by type
 144 |    */
 145 |   @SuppressWarnings("java:S100") // MCP tool method naming
 146 |   @Tool(
 147 |       description =
 148 |           "Single dependency. Returns newest versions by type (stable/rc/beta/alpha/milestone). Set"
 149 |               + " stabilityFilter to ALL (default), STABLE_ONLY, or PREFER_STABLE. Use when asked:"
 150 |               + " 'what's the latest version of X?' Works with all JVM build tools.")
 151 |   public ToolResponse get_latest_version(
 152 |       @ToolParam(
 153 |               description =
 154 |                   "Maven dependency coordinate in format 'groupId:artifactId' (NO version)."
 155 |                       + " Example: 'org.springframework:spring-core'")
 156 |           String dependency,
 157 |       @ToolParam(
 158 |               description =
 159 |                   "Stability filter: ALL (all versions), STABLE_ONLY (production-ready only), or"
 160 |                       + " PREFER_STABLE (prioritize stable, show others too). Default:"
 161 |                       + " PREFER_STABLE",
 162 |               required = false)
 163 |           @Nullable
 164 |           StabilityFilter stabilityFilter) {
 165 |     return executeToolOperation(
 166 |         () -> {
 167 |           MavenCoordinate coordinate = MavenCoordinateParser.parse(dependency);
 168 |           List<String> allVersions = mavenCentralService.getAllVersions(coordinate);
 169 | 
 170 |           if (allVersions.isEmpty()) {
 171 |             return notFoundResponse(coordinate);
 172 |           }
 173 | 
 174 |           StabilityFilter filter =
 175 |               stabilityFilter != null ? stabilityFilter : StabilityFilter.PREFER_STABLE;
 176 |           return buildVersionsByType(coordinate, allVersions, filter);
 177 |         });
 178 |   }
 179 | 
 180 |   /**
 181 |    * Check if specific dependency version exists and identify its stability type.
 182 |    *
 183 |    * @param dependency the dependency coordinate (groupId:artifactId)
 184 |    * @param version the version to check
 185 |    * @return JSON response with existence status and version type
 186 |    */
 187 |   @SuppressWarnings("java:S100") // MCP tool method naming
 188 |   @Tool(
 189 |       description =
 190 |           "Single dependency + version. Validates existence on Maven Central and classifies its"
 191 |               + " stability (stable/rc/beta/alpha/milestone/snapshot). Use when asked: 'does X:Y"
 192 |               + " exist?' or 'is version V stable?'")
 193 |   public ToolResponse check_version_exists(
 194 |       @ToolParam(
 195 |               description =
 196 |                   "Maven dependency coordinate in format 'groupId:artifactId' (NO version)."
 197 |                       + " Example: 'org.springframework:spring-core'")
 198 |           String dependency,
 199 |       @ToolParam(
 200 |               description =
 201 |                   "Specific version string to check for existence. Example: '6.1.4' or"
 202 |                       + " '2.7.18-SNAPSHOT'")
 203 |           String version) {
 204 |     return executeToolOperation(
 205 |         () -> {
 206 |           MavenCoordinate coordinate = MavenCoordinateParser.parse(dependency);
 207 |           String versionToCheck = coordinate.version() != null ? coordinate.version() : version;
 208 | 
 209 |           if (versionToCheck == null || versionToCheck.trim().isEmpty()) {
 210 |             throw new IllegalArgumentException(
 211 |                 "Version must be provided either in dependency string or version parameter");
 212 |           }
 213 | 
 214 |           boolean exists = mavenCentralService.checkVersionExists(coordinate, versionToCheck);
 215 |           String versionType = versionComparator.getVersionTypeString(versionToCheck);
 216 | 
 217 |           return DependencyInfo.success(
 218 |               coordinate,
 219 |               versionToCheck,
 220 |               exists,
 221 |               versionType,
 222 |               versionComparator.isStableVersion(versionToCheck),
 223 |               null);
 224 |         });
 225 |   }
 226 | 
 227 |   /**
 228 |    * Check latest versions for multiple dependencies with filtering options.
 229 |    *
 230 |    * @param dependencies comma or newline separated list of dependency coordinates
 231 |    * @param stabilityFilter controls version filtering: ALL, STABLE_ONLY, or PREFER_STABLE
 232 |    * @return JSON response with bulk check results
 233 |    */
 234 |   @SuppressWarnings("java:S100") // MCP tool method naming
 235 |   @Tool(
 236 |       description =
 237 |           "Bulk. For many coordinates (no versions), returns per-dependency latest versions by"
 238 |               + " type. Set stabilityFilter to ALL (default), STABLE_ONLY, or PREFER_STABLE."
 239 |               + " Use for audits of multiple dependencies.")
 240 |   public ToolResponse check_multiple_dependencies(
 241 |       @ToolParam(
 242 |               description =
 243 |                   "Comma or newline separated list of Maven dependency coordinates in format"
 244 |                       + " 'groupId:artifactId' (NO versions). Example:"
 245 |                       + " 'org.springframework:spring-core,junit:junit'")
 246 |           String dependencies,
 247 |       @ToolParam(
 248 |               description =
 249 |                   "Stability filter: ALL (all versions), STABLE_ONLY (production-ready only), or"
 250 |                       + " PREFER_STABLE (prioritize stable). Default: ALL",
 251 |               required = false)
 252 |           @Nullable
 253 |           StabilityFilter stabilityFilter) {
 254 |     return executeToolOperation(
 255 |         () -> {
 256 |           List<String> depList = parseDependencies(dependencies);
 257 |           StabilityFilter filter = stabilityFilter != null ? stabilityFilter : StabilityFilter.ALL;
 258 | 
 259 |           List<BulkCheckResult> results;
 260 |           try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
 261 |             List<CompletableFuture<BulkCheckResult>> futures =
 262 |                 depList.stream()
 263 |                     .distinct()
 264 |                     .map(
 265 |                         dep ->
 266 |                             CompletableFuture.supplyAsync(
 267 |                                 () -> processVersionCheck(dep, filter), executor))
 268 |                     .toList();
 269 |             results = futures.stream().map(CompletableFuture::join).toList();
 270 |           }
 271 | 
 272 |           return results;
 273 |         });
 274 |   }
 275 | 
 276 |   /**
 277 |    * Compare current dependency versions with latest available and show upgrade recommendations.
 278 |    *
 279 |    * @param currentDependencies comma or newline separated list of dependency coordinates with
 280 |    *     versions
 281 |    * @param stabilityFilter controls upgrade targets: ALL, STABLE_ONLY, or PREFER_STABLE
 282 |    * @return JSON response with version comparison and update recommendations
 283 |    */
 284 |   @SuppressWarnings("java:S100") // MCP tool method naming
 285 |   @Tool(
 286 |       description =
 287 |           "Bulk compare. Input includes versions. Suggests upgrades and classifies update type"
 288 |               + " (major/minor/patch). Set stabilityFilter to ALL (default), STABLE_ONLY, or"
 289 |               + " PREFER_STABLE. Never suggests downgrades.")
 290 |   public ToolResponse compare_dependency_versions(
 291 |       @ToolParam(
 292 |               description =
 293 |                   "Comma or newline separated list of dependency coordinates WITH versions in"
 294 |                       + " format 'groupId:artifactId:version'. Example:"
 295 |                       + " 'org.springframework:spring-core:6.0.0,junit:junit:4.12'")
 296 |           String currentDependencies,
 297 |       @ToolParam(
 298 |               description =
 299 |                   "Stability filter: ALL (any version), STABLE_ONLY (production-ready only), or"
 300 |                       + " PREFER_STABLE (prioritize stable). Default: ALL",
 301 |               required = false)
 302 |           @Nullable
 303 |           StabilityFilter stabilityFilter) {
 304 |     return executeToolOperation(
 305 |         () -> {
 306 |           List<String> depList = parseDependencies(currentDependencies);
 307 |           StabilityFilter filter = stabilityFilter != null ? stabilityFilter : StabilityFilter.ALL;
 308 | 
 309 |           List<VersionComparison.DependencyComparisonResult> results;
 310 |           try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
 311 |             List<CompletableFuture<VersionComparison.DependencyComparisonResult>> futures =
 312 |                 depList.stream()
 313 |                     .distinct()
 314 |                     .map(
 315 |                         dep ->
 316 |                             CompletableFuture.supplyAsync(
 317 |                                 () -> compareDependencyVersion(dep, filter), executor))
 318 |                     .toList();
 319 |             results = futures.stream().map(CompletableFuture::join).toList();
 320 |           }
 321 | 
 322 |           VersionComparison.UpdateSummary summary = calculateUpdateSummary(results);
 323 |           return new VersionComparison(Instant.now(), results, summary);
 324 |         });
 325 |   }
 326 | 
 327 |   /**
 328 |    * Analyze dependency age and freshness classification with actionable insights.
 329 |    *
 330 |    * @param dependency the dependency coordinate (groupId:artifactId)
 331 |    * @param maxAgeInDays optional maximum acceptable age in days (default: no limit)
 332 |    * @return JSON response with age analysis and recommendations
 333 |    */
 334 |   @SuppressWarnings("java:S100") // MCP tool method naming
 335 |   @Tool(
 336 |       description =
 337 |           "Single dependency. Returns days since last release and freshness"
 338 |               + " (fresh/current/aging/stale), with actionable insights. Use when asked about 'how"
 339 |               + " old' or 'last release' of a library.")
 340 |   public ToolResponse analyze_dependency_age(
 341 |       @ToolParam(
 342 |               description =
 343 |                   "Maven dependency coordinate in format 'groupId:artifactId' (NO version)."
 344 |                       + " Example: 'org.springframework:spring-core'")
 345 |           String dependency,
 346 |       @ToolParam(
 347 |               description =
 348 |                   "Optional maximum acceptable age threshold in days. If specified and dependency"
 349 |                       + " exceeds this age, additional recommendations are provided. No limit if"
 350 |                       + " not specified",
 351 |               required = false)
 352 |           @Nullable
 353 |           Integer maxAgeInDays) {
 354 |     return executeToolOperation(
 355 |         () -> {
 356 |           MavenCoordinate coordinate = MavenCoordinateParser.parse(dependency);
 357 |           List<MavenArtifact> versions =
 358 |               mavenCentralService.getRecentVersionsWithAccurateTimestamps(coordinate, 1);
 359 | 
 360 |           if (versions.isEmpty()) {
 361 |             return notFoundResponse(coordinate);
 362 |           }
 363 | 
 364 |           MavenArtifact latestVersion = versions.get(0);
 365 |           DependencyAgeAnalysis basicAnalysis =
 366 |               DependencyAgeAnalysis.fromTimestamp(
 367 |                   coordinate.toCoordinateString(),
 368 |                   latestVersion.version(),
 369 |                   latestVersion.timestamp());
 370 | 
 371 |           // Add custom recommendation if maxAgeInDays is specified
 372 |           DependencyAgeAnalysis analysis = basicAnalysis;
 373 |           if (maxAgeInDays != null && basicAnalysis.daysSinceLastRelease() > maxAgeInDays) {
 374 |             analysis =
 375 |                 new DependencyAgeAnalysis(
 376 |                     basicAnalysis.dependency(),
 377 |                     basicAnalysis.latestVersion(),
 378 |                     basicAnalysis.ageClassification(),
 379 |                     basicAnalysis.daysSinceLastRelease(),
 380 |                     basicAnalysis.lastReleaseDate(),
 381 |                     basicAnalysis.ageDescription(),
 382 |                     "Exceeds specified age threshold of "
 383 |                         + maxAgeInDays
 384 |                         + " days - "
 385 |                         + basicAnalysis.recommendation());
 386 |           }
 387 | 
 388 |           // Create response with basic analysis
 389 |           return DependencyAge.from(analysis, context7Properties.enabled());
 390 |         });
 391 |   }
 392 | 
 393 |   /**
 394 |    * Analyze release patterns and maintenance activity to predict future releases.
 395 |    *
 396 |    * @param dependency the dependency coordinate (groupId:artifactId)
 397 |    * @param monthsToAnalyze number of months of history to analyze (default: 24)
 398 |    * @return JSON response with release pattern analysis and predictions
 399 |    */
 400 |   @SuppressWarnings("java:S100") // MCP tool method naming
 401 |   @Tool(
 402 |       description =
 403 |           "Single dependency. Analyzes historical releases to infer cadence, consistency, and"
 404 |               + " likely next-release timeframe. Useful for maintenance and planning.")
 405 |   public ToolResponse analyze_release_patterns(
 406 |       @ToolParam(
 407 |               description =
 408 |                   "Maven dependency coordinate in format 'groupId:artifactId' (NO version)."
 409 |                       + " Example: 'com.fasterxml.jackson.core:jackson-core'")
 410 |           String dependency,
 411 |       @ToolParam(
 412 |               description =
 413 |                   "Number of months of historical release data to analyze for patterns and"
 414 |                       + " predictions. Default is 24 months if not specified",
 415 |               required = false)
 416 |           @Nullable
 417 |           Integer monthsToAnalyze) {
 418 |     return executeToolOperation(
 419 |         () -> {
 420 |           MavenCoordinate coordinate = MavenCoordinateParser.parse(dependency);
 421 |           int analysisMonths = monthsToAnalyze != null ? monthsToAnalyze : DEFAULT_ANALYSIS_MONTHS;
 422 | 
 423 |           List<MavenArtifact> allVersions =
 424 |               mavenCentralService.getRecentVersionsWithAccurateTimestamps(
 425 |                   coordinate, ACCURATE_TIMESTAMP_VERSION_LIMIT);
 426 | 
 427 |           if (allVersions.isEmpty()) {
 428 |             throw new MavenCentralException(
 429 |                 "No versions found for " + coordinate.toCoordinateString());
 430 |           }
 431 | 
 432 |           return analyzeReleasePattern(
 433 |               coordinate.toCoordinateString(), allVersions, analysisMonths);
 434 |         });
 435 |   }
 436 | 
 437 |   /**
 438 |    * Get enhanced version timeline with temporal analysis and release patterns.
 439 |    *
 440 |    * @param dependency the dependency coordinate (groupId:artifactId)
 441 |    * @param versionCount number of recent versions to include (default: 20)
 442 |    * @return JSON response with version timeline and temporal insights
 443 |    */
 444 |   @SuppressWarnings("java:S100") // MCP tool method naming
 445 |   @Tool(
 446 |       description =
 447 |           "Single dependency. Returns a timeline of recent versions with dates, gaps, and stability"
 448 |               + " patterns. Use for quick release history snapshots.")
 449 |   public ToolResponse get_version_timeline(
 450 |       @ToolParam(
 451 |               description =
 452 |                   "Maven dependency coordinate in format 'groupId:artifactId' (NO version)."
 453 |                       + " Example: 'org.junit.jupiter:junit-jupiter'")
 454 |           String dependency,
 455 |       @ToolParam(
 456 |               description =
 457 |                   "Number of recent versions to include in timeline analysis. Default is 20"
 458 |                       + " versions if not specified. Typical range: 10-50",
 459 |               required = false)
 460 |           @Nullable
 461 |           Integer versionCount) {
 462 |     return executeToolOperation(
 463 |         () -> {
 464 |           MavenCoordinate coordinate = MavenCoordinateParser.parse(dependency);
 465 |           int maxVersions = versionCount != null ? versionCount : DEFAULT_VERSION_COUNT;
 466 | 
 467 |           List<MavenArtifact> versions =
 468 |               mavenCentralService.getRecentVersionsWithAccurateTimestamps(coordinate, maxVersions);
 469 | 
 470 |           if (versions.isEmpty()) {
 471 |             throw new MavenCentralException(
 472 |                 "No versions found for " + coordinate.toCoordinateString());
 473 |           }
 474 | 
 475 |           return analyzeVersionTimeline(coordinate.toCoordinateString(), versions);
 476 |         });
 477 |   }
 478 | 
 479 |   /**
 480 |    * Analyze overall health of multiple dependencies with age and maintenance insights.
 481 |    *
 482 |    * @param dependencies comma or newline separated list of dependency coordinates
 483 |    * @param maxAgeInDays optional maximum acceptable age in days for health scoring
 484 |    * @param stabilityFilter controls recommendations: ALL, STABLE_ONLY, or PREFER_STABLE
 485 |    * @return JSON response with project health summary and individual dependency analysis
 486 |    */
 487 |   @SuppressWarnings("java:S100") // MCP tool method naming
 488 |   @Tool(
 489 |       description =
 490 |           "Bulk project view. Summarizes health across many dependencies using age and maintenance"
 491 |               + " patterns, with concise recommendations. Set stabilityFilter to ALL (default),"
 492 |               + " STABLE_ONLY, or PREFER_STABLE for upgrade recommendations.")
 493 |   public ToolResponse analyze_project_health(
 494 |       @ToolParam(
 495 |               description =
 496 |                   "Comma or newline separated list of Maven dependency coordinates in format"
 497 |                       + " 'groupId:artifactId' (NO versions). Example:"
 498 |                       + " 'org.springframework:spring-core,junit:junit'")
 499 |           String dependencies,
 500 |       @ToolParam(
 501 |               description =
 502 |                   "Optional maximum acceptable age threshold in days for health scoring."
 503 |                       + " Dependencies exceeding this age receive lower health scores. No age"
 504 |                       + " penalty if not specified",
 505 |               required = false)
 506 |           @Nullable
 507 |           Integer maxAgeInDays,
 508 |       @ToolParam(
 509 |               description =
 510 |                   "Stability filter: ALL (any version), STABLE_ONLY (production-ready only), or"
 511 |                       + " PREFER_STABLE (prioritize stable). Default: PREFER_STABLE",
 512 |               required = false)
 513 |           @Nullable
 514 |           StabilityFilter stabilityFilter) {
 515 |     return executeToolOperation(
 516 |         () -> {
 517 |           List<String> depList = parseDependencies(dependencies);
 518 | 
 519 |           if (depList.isEmpty()) {
 520 |             throw new IllegalArgumentException("No dependencies provided for analysis");
 521 |           }
 522 | 
 523 |           // Analyze each dependency for age and patterns
 524 |           List<ProjectHealthAnalysis.DependencyHealthAnalysis> dependencyAnalyses;
 525 |           try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
 526 |             List<CompletableFuture<ProjectHealthAnalysis.DependencyHealthAnalysis>> futures =
 527 |                 depList.stream()
 528 |                     .distinct()
 529 |                     .map(
 530 |                         dep ->
 531 |                             CompletableFuture.supplyAsync(
 532 |                                 () -> analyzeSimpleDependencyHealth(dep, maxAgeInDays), executor))
 533 |                     .toList();
 534 |             dependencyAnalyses = futures.stream().map(CompletableFuture::join).toList();
 535 |           }
 536 | 
 537 |           return buildSimpleHealthSummary(dependencyAnalyses, maxAgeInDays);
 538 |         });
 539 |   }
 540 | 
 541 |   private ToolResponse notFoundResponse(MavenCoordinate coordinate) {
 542 |     String message =
 543 |         "No Maven dependency found for %s:%s%s"
 544 |             .formatted(
 545 |                 coordinate.groupId(),
 546 |                 coordinate.artifactId(),
 547 |                 coordinate.packaging() != null ? ":" + coordinate.packaging() : "");
 548 |     return ToolResponse.Error.notFound(message);
 549 |   }
 550 | 
 551 |   @SuppressWarnings("java:S1172") // preferStable is used by VersionsByType.getPreferredVersion()
 552 |   private VersionsByType buildVersionsByType(
 553 |       MavenCoordinate coordinate, List<String> allVersions, StabilityFilter stabilityFilter) {
 554 |     Map<VersionType, String> versionsByType = HashMap.newHashMap(5);
 555 | 
 556 |     for (String version : allVersions) {
 557 |       VersionType type = versionComparator.getVersionType(version);
 558 |       versionsByType.putIfAbsent(type, version);
 559 |       if (versionsByType.size() == 5) break;
 560 |     }
 561 | 
 562 |     return VersionsByType.create(
 563 |         coordinate.toCoordinateString(),
 564 |         createVersionInfo(versionsByType.get(VersionType.STABLE)),
 565 |         createVersionInfo(versionsByType.get(VersionType.RC)),
 566 |         createVersionInfo(versionsByType.get(VersionType.BETA)),
 567 |         createVersionInfo(versionsByType.get(VersionType.ALPHA)),
 568 |         createVersionInfo(versionsByType.get(VersionType.MILESTONE)),
 569 |         allVersions.size());
 570 |   }
 571 | 
 572 |   private Optional<VersionInfo> createVersionInfo(String version) {
 573 |     return version != null
 574 |         ? Optional.of(new VersionInfo(version, versionComparator.getVersionType(version)))
 575 |         : Optional.empty();
 576 |   }
 577 | 
 578 |   private List<String> parseDependencies(String dependencies) {
 579 |     if (dependencies == null || dependencies.trim().isEmpty()) {
 580 |       return List.of();
 581 |     }
 582 | 
 583 |     return dependencies
 584 |         .lines()
 585 |         .flatMap(line -> Arrays.stream(line.split(",")))
 586 |         .map(String::trim)
 587 |         .filter(dep -> !dep.isEmpty())
 588 |         .toList();
 589 |   }
 590 | 
 591 |   private BulkCheckResult processVersionCheck(String dep, StabilityFilter filter) {
 592 |     return switch (filter) {
 593 |       case STABLE_ONLY -> processStableVersionCheck(dep);
 594 |       case ALL, PREFER_STABLE -> processComprehensiveVersionCheck(dep);
 595 |     };
 596 |   }
 597 | 
 598 |   private BulkCheckResult processStableVersionCheck(String dep) {
 599 |     try {
 600 |       MavenCoordinate coordinate = MavenCoordinateParser.parse(dep);
 601 |       List<String> allVersions = mavenCentralService.getAllVersions(coordinate);
 602 | 
 603 |       if (allVersions.isEmpty()) {
 604 |         return BulkCheckResult.notFound(coordinate.toCoordinateString());
 605 |       }
 606 | 
 607 |       List<String> stableVersions =
 608 |           allVersions.stream().filter(versionComparator::isStableVersion).toList();
 609 | 
 610 |       String latestStable = stableVersions.isEmpty() ? null : stableVersions.get(0);
 611 | 
 612 |       return latestStable != null
 613 |           ? BulkCheckResult.foundStable(
 614 |               coordinate.toCoordinateString(),
 615 |               latestStable,
 616 |               VersionType.STABLE.getDisplayName(),
 617 |               allVersions.size(),
 618 |               stableVersions.size())
 619 |           : BulkCheckResult.noStableVersion(coordinate.toCoordinateString(), allVersions.size());
 620 |     } catch (Exception e) {
 621 |       return BulkCheckResult.error(dep, e.getMessage());
 622 |     }
 623 |   }
 624 | 
 625 |   private VersionComparison.DependencyComparisonResult compareDependencyVersion(
 626 |       String dep, StabilityFilter stabilityFilter) {
 627 |     try {
 628 |       MavenCoordinate coordinate = MavenCoordinateParser.parse(dep);
 629 |       String currentVersion = coordinate.version();
 630 |       String latestVersion =
 631 |           stabilityFilter == StabilityFilter.STABLE_ONLY
 632 |               ? getLatestStableVersion(coordinate)
 633 |               : mavenCentralService.getLatestVersion(coordinate);
 634 | 
 635 |       if (latestVersion == null) {
 636 |         return VersionComparison.DependencyComparisonResult.notFound(
 637 |             coordinate.toCoordinateString(), currentVersion);
 638 |       }
 639 | 
 640 |       if (currentVersion == null) {
 641 |         return VersionComparison.DependencyComparisonResult.noCurrentVersion(
 642 |             coordinate.toCoordinateString());
 643 |       }
 644 | 
 645 |       String latestType = versionComparator.getVersionTypeString(latestVersion);
 646 |       String updateType = versionComparator.determineUpdateType(currentVersion, latestVersion);
 647 |       boolean updateAvailable = versionComparator.compare(currentVersion, latestVersion) < 0;
 648 | 
 649 |       // Return basic comparison result
 650 | 
 651 |       return VersionComparison.DependencyComparisonResult.success(
 652 |           coordinate.toCoordinateString(),
 653 |           currentVersion,
 654 |           latestVersion,
 655 |           latestType,
 656 |           updateType,
 657 |           updateAvailable,
 658 |           context7Properties.enabled());
 659 |     } catch (Exception e) {
 660 |       return VersionComparison.DependencyComparisonResult.error(dep, e.getMessage());
 661 |     }
 662 |   }
 663 | 
 664 |   private BulkCheckResult processComprehensiveVersionCheck(String dep) {
 665 |     try {
 666 |       MavenCoordinate coordinate = MavenCoordinateParser.parse(dep);
 667 |       List<String> allVersions = mavenCentralService.getAllVersions(coordinate);
 668 |       if (allVersions.isEmpty()) {
 669 |         return BulkCheckResult.notFound(dep);
 670 |       }
 671 | 
 672 |       Map<VersionType, String> versionsByType = buildVersionsByType(allVersions);
 673 |       VersionInfoCollection versionInfos = createVersionInfoCollection(versionsByType);
 674 |       int stableCount = countStableVersions(allVersions);
 675 | 
 676 |       String primaryVersion =
 677 |           versionInfos.latestStable() != null
 678 |               ? versionInfos.latestStable().version()
 679 |               : allVersions.get(0);
 680 |       String primaryType =
 681 |           versionInfos.latestStable() != null
 682 |               ? VersionType.STABLE.getDisplayName()
 683 |               : versionComparator.getVersionTypeString(allVersions.get(0));
 684 | 
 685 |       return BulkCheckResult.foundComprehensive(
 686 |           dep,
 687 |           primaryVersion,
 688 |           primaryType,
 689 |           allVersions.size(),
 690 |           stableCount,
 691 |           versionInfos.latestStable(),
 692 |           versionInfos.latestRc(),
 693 |           versionInfos.latestBeta(),
 694 |           versionInfos.latestAlpha(),
 695 |           versionInfos.latestMilestone());
 696 |     } catch (Exception e) {
 697 |       logger.error("Error processing comprehensive version check for {}: {}", dep, e.getMessage());
 698 |       return BulkCheckResult.error(dep, e.getMessage());
 699 |     }
 700 |   }
 701 | 
 702 |   private Map<VersionType, String> buildVersionsByType(List<String> allVersions) {
 703 |     Map<VersionType, String> versionsByType = HashMap.newHashMap(5);
 704 |     Set<VersionType> remainingTypes = EnumSet.allOf(VersionType.class);
 705 | 
 706 |     for (String version : allVersions) {
 707 |       VersionType type = versionComparator.getVersionType(version);
 708 |       if (remainingTypes.contains(type)) {
 709 |         versionsByType.putIfAbsent(type, version);
 710 |         remainingTypes.remove(type);
 711 |         if (remainingTypes.isEmpty()) break;
 712 |       }
 713 |     }
 714 |     return versionsByType;
 715 |   }
 716 | 
 717 |   private VersionInfoCollection createVersionInfoCollection(
 718 |       Map<VersionType, String> versionsByType) {
 719 |     return new VersionInfoCollection(
 720 |         createVersionInfo(versionsByType, VersionType.STABLE),
 721 |         createVersionInfo(versionsByType, VersionType.RC),
 722 |         createVersionInfo(versionsByType, VersionType.BETA),
 723 |         createVersionInfo(versionsByType, VersionType.ALPHA),
 724 |         createVersionInfo(versionsByType, VersionType.MILESTONE));
 725 |   }
 726 | 
 727 |   private VersionInfo createVersionInfo(Map<VersionType, String> versionsByType, VersionType type) {
 728 |     return versionsByType.containsKey(type)
 729 |         ? new VersionInfo(versionsByType.get(type), type)
 730 |         : null;
 731 |   }
 732 | 
 733 |   private int countStableVersions(List<String> allVersions) {
 734 |     return allVersions.stream()
 735 |         .mapToInt(v -> versionComparator.getVersionType(v) == VersionType.STABLE ? 1 : 0)
 736 |         .sum();
 737 |   }
 738 | 
 739 |   private record VersionInfoCollection(
 740 |       VersionInfo latestStable,
 741 |       VersionInfo latestRc,
 742 |       VersionInfo latestBeta,
 743 |       VersionInfo latestAlpha,
 744 |       VersionInfo latestMilestone) {}
 745 | 
 746 |   private VersionComparison.UpdateSummary calculateUpdateSummary(
 747 |       List<VersionComparison.DependencyComparisonResult> results) {
 748 | 
 749 |     Map<String, Long> counts =
 750 |         results.stream()
 751 |             .filter(result -> SUCCESS_STATUS.equals(result.status()))
 752 |             .collect(
 753 |                 Collectors.groupingBy(
 754 |                     VersionComparison.DependencyComparisonResult::updateType,
 755 |                     Collectors.counting()));
 756 | 
 757 |     return new VersionComparison.UpdateSummary(
 758 |         counts.getOrDefault("major", 0L).intValue(),
 759 |         counts.getOrDefault("minor", 0L).intValue(),
 760 |         counts.getOrDefault("patch", 0L).intValue(),
 761 |         counts.getOrDefault("none", 0L).intValue());
 762 |   }
 763 | 
 764 |   private String getLatestStableVersion(MavenCoordinate coordinate) throws MavenCentralException {
 765 |     List<String> allVersions = mavenCentralService.getAllVersions(coordinate);
 766 |     List<String> stableVersions =
 767 |         allVersions.stream().filter(versionComparator::isStableVersion).toList();
 768 |     return stableVersions.isEmpty() ? null : stableVersions.get(0);
 769 |   }
 770 | 
 771 |   private ReleasePatternAnalysis analyzeReleasePattern(
 772 |       String dependency, List<MavenArtifact> allVersions, int analysisMonths) {
 773 | 
 774 |     Instant now = Instant.now();
 775 |     Instant cutoffDate = now.minus((long) analysisMonths * DAYS_IN_MONTH, ChronoUnit.DAYS);
 776 | 
 777 |     // Filter versions within analysis period
 778 |     List<MavenArtifact> analysisVersions =
 779 |         allVersions.stream()
 780 |             .filter(
 781 |                 v -> {
 782 |                   Instant releaseDate = Instant.ofEpochMilli(v.timestamp());
 783 |                   return releaseDate.isAfter(cutoffDate);
 784 |                 })
 785 |             .toList();
 786 | 
 787 |     if (analysisVersions.isEmpty()) {
 788 |       // Fallback to all versions if none in analysis period
 789 |       analysisVersions = allVersions.stream().limit(RECENT_VERSIONS_LIMIT).toList();
 790 |     }
 791 | 
 792 |     // Calculate release intervals
 793 |     List<Long> intervals = new ArrayList<>();
 794 |     for (int i = 1; i < analysisVersions.size(); i++) {
 795 |       long prevTimestamp = analysisVersions.get(i).timestamp();
 796 |       long currentTimestamp = analysisVersions.get(i - 1).timestamp();
 797 |       long intervalDays = (currentTimestamp - prevTimestamp) / MILLISECONDS_TO_DAYS;
 798 |       if (intervalDays > 0) intervals.add(intervalDays);
 799 |     }
 800 | 
 801 |     // Calculate statistics in single pass
 802 |     double averageDays;
 803 |     long maxInterval;
 804 |     long minInterval;
 805 |     if (intervals.isEmpty()) {
 806 |       averageDays = 0;
 807 |       maxInterval = 0;
 808 |       minInterval = 0;
 809 |     } else {
 810 |       var stats = intervals.stream().mapToLong(Long::longValue).summaryStatistics();
 811 |       averageDays = stats.getAverage();
 812 |       maxInterval = stats.getMax();
 813 |       minInterval = stats.getMin();
 814 |     }
 815 |     double releaseVelocity = averageDays > 0 ? (DAYS_IN_MONTH / averageDays) : 0;
 816 | 
 817 |     Instant lastReleaseDate = Instant.ofEpochMilli(analysisVersions.get(0).timestamp());
 818 |     long daysSinceLastRelease = Duration.between(lastReleaseDate, now).toDays();
 819 | 
 820 |     // Classifications
 821 |     ReleasePatternAnalysis.MaintenanceLevel maintenanceLevel =
 822 |         ReleasePatternAnalysis.MaintenanceLevel.classify(releaseVelocity, daysSinceLastRelease);
 823 |     ReleasePatternAnalysis.ReleaseConsistency consistency =
 824 |         ReleasePatternAnalysis.ReleaseConsistency.classify(averageDays, maxInterval, minInterval);
 825 | 
 826 |     // Build recent releases info
 827 |     List<ReleasePatternAnalysis.ReleaseInfo> recentReleases =
 828 |         analysisVersions.stream()
 829 |             .limit(10)
 830 |             .map(
 831 |                 v -> {
 832 |                   Instant releaseDate = Instant.ofEpochMilli(v.timestamp());
 833 |                   return new ReleasePatternAnalysis.ReleaseInfo(v.version(), releaseDate, null);
 834 |                 })
 835 |             .toList();
 836 | 
 837 |     String nextReleasePrediction =
 838 |         ReleasePatternAnalysis.predictNextRelease(averageDays, daysSinceLastRelease, consistency);
 839 |     String recommendation = ReleasePatternAnalysis.generateRecommendation(maintenanceLevel);
 840 | 
 841 |     return new ReleasePatternAnalysis(
 842 |         dependency,
 843 |         analysisVersions.size(),
 844 |         analysisMonths,
 845 |         averageDays,
 846 |         releaseVelocity,
 847 |         maintenanceLevel,
 848 |         consistency,
 849 |         lastReleaseDate,
 850 |         nextReleasePrediction,
 851 |         recentReleases,
 852 |         recommendation);
 853 |   }
 854 | 
 855 |   private VersionTimelineAnalysis analyzeVersionTimeline(
 856 |       String dependency, List<MavenArtifact> versions) {
 857 | 
 858 |     Instant now = Instant.now();
 859 | 
 860 |     // Pre-calculate all intervals and average - single pass optimization
 861 |     long[] intervalDays = new long[versions.size()];
 862 |     List<Long> positiveIntervals = new ArrayList<>();
 863 | 
 864 |     for (int i = 1; i < versions.size(); i++) {
 865 |       long currentTimestamp = versions.get(i - 1).timestamp();
 866 |       long prevTimestamp = versions.get(i).timestamp();
 867 |       long interval = (currentTimestamp - prevTimestamp) / MILLISECONDS_TO_DAYS;
 868 |       intervalDays[i] = interval;
 869 |       if (interval > 0) positiveIntervals.add(interval);
 870 |     }
 871 | 
 872 |     double averageInterval =
 873 |         positiveIntervals.isEmpty()
 874 |             ? 0
 875 |             : positiveIntervals.stream().mapToLong(Long::longValue).average().orElse(0);
 876 | 
 877 |     // Build timeline entries using pre-calculated intervals
 878 |     List<VersionTimelineAnalysis.TimelineEntry> timeline = new ArrayList<>();
 879 |     for (int i = 0; i < versions.size(); i++) {
 880 |       MavenArtifact version = versions.get(i);
 881 |       Instant releaseDate = Instant.ofEpochMilli(version.timestamp());
 882 | 
 883 |       String relativeTime = VersionTimelineAnalysis.formatRelativeTime(releaseDate, now);
 884 |       VersionType versionType = versionComparator.getVersionType(version.version());
 885 | 
 886 |       Long daysSincePrevious = i > 0 ? intervalDays[i] : null;
 887 |       ReleaseGap gap =
 888 |           i > 0 ? ReleaseGap.classify(intervalDays[i], averageInterval) : ReleaseGap.NORMAL;
 889 | 
 890 |       boolean isBreakingChange =
 891 |           versionComparator.determineUpdateType("0.0.0", version.version()).equals("major");
 892 | 
 893 |       timeline.add(
 894 |           new VersionTimelineAnalysis.TimelineEntry(
 895 |               version.version(),
 896 |               versionType,
 897 |               releaseDate,
 898 |               relativeTime,
 899 |               daysSincePrevious,
 900 |               isBreakingChange,
 901 |               gap));
 902 |     }
 903 | 
 904 |     // Calculate metrics
 905 |     Instant oldestDate = Instant.ofEpochMilli(versions.get(versions.size() - 1).timestamp());
 906 |     int timeSpanMonths = (int) Duration.between(oldestDate, now).toDays() / DAYS_IN_MONTH;
 907 | 
 908 |     // Count recent activity with optimized stream operations
 909 |     Instant oneMonthAgo = now.minus(DAYS_IN_MONTH, ChronoUnit.DAYS);
 910 |     Instant threeMonthsAgo = now.minus(3L * DAYS_IN_MONTH, ChronoUnit.DAYS);
 911 | 
 912 |     long releasesLastMonth =
 913 |         versions.stream()
 914 |             .filter(v -> Instant.ofEpochMilli(v.timestamp()).isAfter(oneMonthAgo))
 915 |             .count();
 916 | 
 917 |     long releasesLastQuarter =
 918 |         versions.stream()
 919 |             .filter(v -> Instant.ofEpochMilli(v.timestamp()).isAfter(threeMonthsAgo))
 920 |             .count();
 921 | 
 922 |     // Create analysis objects
 923 |     VersionTimelineAnalysis.VelocityTrend velocityTrend =
 924 |         new VersionTimelineAnalysis.VelocityTrend(
 925 |             TrendDirection.STABLE,
 926 |             "Release velocity appears stable",
 927 |             releasesLastQuarter / 3.0,
 928 |             versions.size() / Math.max(timeSpanMonths, 1.0),
 929 |             0.0);
 930 | 
 931 |     long stableCount =
 932 |         timeline.stream().mapToLong(t -> t.versionType() == VersionType.STABLE ? 1 : 0).sum();
 933 |     double stablePercentage = (double) stableCount / timeline.size() * 100;
 934 | 
 935 |     VersionTimelineAnalysis.StabilityPattern stabilityPattern =
 936 |         new VersionTimelineAnalysis.StabilityPattern(
 937 |             stablePercentage,
 938 |             "Mix of stable and pre-release versions",
 939 |             "Regular stable releases",
 940 |             stablePercentage > 70
 941 |                 ? "Good stability pattern - safe for production use"
 942 |                 : "Consider waiting for stable releases");
 943 | 
 944 |     long lastReleaseAge = Duration.between(timeline.get(0).releaseDate(), now).toDays();
 945 | 
 946 |     VersionTimelineAnalysis.RecentActivity recentActivity =
 947 |         new VersionTimelineAnalysis.RecentActivity(
 948 |             (int) releasesLastMonth,
 949 |             (int) releasesLastQuarter,
 950 |             ActivityLevel.classify((int) releasesLastMonth, (int) releasesLastQuarter),
 951 |             lastReleaseAge,
 952 |             "Recent activity: " + releasesLastQuarter + " releases in last quarter");
 953 | 
 954 |     List<String> insights = generateTimelineInsights(timeline, recentActivity, stabilityPattern);
 955 | 
 956 |     return new VersionTimelineAnalysis(
 957 |         dependency,
 958 |         versions.size(),
 959 |         timeline.size(),
 960 |         timeSpanMonths,
 961 |         timeline,
 962 |         velocityTrend,
 963 |         stabilityPattern,
 964 |         recentActivity,
 965 |         insights);
 966 |   }
 967 | 
 968 |   private List<String> generateTimelineInsights(
 969 |       List<VersionTimelineAnalysis.TimelineEntry> timeline,
 970 |       VersionTimelineAnalysis.RecentActivity recentActivity,
 971 |       VersionTimelineAnalysis.StabilityPattern stabilityPattern) {
 972 | 
 973 |     List<String> insights = new ArrayList<>();
 974 | 
 975 |     if (recentActivity.activityLevel() == ActivityLevel.VERY_ACTIVE) {
 976 |       insights.add("High release frequency indicates active development");
 977 |     } else if (recentActivity.activityLevel() == ActivityLevel.DORMANT) {
 978 |       insights.add("No recent releases - consider checking project status");
 979 |     }
 980 | 
 981 |     if (stabilityPattern.stablePercentage() > VERY_HIGH_STABILITY_THRESHOLD) {
 982 |       insights.add("Strong preference for stable releases - good for production");
 983 |     } else if (stabilityPattern.stablePercentage() < LOW_STABILITY_THRESHOLD) {
 984 |       insights.add("Many pre-release versions - early-stage or experimental project");
 985 |     }
 986 | 
 987 |     long majorGaps =
 988 |         timeline.stream().mapToLong(t -> t.releaseGap() == ReleaseGap.MAJOR_GAP ? 1 : 0).sum();
 989 |     if (majorGaps > 0) {
 990 |       insights.add("Found " + majorGaps + " significant gaps in release schedule");
 991 |     }
 992 | 
 993 |     return insights;
 994 |   }
 995 | 
 996 |   private ProjectHealthAnalysis.DependencyHealthAnalysis analyzeSimpleDependencyHealth(
 997 |       String dependency, Integer maxAgeInDays) {
 998 |     try {
 999 |       MavenCoordinate coordinate = MavenCoordinateParser.parse(dependency);
1000 |       List<MavenArtifact> versions =
1001 |           mavenCentralService.getRecentVersionsWithAccurateTimestamps(coordinate, 10);
1002 | 
1003 |       if (versions.isEmpty()) {
1004 |         return ProjectHealthAnalysis.DependencyHealthAnalysis.notFound(dependency);
1005 |       }
1006 | 
1007 |       MavenArtifact latestVersion = versions.get(0);
1008 |       DependencyAgeAnalysis ageAnalysis =
1009 |           DependencyAgeAnalysis.fromTimestamp(
1010 |               coordinate.toCoordinateString(), latestVersion.version(), latestVersion.timestamp());
1011 | 
1012 |       // Simple maintenance assessment based on recent versions
1013 |       Instant now = Instant.now();
1014 |       Instant sixMonthsAgo = now.minus(6L * DAYS_IN_MONTH, ChronoUnit.DAYS);
1015 | 
1016 |       long recentVersions =
1017 |           versions.stream()
1018 |               .filter(v -> Instant.ofEpochMilli(v.timestamp()).isAfter(sixMonthsAgo))
1019 |               .count();
1020 | 
1021 |       String maintenanceStatus;
1022 |       if (recentVersions >= 3) {
1023 |         maintenanceStatus = ACTIVE_MAINTENANCE;
1024 |       } else if (recentVersions >= 1) {
1025 |         maintenanceStatus = "moderate";
1026 |       } else {
1027 |         maintenanceStatus = "slow";
1028 |       }
1029 | 
1030 |       // Health score (0-100)
1031 |       int healthScore = calculateSimpleHealthScore(ageAnalysis, maintenanceStatus, maxAgeInDays);
1032 | 
1033 |       // No upgrade strategy in basic analysis
1034 | 
1035 |       return ProjectHealthAnalysis.DependencyHealthAnalysis.success(
1036 |           dependency,
1037 |           latestVersion.version(),
1038 |           ageAnalysis.ageClassification().getName(),
1039 |           ageAnalysis.daysSinceLastRelease(),
1040 |           healthScore,
1041 |           maintenanceStatus,
1042 |           context7Properties.enabled());
1043 |     } catch (Exception e) {
1044 |       return ProjectHealthAnalysis.DependencyHealthAnalysis.error(dependency, e.getMessage());
1045 |     }
1046 |   }
1047 | 
1048 |   private int calculateSimpleHealthScore(
1049 |       DependencyAgeAnalysis ageAnalysis, String maintenanceStatus, Integer maxAgeInDays) {
1050 | 
1051 |     int score = PERFECT_HEALTH_SCORE;
1052 | 
1053 |     // Age penalty
1054 |     switch (ageAnalysis.ageClassification()) {
1055 |       case FRESH -> score -= 0; // No penalty
1056 |       case CURRENT -> score -= CURRENT_VERSION_PENALTY;
1057 |       case AGING -> score -= AGING_VERSION_PENALTY;
1058 |       case STALE -> score -= STALE_VERSION_PENALTY;
1059 |     }
1060 | 
1061 |     // Maintenance penalty
1062 |     switch (maintenanceStatus) {
1063 |       case ACTIVE_MAINTENANCE -> score -= 0; // No penalty
1064 |       case "moderate" -> score -= MODERATE_MAINTENANCE_PENALTY;
1065 |       case "slow" -> score -= SLOW_MAINTENANCE_PENALTY;
1066 |       default -> {
1067 |         // Unknown maintenance status - no penalty
1068 |       }
1069 |     }
1070 | 
1071 |     // Custom age threshold penalty
1072 |     if (maxAgeInDays != null && ageAnalysis.daysSinceLastRelease() > maxAgeInDays) {
1073 |       score -= AGE_THRESHOLD_PENALTY;
1074 |     }
1075 | 
1076 |     return Math.max(0, score);
1077 |   }
1078 | 
1079 |   private ProjectHealthAnalysis buildSimpleHealthSummary(
1080 |       List<ProjectHealthAnalysis.DependencyHealthAnalysis> dependencyAnalyses,
1081 |       Integer maxAgeInDays) {
1082 | 
1083 |     int totalDependencies = dependencyAnalyses.size();
1084 |     int successfulAnalyses =
1085 |         (int)
1086 |             dependencyAnalyses.stream()
1087 |                 .mapToLong(dep -> SUCCESS_STATUS.equals(dep.status()) ? 1 : 0)
1088 |                 .sum();
1089 | 
1090 |     // Calculate averages and counts in single pass
1091 |     List<ProjectHealthAnalysis.DependencyHealthAnalysis> successfulDeps =
1092 |         dependencyAnalyses.stream().filter(dep -> SUCCESS_STATUS.equals(dep.status())).toList();
1093 | 
1094 |     if (successfulDeps.isEmpty()) {
1095 |       return new ProjectHealthAnalysis(
1096 |           "unknown",
1097 |           0,
1098 |           totalDependencies,
1099 |           0,
1100 |           new ProjectHealthAnalysis.AgeDistribution(0, 0, 0, 0),
1101 |           dependencyAnalyses,
1102 |           List.of("Unable to analyze any dependencies"));
1103 |     }
1104 | 
1105 |     // Single pass through successful dependencies
1106 |     DependencyMetrics metrics = calculateDependencyMetrics(successfulDeps);
1107 | 
1108 |     double averageHealthScore = metrics.totalHealthScore() / (double) successfulDeps.size();
1109 |     String overallHealth = determineOverallHealth(averageHealthScore);
1110 | 
1111 |     // Create age distribution
1112 |     ProjectHealthAnalysis.AgeDistribution ageDistribution =
1113 |         new ProjectHealthAnalysis.AgeDistribution(
1114 |             (int) metrics.freshCount(),
1115 |             (int) metrics.currentCount(),
1116 |             (int) metrics.agingCount(),
1117 |             (int) metrics.staleCount());
1118 | 
1119 |     // Generate recommendations
1120 |     List<String> recommendations =
1121 |         generateHealthRecommendations(metrics, successfulAnalyses, successfulDeps, maxAgeInDays);
1122 | 
1123 |     return new ProjectHealthAnalysis(
1124 |         overallHealth,
1125 |         (int) Math.round(averageHealthScore),
1126 |         totalDependencies,
1127 |         successfulAnalyses,
1128 |         ageDistribution,
1129 |         dependencyAnalyses,
1130 |         recommendations);
1131 |   }
1132 | 
1133 |   private DependencyMetrics calculateDependencyMetrics(
1134 |       List<ProjectHealthAnalysis.DependencyHealthAnalysis> successfulDeps) {
1135 |     // Calculate all metrics in optimized stream operations
1136 |     int totalHealthScore =
1137 |         successfulDeps.stream()
1138 |             .mapToInt(ProjectHealthAnalysis.DependencyHealthAnalysis::healthScore)
1139 |             .sum();
1140 | 
1141 |     Map<String, Long> ageCounts =
1142 |         successfulDeps.stream()
1143 |             .collect(
1144 |                 Collectors.groupingBy(
1145 |                     ProjectHealthAnalysis.DependencyHealthAnalysis::ageClassification,
1146 |                     Collectors.counting()));
1147 | 
1148 |     long activeMaintenanceCount =
1149 |         successfulDeps.stream()
1150 |             .filter(dep -> ACTIVE_MAINTENANCE.equals(dep.maintenanceLevel()))
1151 |             .count();
1152 | 
1153 |     return new DependencyMetrics(
1154 |         totalHealthScore,
1155 |         ageCounts.getOrDefault(FRESH_AGE, 0L),
1156 |         ageCounts.getOrDefault(CURRENT_AGE, 0L),
1157 |         ageCounts.getOrDefault(AGING_AGE, 0L),
1158 |         ageCounts.getOrDefault(STALE_AGE, 0L),
1159 |         activeMaintenanceCount);
1160 |   }
1161 | 
1162 |   private String determineOverallHealth(double averageHealthScore) {
1163 |     if (averageHealthScore >= EXCELLENT_HEALTH_THRESHOLD) {
1164 |       return EXCELLENT_HEALTH;
1165 |     } else if (averageHealthScore >= GOOD_HEALTH_THRESHOLD) {
1166 |       return GOOD_HEALTH;
1167 |     } else if (averageHealthScore >= FAIR_HEALTH_THRESHOLD) {
1168 |       return FAIR_HEALTH;
1169 |     } else {
1170 |       return POOR_HEALTH;
1171 |     }
1172 |   }
1173 | 
1174 |   private List<String> generateHealthRecommendations(
1175 |       DependencyMetrics metrics,
1176 |       int successfulAnalyses,
1177 |       List<ProjectHealthAnalysis.DependencyHealthAnalysis> successfulDeps,
1178 |       Integer maxAgeInDays) {
1179 |     List<String> recommendations = new ArrayList<>();
1180 | 
1181 |     if (metrics.staleCount() > 0) {
1182 |       recommendations.add(
1183 |           "Review " + metrics.staleCount() + " stale dependencies for alternatives");
1184 |     }
1185 |     if (metrics.agingCount() > successfulAnalyses / 2) {
1186 |       recommendations.add("Consider updating aging dependencies");
1187 |     }
1188 |     if (metrics.activeMaintenanceCount() < successfulAnalyses / 2) {
1189 |       recommendations.add("Monitor maintenance activity for slower-updated dependencies");
1190 |     }
1191 | 
1192 |     // Add age-specific recommendations when custom threshold is set
1193 |     if (maxAgeInDays != null) {
1194 |       long exceedsThreshold =
1195 |           successfulDeps.stream()
1196 |               .filter(
1197 |                   dep ->
1198 |                       STALE_AGE.equals(dep.ageClassification())
1199 |                           || AGING_AGE.equals(dep.ageClassification()))
1200 |               .count();
1201 |       if (exceedsThreshold > 0) {
1202 |         recommendations.add(
1203 |             "Found "
1204 |                 + exceedsThreshold
1205 |                 + " dependencies exceeding your "
1206 |                 + maxAgeInDays
1207 |                 + "-day age threshold");
1208 |       }
1209 |     }
1210 | 
1211 |     return recommendations;
1212 |   }
1213 | 
1214 |   private record DependencyMetrics(
1215 |       int totalHealthScore,
1216 |       long freshCount,
1217 |       long currentCount,
1218 |       long agingCount,
1219 |       long staleCount,
1220 |       long activeMaintenanceCount) {}
1221 | }
1222 | 
```
Page 2/2FirstPrevNextLast