#
tokens: 47420/50000 56/60 files (page 1/2)
lines: off (toggle) GitHub
raw markdown copy
This is page 1 of 2. Use http://codebase.md/arvindand/maven-tools-mcp?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

--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------

```
/mvnw text eol=lf
*.cmd text eol=crlf

```

--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------

```
# Maven Tools MCP Server - Buildpack Optimizations
# Exclude unnecessary files from Docker build context

# Build artifacts
target/
*.jar

# IDE files
.idea/
.vscode/
*.iml

# OS files
.DS_Store
Thumbs.db

# Git and documentation (not needed in runtime)
.git/
*.md
CHANGELOG.md
CLAUDE.md

# Logs and temporary files
logs/
*.log
*.tmp
*.bak

```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
HELP.md
CLAUDE.md
demo
target/
.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/

### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
logs

### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr

### VS Code ###
.vscode/

# Environment variables file
.env

.claude

# Internal planning documents
improvement.md
.mcp.json

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# Maven Tools MCP Server

[![Java](https://img.shields.io/badge/Java-24-orange.svg)](https://openjdk.java.net/)
[![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.5.6-green.svg)](https://spring.io/projects/spring-boot)
[![MCP Protocol](https://img.shields.io/badge/MCP-2024--11--05-blue.svg)](https://modelcontextprotocol.io/)
[![License](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/arvindand/maven-tools-mcp)](https://github.com/arvindand/maven-tools-mcp/releases)
[![Docker](https://img.shields.io/badge/Docker-Multi--Arch-blue.svg)](https://hub.docker.com/r/arvindand/maven-tools-mcp)
[![GitHub stars](https://img.shields.io/github/stars/arvindand/maven-tools-mcp?style=social)](https://github.com/arvindand/maven-tools-mcp/stargazers)

## Universal Maven Central dependency intelligence for JVM build tools

MCP server providing AI assistants with Maven Central dependency intelligence for all JVM build tools (Maven, Gradle, SBT, Mill). Get instant, accurate dependency information by reading maven-metadata.xml files directly from Maven Central repository - faster and more reliable than web searches or search APIs. Features Context7 integration for documentation support.

## 🎯 Why This Matters

- **Problem:** Dependency management involves time-intensive manual searches across Maven Central for version updates and compatibility analysis
- **Solution:** AI-assisted dependency intelligence with instant bulk analysis, trend insights, and risk assessment for any JVM build tool

## ⚡ Quick Demo

![Demo GIF](assets/demo.gif)

Ask your AI assistant:

- *"Check all dependencies in this build file for latest versions"* (paste your build.gradle, pom.xml, build.sbt)
- *"What's the latest Spring Boot version?"*
- *"Which dependencies in my project need updates?"* (any build tool)
- *"Show me only stable versions for production deployment"*
- *"How old are my dependencies and which ones need attention?"*
- *"Analyze the release patterns for my key dependencies"*
- *"Give me a health check for all my project dependencies"*
- *"How do I upgrade Spring Boot from 2.7.0 to the latest version? Show me migration guidance"*
- *"Check these dependencies for upgrades and suggest documentation searches"* (paste your pom.xml/build.gradle)
- *"I'm still using Jackson 2.12.0. Should I upgrade and how?"*

## 🔧 Supported Build Tools

Working with **any build tool** that uses Maven Central Repository:

| Build Tool | Dependency Format | Example Usage |
|------------|------------------|---------------|
| **Maven** | `groupId:artifactId:version` | `org.springframework:spring-core:6.2.8` |
| **Gradle** | `implementation("group:artifact:version")` | Uses same Maven coordinates |
| **SBT** | `libraryDependencies += "group" % "artifact" % "version"` | Same groupId:artifactId format |
| **Mill** | `ivy"group:artifact:version"` | Same Maven Central lookup |

**All tools use standard Maven coordinates** - just provide `groupId:artifactId` and we handle the rest.

## ⚡ Advantages

### vs Simple Lookup Tools

- ✅ **Bulk Operations** - Analyze 20+ dependencies in one call
- ✅ **Version Comparison** - Understand upgrade impact (major/minor/patch)
- ✅ **Stability Filtering** - Choose stable-only or include pre-release versions
- ✅ **Enterprise Performance** - <100ms cached responses, native images
- ✅ **Analytical Intelligence** - Age analysis, release patterns, project health scoring
- ✅ **Context7 Orchestration** - Clear tool orchestration instructions with web search fallback

### vs Manual Dependency Management

- ✅ **Risk Assessment** - Identify breaking changes before upgrading
- ✅ **Universal Support** - Works with any JVM build tool
- ✅ **Complete Analysis** - All version types with intelligent prioritization
- ✅ **Maintenance Intelligence** - Predict maintenance activity and sustainability

## Setup for Claude Desktop

**Step 1:** Locate your Claude Desktop configuration file

- **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
- **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
- **Linux:** `~/.config/Claude/claude_desktop_config.json`

**Step 2:** Add this configuration (using pre-built Docker image):

```json
{
  "mcpServers": {
    "maven-tools": {
      "command": "docker",
      "args": [
        "run", "-i", "--rm",
        "arvindand/maven-tools-mcp:latest"
      ]
    }
  }
}
```

**Step 3:** Restart Claude Desktop

**Prerequisites:** Docker installed and running

**Note:** The Docker image supports both AMD64 (Intel/AMD) and ARM64 (Apple Silicon) architectures. Docker automatically selects the correct version for your platform.

**Troubleshooting:** Context7 integration is enabled by default and contacts `https://mcp.context7.com` during startup. If your network blocks this URL the server prints a Spring stack trace to `stdout`, which causes the MCP handshake to fail. Use the Context7-free native image instead: `arvindand/maven-tools-mcp:latest-noc7`. (Environment-variable toggles only work when running the JVM jar directly.)

**Corporate Networks with SSL Inspection:** If you need Context7 integration but your network uses SSL inspection (MITM proxies), you can build a custom image with your corporate certificates. See the [Corporate Certificate Guide](CORPORATE-CERTIFICATES.md) for detailed instructions.

## Setup for VS Code with GitHub Copilot

**Option 1: Workspace Configuration** - Create `.vscode/mcp.json`:

```json
{
  "servers": {
    "maven-tools": {
      "type": "stdio",
      "command": "docker",
      "args": ["run", "-i", "--rm", "arvindand/maven-tools-mcp:latest"]
    }
  }
}
```

**Option 2: User Settings** - Add to your VS Code settings:

```json
{
  "mcp": {
    "servers": {
      "maven-tools": {
        "type": "stdio", 
        "command": "docker",
        "args": ["run", "-i", "--rm", "arvindand/maven-tools-mcp:latest"]
      }
    }
  }
}
```

**Usage:** Open Chat view (Ctrl+Alt+I), select Agent mode, then use the Tools button to enable Maven tools.

## What it does

**Core Dependency Intelligence:**

- Get latest or stable versions of Maven dependencies
- Check if specific versions exist
- Bulk version checking for multiple dependencies
- Compare versions and get update recommendations

**Advanced Analytics:**

- Analyze dependency age and freshness (fresh/current/aging/stale)
- Assess maintenance activity and release patterns
- Predict next release timeframes
- Comprehensive project health scoring with risk assessment

## Available Tools

### Core Maven Intelligence Tools (8 tools)

| Tool | Purpose | Key Features |
|------|---------|--------------|
| `get_latest_version` | Get newest version by type with stability preferences | preferStable parameter, all version types |
| `check_version_exists` | Verify if specific version exists with type info | Works with any JVM build tool |
| `check_multiple_dependencies` | Check multiple dependencies with filtering | stableOnly parameter, bulk operations |
| `compare_dependency_versions` | Compare current vs latest with upgrade recommendations | includeMigrationGuidance flag |
| `analyze_dependency_age` | Classify dependencies as fresh/current/aging/stale | includeModernizationGuidance flag |
| `analyze_release_patterns` | Analyze maintenance activity and predict releases | monthsToAnalyze parameter, velocity trends |
| `get_version_timeline` | Enhanced version timeline with temporal analysis | versionCount parameter, release gap detection |
| `analyze_project_health` | Comprehensive health analysis for multiple dependencies | includeUpgradeStrategy flag |

### Raw Context7 Documentation Tools (2 tools - Enabled by Default)

| Tool | Purpose | Key Features |
|------|---------|--------------|
| `resolve-library-id` | Search for library documentation | Always available (context7.enabled=true by default) |
| `get-library-docs` | Get library documentation by ID | Always available (context7.enabled=true by default) |

### Tool Parameters

**Core Parameters:**

- `preferStable` - Prioritize stable versions in analysis
- `stableOnly` - Filter to production-ready versions only
- `onlyStableTargets` - Only suggest upgrades to stable versions

**Analytical Parameters:**

- `maxAgeInDays` - Set acceptable age threshold for dependencies
- `monthsToAnalyze` - Specify analysis period for release patterns (default: 24)
- `versionCount` - Number of recent versions to analyze in timeline (default: 20)
- `includeRecommendations` - Include detailed recommendations in health analysis

**Context7 Integration:**

Context7 integration is **enabled by default** (`context7.enabled=true`). Maven tools automatically include explicit orchestration instructions in response models when upgrades or modernization are needed. Additionally, the server acts as an MCP client to expose raw Context7 tools (`resolve-library-id`, `get-library-docs`) directly to your AI assistant. When disabled, responses contain only core dependency analysis without orchestration instructions or Context7 tools.

**Universal Compatibility:**
All tools work with standard Maven coordinates (`groupId:artifactId`) and support any JVM build tool.

### `get_latest_version`

Get latest version of any dependency from Maven Central (works with Maven, Gradle, SBT, Mill) with stability preferences.

**Parameters:**

- `dependency` (string, required): Maven coordinate in format `groupId:artifactId` (NO version)
- `preferStable` (boolean, optional): When true, prioritizes stable version in response (default: false)

**Examples:**

```json
{
  "dependency": "org.springframework:spring-core",
  "preferStable": false
}
```

```json
{
  "dependency": "org.springframework:spring-boot",
  "preferStable": true
}
```

**Response:**

```json
{
  "dependency": "org.springframework:spring-core",
  "latest_stable": { "version": "6.2.7", "type": "stable" },
  "latest_rc": { "version": "7.0.0-RC1", "type": "rc" },
  "latest_beta": { "version": "7.0.0-beta1", "type": "beta" },
  "latest_alpha": { "version": "7.0.0-alpha1", "type": "alpha" },
  "latest_milestone": { "version": "7.0.0-M5", "type": "milestone" },
  "total_versions": 100
}
```

### `check_version_exists`

Check if specific dependency version exists and identify its stability type. Works with any JVM build tool.

**Parameters:**

- `dependency` (string, required): Maven coordinate in format `groupId:artifactId` (NO version)
- `version` (string, required): Version to check

**Example:**

```json
{
  "dependency": "org.jetbrains.kotlin:kotlin-stdlib",
  "version": "1.9.0"
}
```

**Response:**

```json
{
  "exists": true,
  "version": "6.0.0",
  "type": "stable"
}
```

### `check_multiple_dependencies`

Check latest versions for multiple dependencies with filtering options. Works with any JVM build tool.

**Parameters:**

- `dependencies` (string, required): Comma- or newline-separated list of Maven coordinates (NO versions)
- `stableOnly` (boolean, optional): When true, filters to production-ready versions only (default: false)

**Examples:**

```json
{
  "dependencies": "org.jetbrains.kotlin:kotlin-stdlib,com.squareup.retrofit2:retrofit,org.apache.spark:spark-core_2.13",
  "stableOnly": false
}
```

```json
{
  "dependencies": "org.springframework:spring-boot,com.fasterxml.jackson.core:jackson-core",
  "stableOnly": true
}
```

**Response (array):**

```json
[
  {
    "dependency": "org.springframework:spring-core",
    "primary_version": "6.2.7",
    "primary_type": "stable",
    "total_versions": 100,
    "stable_versions": 82,
    "latest_stable": { "version": "6.2.7", "type": "stable" },
    "latest_rc": { "version": "7.0.0-RC1", "type": "rc" },
    "latest_beta": null,
    "latest_alpha": null,
    "latest_milestone": { "version": "7.0.0-M5", "type": "milestone" }
  },
  // ...more results
]
```

### `compare_dependency_versions`

Compare current dependency versions with latest available and show upgrade recommendations with safety controls.

**Parameters:**

- `currentDependencies` (string, required): Comma- or newline-separated list of Maven coordinates with versions (`groupId:artifactId:version`)
- `onlyStableTargets` (boolean, optional): When true, only suggests upgrades to stable versions (default: false)

**Examples:**

```json
{
  "currentDependencies": "org.jetbrains.kotlin:kotlin-stdlib:1.8.0,com.squareup.retrofit2:retrofit:2.9.0",
  "onlyStableTargets": false
}
```

```json
{
  "currentDependencies": "org.springframework:spring-boot:2.7.0,org.hibernate:hibernate-core:5.6.0",
  "onlyStableTargets": true
}
```

**Response:**

```json
{
  "comparison_date": "2025-06-07T22:38:47Z",
  "dependencies": [
    {
      "dependency": "org.springframework:spring-core:6.0.0",
      "current_version": "6.0.0",
      "latest_version": "7.0.0-M5",
      "latest_type": "milestone",
      "update_type": "major",
      "update_available": true,
      "status": "success",
      "error": null
    }
  ],
  "update_summary": {
    "major_updates": 1,
    "minor_updates": 0,
    "patch_updates": 0,
    "no_updates": 0
  }
}
```

### Raw Context7 MCP Tools

**Note:** Context7 integration is enabled by default. The following raw Context7 MCP tools are automatically available through the server's dual MCP architecture (acting as both MCP server and MCP client):

### `resolve-library-id`

Search for library documentation using intelligent name resolution.

**Parameters:**

- `libraryName` (string, required): Search term for library lookup (e.g., "spring boot", "testcontainers")

**Example:**

```json
{
  "libraryName": "testcontainers postgresql"
}
```

### `get-library-docs`

Get comprehensive documentation for a library using its Context7 ID.

**Parameters:**

- `context7CompatibleLibraryID` (string, required): Context7-compatible library ID (from resolve-library-id)
- `topic` (string, optional): Topic for focused documentation (e.g., "setup", "migration", "configuration")
- `tokens` (integer, optional): Maximum tokens to retrieve (default: 10000)

**Example:**

```json
{
  "context7CompatibleLibraryID": "/testcontainers/testcontainers-java",
  "topic": "postgresql setup",
  "tokens": 5000
}
```

These tools are automatically available by default through Spring AI MCP client integration. The server acts as both an MCP server (exposing Maven tools) and an MCP client (exposing Context7 tools), providing a unified interface for both dependency analysis and documentation access.

## Usage Examples

### Getting Started Examples

**Simple Questions:**

- "Get latest Spring Boot version but prioritize stable releases"
- "Check if Kotlin 1.9.0 exists and what stability type it is"
- "Show me latest stable version of Retrofit for production deployment"

**Multi-Build Tool Support:**

- "Check these Gradle dependencies: org.jetbrains.kotlin:kotlin-stdlib,com.squareup.retrofit2:retrofit"
- "I need stable versions only for my SBT project dependencies"
- "Compare my Maven versions but only suggest stable upgrades for production"

**Advanced Stability Controls:**

- "Check multiple dependencies but filter to stable versions only"
- "Compare my current versions with onlyStableTargets=true for safety"
- "Get complete analysis but prefer stable versions in results"

## 🚀 Real-World Use Cases

### Gradle Project Analysis

**Action:** Paste your build.gradle: *"Analyze my Gradle dependencies for outdated versions"*  
**Result:** Universal dependency analysis in seconds across any build tool

### Security Response  

**Action:** *"Show me latest stable versions for these affected dependencies"*  
**Result:** Instant security patch identification with production-safe recommendations

### Multi-Build Tool Projects

**Action:** *"What are the latest stable versions for Spring Boot, Spring Security, and Jackson for both Maven and Gradle?"*  
**Result:** Universal dependency intelligence across all JVM build tools

### Migration Planning with Risk Assessment

**Action:** *"Compare my current versions but only suggest stable upgrades for production safety"*  
**Result:** Risk-assessed upgrade recommendations with stability filtering

## 🆚 Why Not Just Web Search?

| Scenario | Web Search | Maven Tools MCP |
|----------|------------|------------------|
| Single dependency lookup | 3-5 seconds | <100ms (cached) |
| 20 dependencies across build tools | 60+ seconds | <500ms |
| Data accuracy | Variable/outdated | 100% current |
| Bulk operations | Manual, error-prone | Native support |
| Version classification | Manual parsing | Automatic (stable/RC/beta) |
| Stability filtering | Not available | Built-in (stableOnly, preferStable) |
| Build tool compatibility | Tool-specific searches | Universal JVM support |

## ✨ Advanced Features Examples

### Analytical Intelligence & Documentation Enrichment

### Dependency Age Analysis

**Usage:** *"How old is my Spring Boot dependency and should I update it?"*  
**Tool:** `analyze_dependency_age`

```json
{
  "dependency": "org.springframework.boot:spring-boot-starter",
  "age_classification": "current",
  "days_since_release": 45,
  "recommendation": "Actively maintained - consider updating if needed"
}
```

### Release Pattern Analysis  

**Usage:** *"What's the maintenance pattern for Jackson? When might the next release be?"*  
**Tool:** `analyze_release_patterns`

```json
{
  "dependency": "com.fasterxml.jackson.core:jackson-core",
  "maintenance_level": "active",
  "release_velocity": 1.2,
  "next_release_prediction": "Expected in 3 weeks"
}
```

### Project Health Check

**Usage:** *"Give me a health assessment for all my key dependencies"*  
**Tool:** `analyze_project_health`

```json
{
  "overall_health": "good",
  "average_health_score": 78,
  "age_distribution": {"fresh": 2, "current": 8, "aging": 3, "stale": 1}
}
```

### Version Timeline Intelligence

**Usage:** *"Show me the recent release timeline for JUnit with gap analysis"*  
**Tool:** `get_version_timeline`

```json
{
  "insights": ["High release frequency indicates active development"],
  "recent_activity": {"activity_level": "active", "releases_last_quarter": 4}
}
```

## Features

- Version lookup (latest, stable, or specific versions)
- Version type classification (stable, RC, beta, alpha, milestone)
- Bulk operations for multiple dependencies
- Version comparison tools
- **Dependency age analysis with actionable insights**
- **Maintenance pattern analysis and predictions**
- **Project health scoring and recommendations**
- **Context7 migration guidance and upgrade strategies**
- **Documentation enrichment for complex upgrades**
- Caching for better performance
- Works with MCP-compatible AI assistants

> **Note:** Snapshot versions are not supported. This is because the Maven Central API does not index or provide access to snapshot artifacts. Only released versions (stable, rc, beta, alpha, milestone) are available.

## Context7 Guided Delegation Architecture

**Default Behavior:** Context7 integration is **enabled by default**. The server acts as both an MCP server (providing Maven tools) and an MCP client (exposing Context7 tools), giving your AI assistant access to both dependency intelligence and documentation guidance in a single connection. When disabled (`context7.enabled=false`), Maven tools work independently without Context7 guidance hints or raw Context7 tools.

### Dual MCP Architecture

Maven Tools MCP uses a **dual MCP architecture** with guided delegation for Context7 integration:

1. **MCP Server:** Provides 8 Maven dependency analysis tools with intelligent Context7 guidance hints
2. **MCP Client:** Acts as Context7 MCP client to expose raw Context7 tools (`resolve-library-id`, `get-library-docs`)
3. **Intelligent Integration:** Maven tools include smart Context7 search suggestions when upgrades/modernization are needed
4. **Direct Access:** Your AI assistant can use both Maven analysis AND Context7 documentation tools in a single connection

This dual architecture provides both dependency intelligence and documentation access through one MCP server connection, with intelligent guidance for effective Context7 tool usage.

### Context7 Tools (Enabled by Default)

Context7 tools are automatically enabled by default. To disable Context7 integration entirely, use the `-noc7` image variant:

```json
{
  "mcpServers": {
    "maven-tools": {
      "command": "docker",
      "args": [
        "run", "-i", "--rm",
        "arvindand/maven-tools-mcp:latest-noc7"
      ]
    }
  }
}
```

**Note:** Environment variable toggles (`-e CONTEXT7_ENABLED=false`) only work when running the JVM jar directly, not with native images. Native images have configuration compiled in at build time. Use the `-noc7` image variants for a pure Maven tools experience without Context7 integration.

### Image Variants

| Image Tag | Contents | When to Use |
|-----------|----------|-------------|
| `arvindand/maven-tools-mcp:latest` | Native image with Context7 tools enabled | Default experience with documentation integration |
| `arvindand/maven-tools-mcp:latest-noc7` | Native image without Context7 client/tools | Environments without outbound access to `mcp.context7.com` or where only Maven tools are desired |
| `arvindand/maven-tools-mcp:<version>` | Version-specific multi-arch image (Context7 enabled) | Pin to an exact release |
| `arvindand/maven-tools-mcp:<version>-noc7` | Version-specific image without Context7 | Pin to exact release without Context7 |

### Context7 Orchestration Instructions

**Intelligent LLM Orchestration:**

Maven Tools MCP provides explicit orchestration instructions in response models to help LLMs effectively use the raw Context7 MCP tools when documentation is needed. These clear step-by-step instructions include web search fallback for resilient documentation access.

**Context7 Orchestration Example:**

**Usage:** *"Compare my Spring Boot version and show upgrade path"*

**Tool:** `compare_dependency_versions`

```json
{
  "dependencies": [{
    "dependency": "org.springframework.boot:spring-boot-starter:2.7.0",
    "current_version": "2.7.0",
    "latest_version": "3.2.0", 
    "update_type": "major",
    "context7_guidance": {
      "orchestration_instructions": "Use resolve-library-id tool with libraryName='spring-boot-starter' to find documentation ID. Then use get-library-docs tool with the returned Context7 ID and topic='migration guide' to get upgrade instructions. If Context7 doesn't provide sufficient information, perform a web search for 'spring-boot-starter major version upgrade guide'."
    }
  }]
}
```

**Modernization Guidance Example:**

**Usage:** *"Analyze my aging dependencies with modernization suggestions"*

**Tool:** `analyze_dependency_age`

```json
{
  "dependency": "org.hibernate:hibernate-core",
  "age_classification": "AGING",
  "days_since_last_release": 180,
  "recommendation": "Consider upgrading - dependency is showing age",
  "context7_guidance": {
    "orchestration_instructions": "Use resolve-library-id tool with libraryName='hibernate-core' to find documentation ID. Then use get-library-docs tool with the returned Context7 ID and topic='modern usage and best practices' to get modernization guidance. If Context7 doesn't provide sufficient information, perform a web search for 'hibernate-core latest features best practices'."
  }
}
```

## Performance Notes

- **Cache effectiveness:** ~90% of repeated requests served from cache
- **Recommended batch sizes:** 10-20 dependencies for bulk operations
- **First requests:** Build cache (normal), subsequent requests much faster
- **Cache duration:** 24 hours

## 🤔 Frequently Asked Questions

**Q: How is this different from Dependabot/Renovate?**  
A: Those tools create automated PRs. This gives you instant, interactive dependency intelligence through your AI assistant for decision-making and planning.

**Q: How much time does this actually save?**  
A: For single dependencies: from 3-5 seconds (web search) to <100ms. For 20+ dependencies: from 60+ seconds of manual searching to <500ms bulk analysis.

**Q: Why not just search Maven Central directly?**  
A: This reads maven-metadata.xml files directly from Maven Central repository, providing structured, cached responses optimized for AI consumption with intelligent version classification and bulk operations - plus the time savings above.

**Q: Can this replace my IDE's dependency management?**  
A: No, it complements your IDE by providing instant dependency intelligence during natural conversations with AI assistants for planning and decision-making.

**Q: What AI assistants does this work with?**  
A: Any MCP-compatible assistant including Claude Desktop, GitHub Copilot, and other MCP clients. Works through natural conversation.

**Q: Does it work with private Maven repositories?**  
A: Currently only Maven Central.

**Q: What about Gradle dependencies?**  
A: Maven Central hosts both Maven and Gradle dependencies, so it works for Gradle projects too (using Maven coordinates).

**Q: What is Context7 and how does the guided delegation work?**  
A: Context7 is an MCP server by Upstash that provides up-to-date documentation and code examples. Maven Tools MCP uses a guided delegation architecture - our tools provide explicit orchestration instructions to help your AI assistant effectively use the raw Context7 tools when documentation is needed. This includes clear step-by-step tool usage instructions with web search fallback for resilient documentation access.

## Alternative Setup Methods

### Using Docker Compose

**Alternative Claude Desktop configuration** (if you prefer compose):

Download `docker-compose.yml` and configure:

```json
{
  "mcpServers": {
    "maven-tools": {
      "command": "docker",
      "args": [
        "compose", "-f", "/absolute/path/to/docker-compose.yml", 
        "run", "--rm", "maven-tools-mcp"
      ]
    }
  }
}
```

**For development/testing only:**

```bash
docker compose up -d  # Runs server in background for testing
```

### Build from Source (for contributors)

**Prerequisites:**

- Java 24
- Maven 3.9+

```bash
# Clone the repository
git clone https://github.com/arvindand/maven-tools-mcp.git
cd maven-tools-mcp

# Quick build (CI-friendly - unit tests only)
./mvnw clean package -Pci

# Full build with all tests (requires network access)
./mvnw clean package -Pfull

# Run the JAR
java -jar target/maven-tools-mcp-1.5.1-SNAPSHOT.jar
```

**Claude Desktop configuration for JAR:**

```json
{
  "mcpServers": {
    "maven-tools": {
      "command": "java",
      "args": [
        "-jar",
        "/absolute/path/to/maven-tools-mcp-1.5.1-SNAPSHOT.jar"
      ]
    }
  }
}
```

### Build Scripts

For easier builds, use the provided scripts in the `build/` folder:

**Linux/macOS:**

```bash
cd build
./build.sh        # Complete build helper
./build-docker.sh # Docker-focused helper
```

**Windows:**

```cmd
cd build
build.cmd         # Complete build helper
build-docker.cmd  # Docker-focused helper
```

## Enterprise & Custom Clients

This server implements MCP Protocol 2024-11-05 with stdio transport, making it compatible with any MCP-compliant client.

## Configuration

The server can be configured via `application.yaml`:

```yaml
# Cache configuration
spring:
  cache:
    type: caffeine

# Maven Central Repository settings
maven:
  central:
    repository-base-url: https://repo1.maven.org/maven2
    timeout: 10s
    max-results: 100

# Logging (minimal for MCP stdio transport)
logging:
  level:
    root: ERROR
```

## Technical Details

- **Framework**: Spring Boot 3.5.6 with [Spring AI MCP](https://docs.spring.io/spring-ai/reference/api/mcp.html)
- **MCP Protocol**: 2024-11-05
- **Java Version**: 24
- **Transport**: stdio
- **HTTP Client**: OkHttp 5.2.1 with HTTP/2 support
- **Cache**: Caffeine (24-hour TTL, 2000 entries max)
- **Resilience**: Circuit breaker, retry, and rate limiter patterns
- **Data Source**: Maven Central Repository (maven-metadata.xml files)

## References & Resources

### Model Context Protocol (MCP)

- **Official Website**: [modelcontextprotocol.io](https://modelcontextprotocol.io/)
- **GitHub Repository**: [modelcontextprotocol/specification](https://github.com/modelcontextprotocol/specification)
- **Protocol Documentation**: [MCP Specification](https://spec.modelcontextprotocol.io/)

### Spring AI MCP

- **Documentation**: [Spring AI MCP Reference](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-overview.html)
- **GitHub**: [spring-projects/spring-ai](https://github.com/spring-projects/spring-ai)

### Maven Central Repository

- **Repository**: [repo1.maven.org](https://repo1.maven.org/maven2/)
- **Metadata Format**: [Maven Metadata XML Reference](https://maven.apache.org/ref/3.9.6/maven-repository-metadata/)
- **Search API**: [search.maven.org](https://search.maven.org/) (not used in v1.4.0+)

### Context7 MCP Server

- **GitHub Repository**: [upstash/context7](https://github.com/upstash/context7)
- **NPM Package**: [@upstash/context7-mcp](https://www.npmjs.com/package/@upstash/context7-mcp)
- **Documentation**: [Upstash Context7 Blog](https://upstash.com/blog/context7-mcp)

## 📝 Community & Discussion

**Blog Posts:**

- [How I Connected Claude to Maven Central (and Why You Should Too)](https://dev.to/arvindand/how-i-connected-claude-to-maven-central-and-why-you-should-too-2clo)
- [Guided Delegation: Adding Context7 Documentation to My Maven Tools MCP Server](https://dev.to/arvindand/guided-delegation-adding-context7-documentation-to-my-maven-tools-mcp-server-572l)

### Get Involved

- 💬 **Discuss:** Share your experiences and ask questions [on dev.to](https://dev.to/arvindand/how-i-connected-claude-to-maven-central-and-why-you-should-too-2clo)
- 🐛 **Issues:** [Report bugs or request features](https://github.com/arvindand/maven-tools-mcp/issues)
- ⭐ **Support:** Star this repo if it improves your workflow

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

## Author

Arvind Menon

- GitHub: [@arvindand](https://github.com/arvindand)
- Version: 1.5.1

```

--------------------------------------------------------------------------------
/src/main/resources/application-docker.yaml:
--------------------------------------------------------------------------------

```yaml
# Docker profile - ensures clean JSON-RPC communication on STDIO
# The banner and startup logs can interfere with JSON-RPC protocol on STDOUT
spring:
  main:
    banner-mode: "off"  # Must be "off" not OFF - disable banner completely
    log-startup-info: false  # Disable startup info logs on STDOUT

```

--------------------------------------------------------------------------------
/src/main/resources/application-no-context7.yaml:
--------------------------------------------------------------------------------

```yaml
# Profile to completely disable Context7 for native image builds
spring:
  ai:
    mcp:
      client:
        enabled: false  # Disable MCP client completely
        toolcallback:
          enabled: false  # Disable tool callbacks
  main:
    banner-mode: "off"  # Disable banner
    log-startup-info: false

# Disable Context7 guidance hints
context7:
  enabled: false

```

--------------------------------------------------------------------------------
/src/main/java/com/arvindand/mcp/maven/config/Context7Properties.java:
--------------------------------------------------------------------------------

```java
package com.arvindand.mcp.maven.config;

import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * Configuration properties for Context7 MCP integration.
 *
 * @param enabled whether Context7 integration is enabled (exposes raw Context7 MCP tools and
 *     includes guidance hints)
 * @author Arvind Menon
 * @since 1.2.0
 */
@ConfigurationProperties(prefix = "context7")
public record Context7Properties(boolean enabled) {}

```

--------------------------------------------------------------------------------
/src/main/java/com/arvindand/mcp/maven/config/CacheConstants.java:
--------------------------------------------------------------------------------

```java
package com.arvindand.mcp.maven.config;

/**
 * Cache names used throughout the Maven Tools MCP application.
 *
 * @author Arvind Menon
 * @since 1.3.0
 */
public final class CacheConstants {

  // Maven Central cache names
  public static final String MAVEN_VERSION_CHECKS = "maven-version-checks";
  public static final String MAVEN_ALL_VERSIONS = "maven-all-versions";
  public static final String MAVEN_ACCURATE_HISTORICAL_DATA = "maven-accurate-historical-data";

  private CacheConstants() {
    // Utility class
  }
}

```

--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------

```yaml
# Docker Compose for Maven Tools MCP Server
# This provides an easy way to run the MCP server with pre-built images

services:
  maven-tools-mcp:
    image: arvindand/maven-tools-mcp:latest
    container_name: maven-tools-mcp
    
    # MCP uses stdio transport - stdin_open required, tty should be false
    stdin_open: true
    tty: false  # Changed: tty interferes with JSON-RPC stdio
    
    # Network access for Maven Central API
    network_mode: "bridge"
    
    # Environment variables for configuration
    environment:
      - SPRING_PROFILES_ACTIVE=docker
```

--------------------------------------------------------------------------------
/src/test/java/com/arvindand/mcp/maven/config/Context7PropertiesTest.java:
--------------------------------------------------------------------------------

```java
package com.arvindand.mcp.maven.config;

import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;

/**
 * Test to verify Context7Properties binding.
 *
 * @author Arvind Menon
 * @since 1.2.0
 */
@SpringBootTest
@ActiveProfiles("test")
class Context7PropertiesTest {

  @Autowired private Context7Properties context7Properties;

  @Test
  void testContext7PropertiesEnabledByDefault() {
    System.out.println("Context7Properties.enabled = " + context7Properties.enabled());
    assertTrue(context7Properties.enabled(), "Context7 should be enabled by default");
  }
}

```

--------------------------------------------------------------------------------
/src/test/resources/application-test.yaml:
--------------------------------------------------------------------------------

```yaml
logging:
  level:
    # Your app logs at INFO level (goes to STDERR via logback config)
    "[com.arvindand.mcp.maven]": DEBUG
    # Framework logs at ERROR level to minimize noise
    "[org.springframework.ai.mcp]": ERROR
    "[org.springframework.boot]": ERROR
    "[org.springframework]": ERROR
    "[org.apache.http]": ERROR
    "[ch.qos.logback]": ERROR
    root: ERROR

spring:
  cache:
    type: caffeine
  ai:
    mcp:
      client:
        enabled: false
        type: SYNC
        request-timeout: 5s
        streamable-http:
          connections:
            context7:
              url: https://mcp.context7.com
    
# Context7 configuration - enabled by default to match production
# Individual tests can override via @TestConfiguration if needed
context7:
  enabled: true
```

--------------------------------------------------------------------------------
/src/main/java/com/arvindand/mcp/maven/config/MavenCentralProperties.java:
--------------------------------------------------------------------------------

```java
package com.arvindand.mcp.maven.config;

import java.time.Duration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;

/**
 * Configuration properties for Maven Central integration. Uses direct repository metadata access
 * for accurate version information.
 *
 * @param repositoryBaseUrl the base URL for direct Maven repository access
 * @param timeout the timeout duration for API calls
 * @param maxResults the maximum number of results to retrieve per request
 * @author Arvind Menon
 * @since 0.1.0
 */
@ConfigurationProperties(prefix = "maven.central")
public record MavenCentralProperties(
    @DefaultValue("https://repo1.maven.org/maven2") String repositoryBaseUrl,
    @DefaultValue("10s") Duration timeout,
    @DefaultValue("100") int maxResults) {}

```

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

```java
package com.arvindand.mcp.maven.service;

/**
 * Exception thrown when there are issues with Maven Central API interactions. This runtime
 * exception wraps various error conditions that can occur when communicating with Maven Central's
 * search API.
 *
 * @author Arvind Menon
 * @since 0.1.0
 */
public class MavenCentralException extends RuntimeException {

  /**
   * Constructs a new MavenCentralException with the specified detail message.
   *
   * @param message the detail message
   */
  public MavenCentralException(String message) {
    super(message);
  }

  /**
   * Constructs a new MavenCentralException with the specified detail message and cause.
   *
   * @param message the detail message
   * @param cause the cause of this exception
   */
  public MavenCentralException(String message, Throwable cause) {
    super(message, cause);
  }
}

```

--------------------------------------------------------------------------------
/src/main/java/com/arvindand/mcp/maven/model/MavenArtifact.java:
--------------------------------------------------------------------------------

```java
package com.arvindand.mcp.maven.model;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;

/**
 * Represents a Maven artifact, typically containing version and timestamp information.
 *
 * @param id the unique identifier for this artifact
 * @param groupId the Maven group identifier
 * @param artifactId the Maven artifact identifier
 * @param version the artifact version
 * @param packaging the packaging type
 * @param timestamp the timestamp when this artifact was published
 */
@JsonIgnoreProperties(ignoreUnknown = true)
public record MavenArtifact(
    @JsonProperty("id") String id,
    @JsonProperty("g") String groupId,
    @JsonProperty("a") String artifactId,
    @JsonProperty("v") String version,
    @JsonProperty("p") String packaging,
    @JsonProperty("timestamp") long timestamp) {}

```

--------------------------------------------------------------------------------
/src/test/java/com/arvindand/mcp/maven/TestHelpers.java:
--------------------------------------------------------------------------------

```java
package com.arvindand.mcp.maven;

import static org.junit.jupiter.api.Assertions.assertInstanceOf;

import com.arvindand.mcp.maven.model.ToolResponse;

/**
 * Shared test utility methods for integration tests.
 *
 * @author Arvind Menon
 * @since 1.3.0
 */
public final class TestHelpers {

  private TestHelpers() {
    // Prevent instantiation of utility class
  }

  /**
   * Extracts success data from ToolResponse for test assertions.
   *
   * @param <T> the expected type of the success data
   * @param response the ToolResponse to extract data from
   * @return the success data
   * @throws AssertionError if the response is not a success response
   */
  @SuppressWarnings("unchecked")
  public static <T> T getSuccessData(ToolResponse response) {
    assertInstanceOf(
        ToolResponse.Success.class, response, "Expected success response but got: " + response);
    return (T) ((ToolResponse.Success<?>) response).data();
  }
}

```

--------------------------------------------------------------------------------
/.mvn/wrapper/maven-wrapper.properties:
--------------------------------------------------------------------------------

```
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.
wrapperVersion=3.3.2
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip

```

--------------------------------------------------------------------------------
/src/main/java/com/arvindand/mcp/maven/config/JacksonConfig.java:
--------------------------------------------------------------------------------

```java
package com.arvindand.mcp.maven.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

/**
 * Jackson configuration for handling JDK8 types like Optional and Java 8 time types.
 *
 * @author Arvind Menon
 * @since 1.2.0
 */
@Configuration(proxyBeanMethods = false)
public class JacksonConfig {

  /**
   * Configures ObjectMapper with JDK8 and JSR310 module support for Optional and time types.
   *
   * @return configured ObjectMapper
   */
  @Bean
  @Primary
  public ObjectMapper objectMapper() {
    ObjectMapper mapper = new ObjectMapper();
    mapper.registerModule(new Jdk8Module());
    mapper.registerModule(new JavaTimeModule());
    return mapper;
  }
}

```

--------------------------------------------------------------------------------
/src/main/java/com/arvindand/mcp/maven/model/StabilityFilter.java:
--------------------------------------------------------------------------------

```java
package com.arvindand.mcp.maven.model;

/**
 * Filter for controlling version stability preferences across MCP tools.
 *
 * <p>Provides unified parameter semantics for stability filtering, replacing the previous
 * inconsistent parameters (preferStable, stableOnly, onlyStableTargets).
 *
 * @author Arvind Menon
 * @since 1.5.0
 */
public enum StabilityFilter {

  /**
   * Include all version types (stable, RC, beta, alpha, milestone, snapshot).
   *
   * <p>Use when you want comprehensive version information regardless of stability.
   */
  ALL,

  /**
   * Only include production-ready stable versions (excludes RC, beta, alpha, milestone, snapshot).
   *
   * <p>Use when you need only proven, production-ready releases.
   */
  STABLE_ONLY,

  /**
   * Prioritize stable versions in results, but include other types if no stable exists.
   *
   * <p>Use when you prefer stable but want fallback options for libraries without stable releases.
   */
  PREFER_STABLE
}

```

--------------------------------------------------------------------------------
/src/main/java/com/arvindand/mcp/maven/MavenMcpServerApplication.java:
--------------------------------------------------------------------------------

```java
package com.arvindand.mcp.maven;

import com.arvindand.mcp.maven.config.MavenCentralProperties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;

/**
 * Main Spring Boot application class for the Maven MCP Server. This application provides Model
 * Context Protocol (MCP) tools for Maven dependency management, including latest version lookup and
 * version existence checks with caching support.
 *
 * @author Arvind Menon
 * @since 0.1.0
 */
@SpringBootApplication
@EnableCaching
@EnableConfigurationProperties(MavenCentralProperties.class)
public class MavenMcpServerApplication {

  /**
   * Main method to start the Maven MCP Server application.
   *
   * @param args command line arguments
   */
  public static void main(String[] args) {
    SpringApplication.run(MavenMcpServerApplication.class, args);
  }
}

```

--------------------------------------------------------------------------------
/src/main/resources/logback-spring.xml:
--------------------------------------------------------------------------------

```
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- Console appender that writes to STDERR to avoid interfering with MCP JSON-RPC on STDOUT -->
    <appender name="STDERR" class="ch.qos.logback.core.ConsoleAppender">
        <target>System.err</target>
        <encoder>
            <pattern>%d{yyyy-MM-dd'T'HH:mm:ss.SSSXXX} %5p --- [%15.15t] %-40.40logger{39} : %m%n</pattern>
        </encoder>
    </appender>

    <!-- Root logger configuration -->
    <root level="ERROR">
        <appender-ref ref="STDERR" />
    </root>

    <!-- Application-specific logger -->
    <logger name="com.arvindand.mcp.maven" level="INFO" additivity="false">
        <appender-ref ref="STDERR" />
    </logger>

    <!-- Spring Framework loggers -->
    <logger name="org.springframework" level="ERROR" additivity="false">
        <appender-ref ref="STDERR" />
    </logger>

    <!-- Spring AI MCP loggers -->
    <logger name="org.springframework.ai.mcp" level="ERROR" additivity="false">
        <appender-ref ref="STDERR" />
    </logger>
</configuration>

```

--------------------------------------------------------------------------------
/src/main/java/com/arvindand/mcp/maven/model/VersionInfo.java:
--------------------------------------------------------------------------------

```java
package com.arvindand.mcp.maven.model;

import com.fasterxml.jackson.annotation.JsonValue;

/**
 * Version information for a Maven dependency with type-safe version classification.
 *
 * @param version the version string
 * @param type the version type classification
 * @author Arvind Menon
 * @since 0.1.0
 */
public record VersionInfo(String version, VersionType type) {

  /** Type-safe enumeration of Maven version types. */
  public enum VersionType {
    STABLE("stable"),
    RC("rc"),
    BETA("beta"),
    ALPHA("alpha"),
    MILESTONE("milestone");

    private final String displayName;

    VersionType(String displayName) {
      this.displayName = displayName;
    }

    @JsonValue
    public String getDisplayName() {
      return displayName;
    }

    /** Parse version type from string, defaulting to STABLE for unknown types. */
    public static VersionType fromString(String type) {
      if (type == null) return STABLE;
      return switch (type.toLowerCase()) {
        case "rc" -> RC;
        case "beta" -> BETA;
        case "alpha" -> ALPHA;
        case "milestone" -> MILESTONE;
        default -> STABLE;
      };
    }
  }
}

```

--------------------------------------------------------------------------------
/src/main/java/com/arvindand/mcp/maven/config/McpToolsConfig.java:
--------------------------------------------------------------------------------

```java
package com.arvindand.mcp.maven.config;

import com.arvindand.mcp.maven.service.MavenDependencyTools;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Configuration for Maven dependency tools. Registers MCP tools as Spring beans using the Spring AI
 * MCP Server Boot Starter pattern.
 *
 * @author Arvind Menon
 * @since 0.1.0
 */
@Configuration
@EnableConfigurationProperties({MavenCentralProperties.class, Context7Properties.class})
public class McpToolsConfig {

  /**
   * Bean for Maven dependency tools registration with MCP server.
   *
   * @param mavenDependencyTools the Maven dependency tools service
   * @return list of tool callbacks for MCP server auto-configuration
   */
  @Bean
  public ToolCallbackProvider mavenDependencyToolsCallbackProvider(
      MavenDependencyTools mavenDependencyTools) {
    return MethodToolCallbackProvider.builder().toolObjects(mavenDependencyTools).build();
  }
}

```

--------------------------------------------------------------------------------
/src/main/java/com/arvindand/mcp/maven/model/ToolResponse.java:
--------------------------------------------------------------------------------

```java
package com.arvindand.mcp.maven.model;

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

/**
 * Base interface for all MCP tool responses. This allows type-safe tool return types while
 * supporting both success and error responses.
 *
 * @author Arvind Menon
 * @since 1.3.0
 */
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public sealed interface ToolResponse permits ToolResponse.Success, ToolResponse.Error {

  /** Success response containing the actual tool data. */
  @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
  record Success<T>(String status, T data) implements ToolResponse {
    public static <T> Success<T> of(T data) {
      return new Success<>("success", data);
    }
  }

  /** Error response with structured error details. */
  @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
  record Error(String status, McpError error) implements ToolResponse {
    public static Error of(McpError error) {
      return new Error("error", error);
    }

    public static Error of(String message) {
      return new Error("error", McpError.internalError(message));
    }

    public static Error notFound(String message) {
      return new Error("not_found", McpError.invalidInput(message, java.util.Map.of()));
    }
  }
}

```

--------------------------------------------------------------------------------
/src/main/resources/META-INF/additional-spring-configuration-metadata.json:
--------------------------------------------------------------------------------

```json
{
  "properties": [
    {
      "name": "maven.central.repository-base-url",
      "type": "java.lang.String",
      "description": "The base URL for direct Maven repository access. Used for fetching maven-metadata.xml files directly for accurate version information.",
      "defaultValue": "https://repo1.maven.org/maven2"
    },
    {
      "name": "maven.central.timeout",
      "type": "java.lang.String",
      "description": "The timeout duration for requests to the Maven Central repository (e.g., '10s' for 10 seconds)."
    },
    {
      "name": "maven.central.max-results",
      "type": "java.lang.String",
      "description": "The maximum number of results to retrieve per request from the Maven Central repository."
    },
    {
      "name": "maven.central.connection-pool-size",
      "type": "java.lang.Integer",
      "description": "The maximum number of idle connections to maintain in the OkHttp connection pool for Maven Central repository requests.",
      "defaultValue": 50
    },
    {
      "name": "context7.enabled",
      "type": "java.lang.Boolean",
      "description": "Whether Context7 guidance hints are included in Maven tool responses. When enabled, tools include intelligent search suggestions to help LLMs effectively use Context7 tools for migration guidance and upgrade strategies.",
      "defaultValue": true
    }
  ]
}
```

--------------------------------------------------------------------------------
/src/main/java/com/arvindand/mcp/maven/config/CacheConfig.java:
--------------------------------------------------------------------------------

```java
package com.arvindand.mcp.maven.config;

import static com.arvindand.mcp.maven.config.CacheConstants.*;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.time.Duration;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Cache configuration for Maven Tools MCP.
 *
 * <p>Configures cache regions for Maven Central API results with long TTL (24h) for stable data.
 *
 * @author Arvind Menon
 * @since 1.2.0
 */
@Configuration
@EnableCaching
public class CacheConfig {

  @Bean
  public CacheManager cacheManager() {
    CaffeineCacheManager cacheManager = new CaffeineCacheManager();

    // Maven Central caches - long TTL since data is stable
    cacheManager.registerCustomCache(MAVEN_VERSION_CHECKS, mavenCentralCache());
    cacheManager.registerCustomCache(MAVEN_ALL_VERSIONS, mavenCentralCache());
    cacheManager.registerCustomCache(MAVEN_ACCURATE_HISTORICAL_DATA, mavenCentralCache());

    return cacheManager;
  }

  private Cache<Object, Object> mavenCentralCache() {
    return Caffeine.newBuilder().maximumSize(2000).expireAfterWrite(Duration.ofHours(24)).build();
  }
}

```

--------------------------------------------------------------------------------
/src/main/java/com/arvindand/mcp/maven/model/DependencyInfo.java:
--------------------------------------------------------------------------------

```java
package com.arvindand.mcp.maven.model;

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

/**
 * Represents comprehensive dependency information including existence, version details, and
 * metadata.
 *
 * @param status the status of the dependency check (success, error, not_found)
 * @param groupId the Maven group ID
 * @param artifactId the Maven artifact ID
 * @param version the specific version checked
 * @param exists whether the dependency version exists in Maven Central
 * @param type the version type classification (stable, rc, beta, alpha, milestone)
 * @param isStable whether this is considered a stable version
 * @param timestamp the release timestamp in milliseconds since epoch (if available)
 * @author Arvind Menon
 * @since 1.3.0
 */
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record DependencyInfo(
    String status,
    String groupId,
    String artifactId,
    String version,
    boolean exists,
    String type,
    boolean isStable,
    Long timestamp) {
  public static DependencyInfo success(
      MavenCoordinate coordinate,
      String version,
      boolean exists,
      String type,
      boolean isStable,
      Long timestamp) {
    return new DependencyInfo(
        "success",
        coordinate.groupId(),
        coordinate.artifactId(),
        version,
        exists,
        type,
        isStable,
        timestamp);
  }
}

```

--------------------------------------------------------------------------------
/src/main/java/com/arvindand/mcp/maven/model/VersionsByType.java:
--------------------------------------------------------------------------------

```java
package com.arvindand.mcp.maven.model;

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import java.util.Optional;

/**
 * Represents versions organized by their stability type for a dependency.
 *
 * @param dependency the dependency coordinate (groupId:artifactId)
 * @param latestStable latest stable version, if available
 * @param latestRc latest release candidate version, if available
 * @param latestBeta latest beta version, if available
 * @param latestAlpha latest alpha version, if available
 * @param latestMilestone latest milestone version, if available
 * @param totalVersions total number of versions found
 * @author Arvind Menon
 * @since 1.2.0
 */
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record VersionsByType(
    String dependency,
    Optional<VersionInfo> latestStable,
    Optional<VersionInfo> latestRc,
    Optional<VersionInfo> latestBeta,
    Optional<VersionInfo> latestAlpha,
    Optional<VersionInfo> latestMilestone,
    int totalVersions) {

  /** Creates a VersionsByType with primary version based on preference. */
  public static VersionsByType create(
      String dependency,
      Optional<VersionInfo> stable,
      Optional<VersionInfo> rc,
      Optional<VersionInfo> beta,
      Optional<VersionInfo> alpha,
      Optional<VersionInfo> milestone,
      int totalVersions) {
    return new VersionsByType(dependency, stable, rc, beta, alpha, milestone, totalVersions);
  }

  /** Gets the preferred version based on stability preference. */
  public Optional<VersionInfo> getPreferredVersion(boolean preferStable) {
    if (preferStable && latestStable.isPresent()) {
      return latestStable;
    }

    // Return first available version in order of stability
    return latestStable
        .or(() -> latestRc)
        .or(() -> latestBeta)
        .or(() -> latestAlpha)
        .or(() -> latestMilestone);
  }
}

```

--------------------------------------------------------------------------------
/src/main/java/com/arvindand/mcp/maven/config/HttpClientConfig.java:
--------------------------------------------------------------------------------

```java
package com.arvindand.mcp.maven.config;

import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import io.github.resilience4j.ratelimiter.RateLimiterRegistry;
import io.github.resilience4j.retry.RetryRegistry;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.TimeUnit;
import okhttp3.ConnectionPool;
import okhttp3.OkHttpClient;
import okhttp3.Protocol;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;

/**
 * HTTP client configuration with OkHttp for improved performance and HTTP/2 support.
 *
 * <p>Configures connection pooling, timeouts, and protocols for Maven Central API access.
 *
 * @author Arvind Menon
 * @since 1.5.0
 */
@Configuration
public class HttpClientConfig {

  @Bean
  OkHttpClient okHttpClient(
      @Value("${maven.central.connection-pool-size:50}") int poolSize,
      @Value("${maven.central.timeout:8s}") Duration timeout) {

    ConnectionPool connectionPool =
        new ConnectionPool(
            poolSize, // maxIdleConnections
            24, // keepAliveDuration
            TimeUnit.HOURS);

    return new OkHttpClient.Builder()
        .connectionPool(connectionPool)
        .connectTimeout(timeout)
        .readTimeout(timeout.plusSeconds(2))
        .writeTimeout(timeout)
        .protocols(List.of(Protocol.HTTP_2, Protocol.HTTP_1_1))
        .retryOnConnectionFailure(true)
        .build();
  }

  @Bean
  RestClient mavenCentralRestClient(OkHttpClient okHttpClient) {
    return RestClient.builder().build();
  }

  @Bean
  CircuitBreakerRegistry circuitBreakerRegistry() {
    return CircuitBreakerRegistry.ofDefaults();
  }

  @Bean
  RetryRegistry retryRegistry() {
    return RetryRegistry.ofDefaults();
  }

  @Bean
  RateLimiterRegistry rateLimiterRegistry() {
    return RateLimiterRegistry.ofDefaults();
  }
}

```

--------------------------------------------------------------------------------
/src/main/java/com/arvindand/mcp/maven/model/MavenCoordinate.java:
--------------------------------------------------------------------------------

```java
package com.arvindand.mcp.maven.model;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

/**
 * Represents a Maven coordinate with groupId, artifactId, version, packaging, and classifier. This
 * record provides an immutable representation of Maven artifact coordinates.
 *
 * @param groupId the Maven group identifier
 * @param artifactId the Maven artifact identifier
 * @param version the artifact version (optional)
 * @param packaging the packaging type (optional, defaults to 'jar')
 * @param classifier the artifact classifier (optional)
 * @author Arvind Menon
 * @since 0.1.0
 */
@JsonIgnoreProperties(ignoreUnknown = true)
public record MavenCoordinate(
    String groupId, String artifactId, String version, String packaging, String classifier) {

  /**
   * Creates a MavenCoordinate with groupId and artifactId only.
   *
   * @param groupId the Maven group identifier
   * @param artifactId the Maven artifact identifier
   * @return a new MavenCoordinate instance
   */
  public static MavenCoordinate of(String groupId, String artifactId) {
    return new MavenCoordinate(groupId, artifactId, null, null, null);
  }

  /**
   * Creates a MavenCoordinate with groupId, artifactId, and version.
   *
   * @param groupId the Maven group identifier
   * @param artifactId the Maven artifact identifier
   * @param version the artifact version
   * @return a new MavenCoordinate instance
   */
  public static MavenCoordinate of(String groupId, String artifactId, String version) {
    return new MavenCoordinate(groupId, artifactId, version, null, null);
  }

  /**
   * Creates a formatted Maven coordinate string for display.
   *
   * @return the formatted coordinate string in Maven standard format
   */
  public String toCoordinateString() {
    StringBuilder sb = new StringBuilder();
    sb.append(groupId).append(":").append(artifactId);
    if (version != null) {
      sb.append(":").append(version);
    }
    if (packaging != null) {
      sb.append(":").append(packaging);
    }
    if (classifier != null) {
      sb.append(":").append(classifier);
    }
    return sb.toString();
  }
}

```

--------------------------------------------------------------------------------
/src/main/java/com/arvindand/mcp/maven/model/DependencyAge.java:
--------------------------------------------------------------------------------

```java
package com.arvindand.mcp.maven.model;

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import java.time.Instant;
import java.util.Optional;

/**
 * Comprehensive dependency age information with analysis and guidance.
 *
 * @param dependency the Maven coordinate analyzed
 * @param latestVersion the latest version found
 * @param daysSinceLastRelease days since the latest version was released
 * @param lastReleaseDate ISO formatted date of the last release
 * @param ageClassification age classification (fresh/current/aging/stale)
 * @param ageDescription human-readable description of the dependency's age status
 * @param recommendation actionable recommendation based on age analysis
 * @param context7Guidance optional Context7 guidance for deeper integration insights
 * @param maxAgeInDays the configured threshold for age classification
 * @author Arvind Menon
 * @since 1.3.0
 */
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record DependencyAge(
    String dependency,
    String latestVersion,
    DependencyAgeAnalysis.AgeClassification ageClassification,
    long daysSinceLastRelease,
    Instant lastReleaseDate,
    String ageDescription,
    String recommendation,
    Optional<Context7Guidance> context7Guidance) {

  /** Creates a response from a basic DependencyAgeAnalysis with Context7 guidance. */
  public static DependencyAge from(DependencyAgeAnalysis analysis, boolean context7Enabled) {
    // Add Context7 guidance for aging/stale dependencies when Context7 is enabled
    Optional<Context7Guidance> guidance =
        (context7Enabled
                && (analysis.ageClassification() == DependencyAgeAnalysis.AgeClassification.AGING
                    || analysis.ageClassification()
                        == DependencyAgeAnalysis.AgeClassification.STALE))
            ? Optional.of(
                Context7Guidance.forModernization(
                    analysis.dependency(), analysis.ageClassification().getName()))
            : Optional.empty();

    return new DependencyAge(
        analysis.dependency(),
        analysis.latestVersion(),
        analysis.ageClassification(),
        analysis.daysSinceLastRelease(),
        analysis.lastReleaseDate(),
        analysis.ageDescription(),
        analysis.recommendation(),
        guidance);
  }
}

```

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

```java
package com.arvindand.mcp.maven.util;

import com.arvindand.mcp.maven.model.MavenCoordinate;

/**
 * Utility class for parsing Maven coordinates from strings.
 *
 * @author Arvind Menon
 * @since 0.1.0
 */
public final class MavenCoordinateParser {

  private MavenCoordinateParser() {}

  /**
   * Parses a Maven coordinate string in the format:
   * "groupId:artifactId[:version][:packaging][:classifier]"
   *
   * @param dependency the dependency string to parse
   * @return the parsed MavenCoordinate
   * @throws IllegalArgumentException if the format is invalid
   */
  public static MavenCoordinate parse(String dependency) {
    if (dependency == null || dependency.trim().isEmpty()) {
      throw new IllegalArgumentException("Dependency string cannot be null or empty");
    }

    String[] parts = dependency.split(":");
    return switch (parts.length) {
      case 0, 1 ->
          throw new IllegalArgumentException(
              "Invalid Maven coordinate format. Minimum format is 'groupId:artifactId'. Got: "
                  + dependency);
      case 2, 3, 4, 5 -> parseValidCoordinate(parts, dependency);
      default ->
          throw new IllegalArgumentException(
              "Invalid Maven coordinate format. Maximum format is"
                  + " 'groupId:artifactId:version:packaging:classifier'. Got: "
                  + dependency);
    };
  }

  /**
   * Validates that a Maven coordinate string has the minimum required components.
   *
   * @param dependency the dependency string to validate
   * @throws IllegalArgumentException if the format is invalid
   */
  public static void validate(String dependency) {
    parse(dependency);
  }

  private static MavenCoordinate parseValidCoordinate(String[] parts, String original) {
    String groupId = parts[0].trim();
    String artifactId = parts[1].trim();

    if (groupId.isEmpty() || artifactId.isEmpty()) {
      throw new IllegalArgumentException(
          "GroupId and artifactId cannot be empty. Got: " + original);
    }

    String version = getPartOrNull(parts, 2);
    String packaging = getPartOrNull(parts, 3);
    String classifier = getPartOrNull(parts, 4);

    return new MavenCoordinate(groupId, artifactId, version, packaging, classifier);
  }

  private static String getPartOrNull(String[] parts, int index) {
    if (index >= parts.length) return null;
    String part = parts[index].trim();
    return part.isEmpty() ? null : part;
  }
}

```

--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------

```yaml
name: CI/CD Pipeline

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  unit-tests:
    name: Unit Tests
    runs-on: ubuntu-latest
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v4
      
    - name: Set up Java 24
      uses: actions/setup-java@v4
      with:
        java-version: '24'
        distribution: 'temurin'
        
    - name: Cache Maven dependencies
      uses: actions/cache@v4
      with:
        path: ~/.m2
        key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
        restore-keys: ${{ runner.os }}-m2
        
    - name: Make mvnw executable
      run: chmod +x ./mvnw
        
    - name: Run unit tests
      run: ./mvnw clean test
      
    - name: Upload test results
      uses: actions/upload-artifact@v4
      if: always()
      with:
        name: unit-test-results
        path: target/surefire-reports/

  build:
    name: Build Application
    runs-on: ubuntu-latest
    needs: unit-tests
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v4
      
    - name: Set up Java 24
      uses: actions/setup-java@v4
      with:
        java-version: '24'
        distribution: 'temurin'
        
    - name: Cache Maven dependencies
      uses: actions/cache@v4
      with:
        path: ~/.m2
        key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
        restore-keys: ${{ runner.os }}-m2
        
    - name: Make mvnw executable
      run: chmod +x ./mvnw
        
    - name: Build application
      run: ./mvnw clean package -DskipTests
      
    - name: Upload build artifacts
      uses: actions/upload-artifact@v4
      with:
        name: application-jar
        path: target/*.jar

  integration-tests:
    name: Integration Tests (Manual)
    runs-on: ubuntu-latest
    needs: build
    if: github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, '[run-integration]')
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v4
      
    - name: Set up Java 24
      uses: actions/setup-java@v4
      with:
        java-version: '24'
        distribution: 'temurin'
        
    - name: Cache Maven dependencies
      uses: actions/cache@v4
      with:
        path: ~/.m2
        key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
        restore-keys: ${{ runner.os }}-m2
        
    - name: Make mvnw executable
      run: chmod +x ./mvnw
        
    - name: Run integration tests
      run: ./mvnw clean verify -DskipUTs=true
      
    - name: Upload integration test results
      uses: actions/upload-artifact@v4
      if: always()
      with:
        name: integration-test-results
        path: target/failsafe-reports/

```

--------------------------------------------------------------------------------
/src/main/java/com/arvindand/mcp/maven/model/McpError.java:
--------------------------------------------------------------------------------

```java
package com.arvindand.mcp.maven.model;

import java.util.Map;

/**
 * Structured error response for MCP tools following MCP specification patterns.
 *
 * <p>Provides error classification, human-readable messages, contextual data, and retry guidance
 * for better error handling and LLM interpretation.
 *
 * @param code Error classification code (INVALID_INPUT, EXTERNAL_SERVICE_UNAVAILABLE, etc.)
 * @param message Human-readable error description
 * @param data Additional contextual information (coordinate, expected format, service name, etc.)
 * @param retryAfter Suggested retry delay in seconds (null if no retry recommended)
 * @author Arvind Menon
 * @since 1.5.0
 */
public record McpError(String code, String message, Map<String, Object> data, Integer retryAfter) {

  // Error code constants
  public static final String INVALID_INPUT = "INVALID_INPUT";
  public static final String PARSE_ERROR = "PARSE_ERROR";
  public static final String EXTERNAL_SERVICE_UNAVAILABLE = "EXTERNAL_SERVICE_UNAVAILABLE";
  public static final String INTERNAL_ERROR = "INTERNAL_ERROR";

  /**
   * Create error for invalid input parameters.
   *
   * @param message Human-readable error description
   * @param data Contextual data about the invalid input
   * @return McpError with INVALID_INPUT code
   */
  public static McpError invalidInput(String message, Map<String, Object> data) {
    return new McpError(INVALID_INPUT, message, data, null);
  }

  /**
   * Create error for Maven coordinate parsing failures.
   *
   * @param coordinate The invalid coordinate string
   * @param expectedFormat Expected format description
   * @return McpError with PARSE_ERROR code and helpful context
   */
  public static McpError parseError(String coordinate, String expectedFormat) {
    return new McpError(
        PARSE_ERROR,
        "Invalid Maven coordinate format",
        Map.of(
            "coordinate", coordinate,
            "expected_format", expectedFormat,
            "example", "org.springframework.boot:spring-boot-starter"),
        null);
  }

  /**
   * Create error for Maven Central API unavailability.
   *
   * @param message Description of the failure
   * @param retryAfter Suggested retry delay in seconds
   * @return McpError with EXTERNAL_SERVICE_UNAVAILABLE code
   */
  public static McpError mavenCentralUnavailable(String message, int retryAfter) {
    return new McpError(
        EXTERNAL_SERVICE_UNAVAILABLE, message, Map.of("service", "Maven Central"), retryAfter);
  }

  /**
   * Create error for unexpected internal errors.
   *
   * @param message Error description
   * @return McpError with INTERNAL_ERROR code
   */
  public static McpError internalError(String message) {
    return new McpError(INTERNAL_ERROR, message, Map.of(), null);
  }
}

```

--------------------------------------------------------------------------------
/src/main/java/com/arvindand/mcp/maven/model/VersionComparison.java:
--------------------------------------------------------------------------------

```java
package com.arvindand.mcp.maven.model;

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import java.time.Instant;
import java.util.List;
import java.util.Optional;

/**
 * Represents the result of comparing dependency versions with upgrade recommendations.
 *
 * @param comparisonDate when the comparison was performed
 * @param dependencies individual comparison results for each dependency
 * @param updateSummary overall summary of available updates
 * @author Arvind Menon
 * @since 1.3.0
 */
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record VersionComparison(
    Instant comparisonDate,
    List<DependencyComparisonResult> dependencies,
    UpdateSummary updateSummary) {

  /** Individual dependency comparison result. */
  public record DependencyComparisonResult(
      String dependency,
      String currentVersion,
      String latestVersion,
      String latestType,
      String updateType,
      boolean updateAvailable,
      String status,
      String error,
      Optional<Context7Guidance> context7Guidance) {

    public static DependencyComparisonResult success(
        String dependency,
        String currentVersion,
        String latestVersion,
        String latestType,
        String updateType,
        boolean updateAvailable,
        boolean context7Enabled) {
      // Add Context7 guidance if update is available and Context7 is enabled
      Optional<Context7Guidance> guidance =
          (updateAvailable && context7Enabled)
              ? Optional.of(Context7Guidance.forMigration(dependency, updateType))
              : Optional.empty();

      return new DependencyComparisonResult(
          dependency,
          currentVersion,
          latestVersion,
          latestType,
          updateType,
          updateAvailable,
          "success",
          null,
          guidance);
    }

    public static DependencyComparisonResult notFound(String dependency, String currentVersion) {
      return new DependencyComparisonResult(
          dependency, currentVersion, null, null, null, false, "not_found", null, Optional.empty());
    }

    public static DependencyComparisonResult noCurrentVersion(String dependency) {
      return new DependencyComparisonResult(
          dependency, null, null, null, null, false, "no_current_version", null, Optional.empty());
    }

    public static DependencyComparisonResult error(String dependency, String error) {
      return new DependencyComparisonResult(
          dependency, null, null, null, null, false, "error", error, Optional.empty());
    }
  }

  /** Summary of update types. */
  public record UpdateSummary(
      int majorUpdates, int minorUpdates, int patchUpdates, int noUpdates) {}
}

```

--------------------------------------------------------------------------------
/src/main/resources/application.yaml:
--------------------------------------------------------------------------------

```yaml
spring:
  threads:
    virtual:
      enabled: true
  main:
    web-application-type: none
    banner-mode: off
  application:
    name: maven-tools-mcp
  ai:
    mcp:
      server:
        name: maven-tools-mcp
        version: 1.5.1
        type: SYNC
        instructions: "Universal JVM dependency intelligence server for any build tool using Maven Central Repository. Provides comprehensive dependency analysis, latest version lookup, stability filtering, bulk operations, and upgrade recommendations with intelligent caching support. Works with Maven, Gradle, SBT, Mill, and any JVM build tool."
      client:
        enabled: true                     # Enable MCP client with SSE transport (Docker default)
        type: ASYNC
        request-timeout: 15s
        toolcallback:
          enabled: true                   # Enable exposing Context7 tools as raw MCP tools
        streamable-http:
          connections:
            context7:
              url: https://mcp.context7.com/
  cache:
    type: caffeine

# Logging configuration - let logback-spring.xml handle the appenders
logging:
  level:
    # Your app logs at INFO level (goes to STDERR via logback config)
    "[com.arvindand.mcp.maven]": INFO
    # Framework logs at ERROR level to minimize noise
    "[org.springframework.ai.mcp]": ERROR
    "[org.springframework.boot]": ERROR
    "[org.springframework]": ERROR
    "[org.apache.http]": ERROR
    "[ch.qos.logback]": ERROR
    root: ERROR
  # Keep your logback configuration
  config: "classpath:logback-spring.xml"

# Maven Central Repository configuration
maven:
  central:
    repository-base-url: https://repo1.maven.org/maven2
    timeout: 8s
    max-results: 100
    connection-pool-size: 50

# Resilience4j configuration for Maven Central API resilience
resilience4j:
  circuitbreaker:
    instances:
      maven-central:
        failure-rate-threshold: 50
        slow-call-rate-threshold: 50
        slow-call-duration-threshold: 5s
        wait-duration-in-open-state: 30s
        permitted-number-of-calls-in-half-open-state: 5
        sliding-window-size: 10
        sliding-window-type: COUNT_BASED

  retry:
    instances:
      maven-central:
        max-attempts: 3
        wait-duration: 1s
        exponential-backoff-multiplier: 2
        retry-exceptions:
          - java.net.ConnectException
          - java.net.SocketTimeoutException

  ratelimiter:
    instances:
      maven-central:
        limit-for-period: 10
        limit-refresh-period: 1s
        timeout-duration: 0s

# Context7 guidance hints - lightweight suggestions in response models to help LLMs
# effectively use Context7 tools for migration guidance and upgrade strategies.
# No external calls are made - only response enrichment with search hints.
context7:
  enabled: true                       # Set to false to disable Context7 guidance hints in responses

```

--------------------------------------------------------------------------------
/src/test/java/com/arvindand/mcp/maven/service/MavenDependencyToolsContext7EnabledIT.java:
--------------------------------------------------------------------------------

```java
package com.arvindand.mcp.maven.service;

import static com.arvindand.mcp.maven.TestHelpers.getSuccessData;
import static org.junit.jupiter.api.Assertions.*;

import com.arvindand.mcp.maven.config.Context7Properties;
import com.arvindand.mcp.maven.model.*;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.test.context.ActiveProfiles;

/**
 * Integration test for MavenDependencyTools with Context7 enabled. Verifies that Context7 guidance
 * hints are included when context7.enabled=true.
 *
 * @author Arvind Menon
 * @since 1.3.0
 */
@SpringBootTest
@ActiveProfiles("test")
class MavenDependencyToolsContext7EnabledIT {

  @TestConfiguration
  static class Context7EnabledTestConfig {
    @Bean
    @Primary
    public Context7Properties context7Properties() {
      return new Context7Properties(true); // Enable Context7 for this test
    }
  }

  @Autowired private MavenDependencyTools mavenDependencyTools;

  /** Tests that Context7 guidance is included when context7.enabled=true. */
  @Test
  void testContext7GuidanceEnabledForVersionComparison() {
    // Use an older Spring Boot version that will trigger Context7 guidance when enabled
    String oldDependencies = "org.springframework.boot:spring-boot-starter:2.5.0";
    ToolResponse resp =
        mavenDependencyTools.compare_dependency_versions(oldDependencies, StabilityFilter.ALL);

    VersionComparison comparison = getSuccessData(resp);
    assertNotNull(comparison);

    // Check if any dependencies have updates available and Context7 guidance
    for (var dep : comparison.dependencies()) {
      if (dep.updateAvailable()) {
        // When Context7 is enabled and updates are available, guidance should be present
        assertTrue(
            dep.context7Guidance().isPresent(),
            "Context7 guidance should be present for updates when enabled");

        var guidance = dep.context7Guidance().get();
        assertNotNull(guidance.orchestrationInstructions());
        assertTrue(guidance.orchestrationInstructions().contains("resolve-library-id"));
        assertTrue(guidance.orchestrationInstructions().contains("get-library-docs"));
      }
    }
  }

  /** Tests that the tool works with Context7 enabled (simplified test). */
  @Test
  void testContext7EnabledBasicOperation() {
    // This test just verifies that tools work when Context7 is enabled
    // The main functionality is covered by the Context7 guidance test above
    ToolResponse resp = mavenDependencyTools.get_latest_version("junit:junit", StabilityFilter.ALL);

    // Just verify we get a successful response
    assertInstanceOf(
        ToolResponse.Success.class, resp, "Should get successful response with Context7 enabled");
  }
}

```

--------------------------------------------------------------------------------
/src/main/java/com/arvindand/mcp/maven/model/ProjectHealthAnalysis.java:
--------------------------------------------------------------------------------

```java
package com.arvindand.mcp.maven.model;

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import java.util.List;
import java.util.Optional;

/**
 * Comprehensive health analysis for multiple dependencies in a project.
 *
 * @param analysisDate ISO formatted date when analysis was performed
 * @param dependencyCount total number of dependencies analyzed
 * @param healthSummary overall health assessment
 * @param ageDistribution breakdown of dependencies by age classification
 * @param recommendations prioritized list of recommendations
 * @param dependencies individual analysis for each dependency
 * @param maxAgeInDays the age threshold used for classification
 * @author Arvind Menon
 * @since 1.1.0
 */
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record ProjectHealthAnalysis(
    String analysisDate,
    int dependencyCount,
    int successfulAnalysis,
    int failedAnalysis,
    AgeDistribution ageDistribution,
    List<DependencyHealthAnalysis> dependencies,
    List<String> recommendations) {

  /** Distribution of dependencies by age classification. */
  public record AgeDistribution(int fresh, int current, int aging, int stale) {}

  /** Health analysis for a single dependency. */
  public record DependencyHealthAnalysis(
      String dependency,
      String status,
      String latestVersion,
      String ageClassification,
      long daysSinceRelease,
      int healthScore,
      String maintenanceLevel,
      Optional<Context7Guidance> context7Guidance,
      Optional<String> error) {

    /** Creates a successful analysis result. */
    public static DependencyHealthAnalysis success(
        String dependency,
        String latestVersion,
        String ageClassification,
        long daysSinceRelease,
        int healthScore,
        String maintenanceLevel,
        boolean context7Enabled) {
      // Add Context7 guidance for aging/stale dependencies when Context7 is enabled
      Optional<Context7Guidance> guidance =
          (context7Enabled
                  && ("aging".equals(ageClassification) || "stale".equals(ageClassification)))
              ? Optional.of(Context7Guidance.forModernization(dependency, ageClassification))
              : Optional.empty();

      return new DependencyHealthAnalysis(
          dependency,
          "success",
          latestVersion,
          ageClassification,
          daysSinceRelease,
          healthScore,
          maintenanceLevel,
          guidance,
          Optional.empty());
    }

    /** Creates an error result. */
    public static DependencyHealthAnalysis error(String dependency, String error) {
      return new DependencyHealthAnalysis(
          dependency, "error", null, null, 0, 0, null, Optional.empty(), Optional.of(error));
    }

    /** Creates a not found result. */
    public static DependencyHealthAnalysis notFound(String dependency) {
      return new DependencyHealthAnalysis(
          dependency,
          "not_found",
          null,
          null,
          0,
          0,
          null,
          Optional.empty(),
          Optional.of("Dependency not found in Maven Central"));
    }
  }
}

```

--------------------------------------------------------------------------------
/src/main/java/com/arvindand/mcp/maven/model/Context7Guidance.java:
--------------------------------------------------------------------------------

```java
package com.arvindand.mcp.maven.model;

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

/**
 * Context7 orchestration instructions for LLM tool delegation.
 *
 * <p>Provides explicit step-by-step instructions for LLMs to effectively orchestrate the raw
 * Context7 tools 'resolve-library-id' and 'get-library-docs', including fallback web search
 * instructions when Context7 data is insufficient.
 *
 * @author Arvind Menon
 * @since 1.2.0
 */
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record Context7Guidance(String orchestrationInstructions) {

  private static final String STALE = "stale";
  private static final String MAJOR = "major";

  /**
   * Create orchestration instructions for version migration scenarios.
   *
   * @param dependency the Maven coordinate (groupId:artifactId format)
   * @param updateType the type of update (major, minor, patch)
   * @return Context7 orchestration instructions for migration
   */
  public static Context7Guidance forMigration(String dependency, String updateType) {
    String artifactId = extractArtifactId(dependency);
    String orchestrationInstructions = generateMigrationOrchestration(artifactId, updateType);

    return new Context7Guidance(orchestrationInstructions);
  }

  /**
   * Create orchestration instructions for dependency modernization scenarios.
   *
   * @param dependency the Maven coordinate (groupId:artifactId format)
   * @param ageClassification the age classification (fresh, current, aging, stale)
   * @return Context7 orchestration instructions for modernization
   */
  public static Context7Guidance forModernization(String dependency, String ageClassification) {
    String artifactId = extractArtifactId(dependency);
    String orchestrationInstructions =
        generateModernizationOrchestration(artifactId, ageClassification);

    return new Context7Guidance(orchestrationInstructions);
  }

  private static String extractArtifactId(String dependency) {
    if (dependency == null || !dependency.contains(":")) {
      return dependency;
    }

    String[] parts = dependency.split(":");
    return parts[1]; // Return artifactId directly
  }

  private static String generateMigrationOrchestration(String artifactId, String updateType) {
    String topic = MAJOR.equals(updateType) ? "migration guide" : "upgrade guide";
    String fallbackSearch = artifactId + " " + updateType + " version upgrade guide";

    return String.format(
        "Use resolve-library-id tool with libraryName='%s' to find documentation ID. "
            + "Then use get-library-docs tool with the returned Context7 ID and topic='%s' to get upgrade instructions. "
            + "If Context7 doesn't provide sufficient information, perform a web search for '%s'.",
        artifactId, topic, fallbackSearch);
  }

  private static String generateModernizationOrchestration(
      String artifactId, String ageClassification) {
    String topic =
        STALE.equals(ageClassification)
            ? "alternatives and replacements"
            : "modern usage and best practices";
    String fallbackSearch =
        artifactId
            + " "
            + (STALE.equals(ageClassification)
                ? "modernization alternatives"
                : "latest features best practices");

    return String.format(
        "Use resolve-library-id tool with libraryName='%s' to find documentation ID. "
            + "Then use get-library-docs tool with the returned Context7 ID and topic='%s' to get modernization guidance. "
            + "If Context7 doesn't provide sufficient information, perform a web search for '%s'.",
        artifactId, topic, fallbackSearch);
  }
}

```

--------------------------------------------------------------------------------
/src/test/java/com/arvindand/mcp/maven/service/MavenCentralServiceUnitTest.java:
--------------------------------------------------------------------------------

```java
package com.arvindand.mcp.maven.service;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import com.arvindand.mcp.maven.config.MavenCentralProperties;
import com.arvindand.mcp.maven.model.MavenCoordinate;
import java.time.Duration;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

/**
 * Unit tests for MavenCentralService. Tests the service behavior with utility methods and models.
 *
 * @author Arvind Menon
 * @since 0.1.0
 */
class MavenCentralServiceUnitTest {

  private MavenCentralProperties properties;

  @BeforeEach
  void setUp() {
    properties =
        new MavenCentralProperties("https://repo1.maven.org/maven2", Duration.ofSeconds(10), 100);
  }

  /** Tests version comparison and ordering. */
  @Test
  void testVersionOrdering() {
    // Given versions in random order
    String[] versions = {"1.0.0", "2.1.0", "2.0.0", "1.5.0", "2.0.1"};

    // When getting latest
    String latest = com.arvindand.mcp.maven.util.VersionComparator.getLatest(versions);

    // Then
    assertThat(latest).isEqualTo("2.1.0");
  }

  /** Tests Maven coordinate parsing. */
  @Test
  void testMavenCoordinateParsing() {
    // Given
    String coordinateString = "org.springframework:spring-core:6.1.4:jar";

    // When
    MavenCoordinate coordinate =
        com.arvindand.mcp.maven.util.MavenCoordinateParser.parse(coordinateString);

    // Then
    assertThat(coordinate.groupId()).isEqualTo("org.springframework");
    assertThat(coordinate.artifactId()).isEqualTo("spring-core");
    assertThat(coordinate.version()).isEqualTo("6.1.4");
    assertThat(coordinate.packaging()).isEqualTo("jar");
  }

  /** Tests invalid coordinate parsing. */
  @Test
  void testInvalidCoordinateParsing() {
    // Given
    String invalidCoordinate = "invalid";

    // When & Then
    assertThatThrownBy(
            () -> com.arvindand.mcp.maven.util.MavenCoordinateParser.parse(invalidCoordinate))
        .isInstanceOf(IllegalArgumentException.class)
        .hasMessageContaining("Invalid Maven coordinate format");
  }

  /** Tests coordinate string formatting. */
  @Test
  void testCoordinateStringFormatting() {
    // Given
    MavenCoordinate coordinate =
        new MavenCoordinate("org.springframework", "spring-core", "6.1.4", "jar", null);

    // When
    String coordinateString = coordinate.toCoordinateString();

    // Then
    assertThat(coordinateString).isEqualTo("org.springframework:spring-core:6.1.4:jar");
  }

  /** Tests MavenCoordinate static factory methods. */
  @Test
  void testMavenCoordinateFactoryMethods() {
    // Test of() with groupId and artifactId only
    MavenCoordinate coord1 = MavenCoordinate.of("org.springframework", "spring-core");
    assertThat(coord1.groupId()).isEqualTo("org.springframework");
    assertThat(coord1.artifactId()).isEqualTo("spring-core");
    assertThat(coord1.version()).isNull();
    assertThat(coord1.packaging()).isNull();
    assertThat(coord1.classifier()).isNull();

    // Test of() with groupId, artifactId, and version
    MavenCoordinate coord2 = MavenCoordinate.of("org.springframework", "spring-core", "6.1.4");
    assertThat(coord2.groupId()).isEqualTo("org.springframework");
    assertThat(coord2.artifactId()).isEqualTo("spring-core");
    assertThat(coord2.version()).isEqualTo("6.1.4");
    assertThat(coord2.packaging()).isNull();
    assertThat(coord2.classifier()).isNull();
  }

  /** Tests properties configuration. */
  @Test
  void testPropertiesConfiguration() {
    assertThat(properties.repositoryBaseUrl()).isEqualTo("https://repo1.maven.org/maven2");
    assertThat(properties.timeout()).isEqualTo(Duration.ofSeconds(10));
    assertThat(properties.maxResults()).isEqualTo(100);
  }
}

```

--------------------------------------------------------------------------------
/src/test/java/com/arvindand/mcp/maven/model/Context7GuidanceTest.java:
--------------------------------------------------------------------------------

```java
package com.arvindand.mcp.maven.model;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.Test;

/**
 * Unit tests for simplified Context7Guidance model and orchestration instruction generation.
 *
 * @author Arvind Menon
 * @since 1.2.0
 */
class Context7GuidanceTest {

  @Test
  void testForMigration_Major() {
    Context7Guidance guidance =
        Context7Guidance.forMigration("org.springframework.boot:spring-boot-starter", "major");

    assertNotNull(guidance);
    assertNotNull(guidance.orchestrationInstructions());
    assertTrue(guidance.orchestrationInstructions().contains("resolve-library-id"));
    assertTrue(guidance.orchestrationInstructions().contains("get-library-docs"));
    assertTrue(guidance.orchestrationInstructions().contains("spring-boot-starter"));
    assertTrue(guidance.orchestrationInstructions().contains("migration guide"));
    assertTrue(guidance.orchestrationInstructions().contains("web search"));
  }

  @Test
  void testForMigration_Minor() {
    Context7Guidance guidance =
        Context7Guidance.forMigration("com.fasterxml.jackson.core:jackson-core", "minor");

    assertNotNull(guidance.orchestrationInstructions());
    assertTrue(guidance.orchestrationInstructions().contains("jackson-core"));
    assertTrue(guidance.orchestrationInstructions().contains("upgrade guide"));
    assertTrue(guidance.orchestrationInstructions().contains("minor version"));
  }

  @Test
  void testForModernization_Aging() {
    Context7Guidance guidance =
        Context7Guidance.forModernization("org.hibernate:hibernate-core", "aging");

    assertNotNull(guidance.orchestrationInstructions());
    assertTrue(guidance.orchestrationInstructions().contains("hibernate-core"));
    assertTrue(guidance.orchestrationInstructions().contains("modern usage and best practices"));
    assertTrue(guidance.orchestrationInstructions().contains("latest features best practices"));
  }

  @Test
  void testForModernization_Stale() {
    Context7Guidance guidance =
        Context7Guidance.forModernization("commons-lang:commons-lang", "stale");

    assertNotNull(guidance.orchestrationInstructions());
    assertTrue(guidance.orchestrationInstructions().contains("commons-lang"));
    assertTrue(guidance.orchestrationInstructions().contains("alternatives and replacements"));
    assertTrue(guidance.orchestrationInstructions().contains("modernization alternatives"));
  }

  @Test
  void testArtifactIdExtraction() {
    // Test that we use artifactId directly without complex library name extraction
    Context7Guidance springGuidance =
        Context7Guidance.forMigration("org.springframework.boot:spring-boot-starter", "major");
    assertTrue(springGuidance.orchestrationInstructions().contains("spring-boot-starter"));

    Context7Guidance hibernateGuidance =
        Context7Guidance.forMigration("org.hibernate:hibernate-core", "major");
    assertTrue(hibernateGuidance.orchestrationInstructions().contains("hibernate-core"));

    Context7Guidance jacksonGuidance =
        Context7Guidance.forMigration("com.fasterxml.jackson.core:jackson-core", "major");
    assertTrue(jacksonGuidance.orchestrationInstructions().contains("jackson-core"));
  }

  @Test
  void testOrchestrationInstructionsContainRequiredElements() {
    Context7Guidance guidance = Context7Guidance.forMigration("test:test-artifact", "patch");

    // Verify all required orchestration elements are present
    String instructions = guidance.orchestrationInstructions();
    assertTrue(instructions.contains("resolve-library-id tool"));
    assertTrue(instructions.contains("get-library-docs tool"));
    assertTrue(instructions.contains("Context7 ID"));
    assertTrue(instructions.contains("web search"));
    assertTrue(instructions.contains("test-artifact"));
  }
}

```

--------------------------------------------------------------------------------
/src/main/java/com/arvindand/mcp/maven/model/MavenMetadata.java:
--------------------------------------------------------------------------------

```java
package com.arvindand.mcp.maven.model;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import java.util.List;

/**
 * Represents the structure of maven-metadata.xml files found in Maven repositories.
 *
 * <p>Maven repositories contain metadata files that enable discovery and resolution operations. The
 * maven-metadata.xml file lists version information, timestamps, and other artifact details.
 *
 * @param groupId the Maven group identifier
 * @param artifactId the Maven artifact identifier
 * @param version the artifact version (for version-specific metadata)
 * @param versioning versioning information including available versions and timestamps
 * @author Arvind Menon
 * @since 1.4.0
 */
@JsonIgnoreProperties(ignoreUnknown = true)
@JacksonXmlRootElement(localName = "metadata")
public record MavenMetadata(
    @JacksonXmlProperty(localName = "groupId") String groupId,
    @JacksonXmlProperty(localName = "artifactId") String artifactId,
    @JacksonXmlProperty(localName = "version") String version,
    @JacksonXmlProperty(localName = "versioning") VersioningInfo versioning) {

  /**
   * Contains versioning information for the artifact.
   *
   * @param latest the latest deployed version (release or snapshot)
   * @param release the latest release version (non-snapshot)
   * @param versions list of all available versions
   * @param lastUpdated timestamp when metadata was last updated (format: yyyyMMddHHmmss)
   * @param snapshot snapshot-specific information
   */
  @JsonIgnoreProperties(ignoreUnknown = true)
  public record VersioningInfo(
      @JacksonXmlProperty(localName = "latest") String latest,
      @JacksonXmlProperty(localName = "release") String release,
      @JacksonXmlProperty(localName = "versions") VersionList versions,
      @JacksonXmlProperty(localName = "lastUpdated") String lastUpdated,
      @JacksonXmlProperty(localName = "snapshot") SnapshotInfo snapshot) {

    /**
     * Gets all available versions as a list of strings.
     *
     * @return list of version strings, or empty list if none available
     */
    public List<String> getVersionStrings() {
      return versions != null && versions.versionList() != null
          ? versions.versionList()
          : List.of();
    }

    /**
     * Checks if this versioning info contains any version information.
     *
     * @return true if versions are available
     */
    public boolean hasVersions() {
      return !getVersionStrings().isEmpty();
    }
  }

  /**
   * Wrapper for the list of versions to handle XML parsing. Uses JacksonXmlElementWrapper to
   * properly handle multiple version elements.
   *
   * @param versionList the list of version strings
   */
  @JsonIgnoreProperties(ignoreUnknown = true)
  public record VersionList(
      @JacksonXmlProperty(localName = "version") @JacksonXmlElementWrapper(useWrapping = false)
          List<String> versionList) {}

  /**
   * Contains snapshot-specific versioning information.
   *
   * @param timestamp snapshot timestamp
   * @param buildNumber snapshot build number
   * @param localCopy whether this is a local copy
   */
  @JsonIgnoreProperties(ignoreUnknown = true)
  public record SnapshotInfo(
      @JacksonXmlProperty(localName = "timestamp") String timestamp,
      @JacksonXmlProperty(localName = "buildNumber") String buildNumber,
      @JacksonXmlProperty(localName = "localCopy") Boolean localCopy) {}

  /**
   * Checks if this metadata contains valid versioning information.
   *
   * @return true if versioning info is available and contains versions
   */
  public boolean hasValidVersioning() {
    return versioning != null && versioning.hasVersions();
  }

  /**
   * Gets the artifact coordinate string for this metadata.
   *
   * @return coordinate string in format "groupId:artifactId"
   */
  public String getCoordinate() {
    return groupId + ":" + artifactId;
  }
}

```

--------------------------------------------------------------------------------
/src/main/java/com/arvindand/mcp/maven/model/DependencyAgeAnalysis.java:
--------------------------------------------------------------------------------

```java
package com.arvindand.mcp.maven.model;

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import java.time.Instant;
import java.time.temporal.ChronoUnit;

/**
 * Analysis of dependency age and freshness classification.
 *
 * @param dependency the dependency coordinate
 * @param latestVersion the latest version analyzed
 * @param ageClassification age classification (fresh/current/aging/stale)
 * @param daysSinceLastRelease days since the latest version was released
 * @param lastReleaseDate when the latest version was released
 * @param ageDescription human-readable age description
 * @param recommendation suggested action based on age analysis
 * @author Arvind Menon
 * @since 1.1.0
 */
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record DependencyAgeAnalysis(
    String dependency,
    String latestVersion,
    AgeClassification ageClassification,
    long daysSinceLastRelease,
    Instant lastReleaseDate,
    String ageDescription,
    String recommendation) {

  /** Age classification categories for dependencies. */
  public enum AgeClassification {
    FRESH("fresh", "Released within the last 30 days"),
    CURRENT("current", "Released within the last 6 months"),
    AGING("aging", "Released 6 months to 2 years ago"),
    STALE("stale", "Released more than 2 years ago");

    private final String name;
    private final String description;

    AgeClassification(String name, String description) {
      this.name = name;
      this.description = description;
    }

    public String getName() {
      return name;
    }

    public String getDescription() {
      return description;
    }

    /**
     * Classify dependency age based on days since last release.
     *
     * @param daysSinceRelease days since the latest version was released
     * @return appropriate age classification
     */
    public static AgeClassification classify(long daysSinceRelease) {
      if (daysSinceRelease <= 30) {
        return FRESH;
      } else if (daysSinceRelease <= 180) {
        return CURRENT;
      } else if (daysSinceRelease <= 730) {
        return AGING;
      } else {
        return STALE;
      }
    }
  }

  /**
   * Create age analysis from Maven Central timestamp.
   *
   * @param dependency the dependency coordinate
   * @param latestVersion the latest version
   * @param timestamp Maven Central timestamp (milliseconds)
   * @return dependency age analysis
   */
  public static DependencyAgeAnalysis fromTimestamp(
      String dependency, String latestVersion, long timestamp) {
    Instant releaseDate = Instant.ofEpochMilli(timestamp);
    Instant now = Instant.now();
    long daysSinceRelease = ChronoUnit.DAYS.between(releaseDate, now);

    AgeClassification classification = AgeClassification.classify(daysSinceRelease);
    String ageDescription = formatAgeDescription(daysSinceRelease);
    String recommendation = generateRecommendation(classification);

    return new DependencyAgeAnalysis(
        dependency,
        latestVersion,
        classification,
        daysSinceRelease,
        releaseDate,
        ageDescription,
        recommendation);
  }

  private static final String RELEASED = "Released ";

  private static String formatAgeDescription(long days) {
    if (days <= 1) {
      return "Released today or yesterday";
    } else if (days <= 7) {
      return RELEASED + days + " days ago";
    } else if (days <= 30) {
      return RELEASED + (days / 7) + " weeks ago";
    } else if (days <= 365) {
      return RELEASED + (days / 30) + " months ago";
    } else {
      return RELEASED + (days / 365) + " years ago";
    }
  }

  private static String generateRecommendation(AgeClassification classification) {
    return switch (classification) {
      case FRESH -> "Recently updated - safe to use latest version";
      case CURRENT -> "Actively maintained - consider updating if needed";
      case AGING -> "Consider checking for updates or alternatives";
      case STALE -> "Review for continued maintenance and consider alternatives";
    };
  }
}

```

--------------------------------------------------------------------------------
/src/main/java/com/arvindand/mcp/maven/model/BulkCheckResult.java:
--------------------------------------------------------------------------------

```java
package com.arvindand.mcp.maven.model;

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

/**
 * Represents the result of a bulk dependency check with comprehensive version information.
 *
 * @param dependency the dependency coordinate
 * @param version the primary version (latest stable or latest overall if no stable)
 * @param type the version type of the primary version
 * @param status the status (found, not_found, error)
 * @param error the error message (if status is error)
 * @param totalVersions total versions count
 * @param stableVersions stable versions count
 * @param latestStable latest stable version info
 * @param latestRc latest RC version info
 * @param latestBeta latest beta version info
 * @param latestAlpha latest alpha version info
 * @param latestMilestone latest milestone version info
 * @author Arvind Menon
 * @since 0.1.0
 */
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record BulkCheckResult(
    String dependency,
    String version,
    String type,
    String status,
    String error,
    Integer totalVersions,
    Integer stableVersions,
    VersionInfo latestStable,
    VersionInfo latestRc,
    VersionInfo latestBeta,
    VersionInfo latestAlpha,
    VersionInfo latestMilestone) {

  public enum Status {
    FOUND("found"),
    NOT_FOUND("not_found"),
    NO_STABLE_VERSION("no_stable_version"),
    ERROR("error");

    private final String value;

    Status(String value) {
      this.value = value;
    }

    public String getValue() {
      return value;
    }
  }

  public static BulkCheckResult found(String dependency, String version, String type) {
    return new BulkCheckResult(
        dependency,
        version,
        type,
        Status.FOUND.getValue(),
        null,
        null,
        null,
        null,
        null,
        null,
        null,
        null);
  }

  public static BulkCheckResult foundStable(
      String dependency, String version, String type, int totalVersions, int stableVersions) {
    return new BulkCheckResult(
        dependency,
        version,
        type,
        Status.FOUND.getValue(),
        null,
        totalVersions,
        stableVersions,
        null,
        null,
        null,
        null,
        null);
  }

  public static BulkCheckResult foundWithCounts(
      String dependency, String version, String type, int totalVersions, int stableVersions) {
    return new BulkCheckResult(
        dependency,
        version,
        type,
        Status.FOUND.getValue(),
        null,
        totalVersions,
        stableVersions,
        null,
        null,
        null,
        null,
        null);
  }

  public static BulkCheckResult foundComprehensive(
      String dependency,
      String primaryVersion,
      String primaryType,
      int totalVersions,
      int stableVersions,
      VersionInfo latestStable,
      VersionInfo latestRc,
      VersionInfo latestBeta,
      VersionInfo latestAlpha,
      VersionInfo latestMilestone) {
    return new BulkCheckResult(
        dependency,
        primaryVersion,
        primaryType,
        Status.FOUND.getValue(),
        null,
        totalVersions,
        stableVersions,
        latestStable,
        latestRc,
        latestBeta,
        latestAlpha,
        latestMilestone);
  }

  public static BulkCheckResult notFound(String dependency) {
    return new BulkCheckResult(
        dependency,
        null,
        null,
        Status.NOT_FOUND.getValue(),
        null,
        null,
        null,
        null,
        null,
        null,
        null,
        null);
  }

  public static BulkCheckResult noStableVersion(String dependency, int totalVersions) {
    return new BulkCheckResult(
        dependency,
        null,
        null,
        Status.NO_STABLE_VERSION.getValue(),
        null,
        totalVersions,
        null,
        null,
        null,
        null,
        null,
        null);
  }

  public static BulkCheckResult error(String dependency, String error) {
    return new BulkCheckResult(
        dependency,
        null,
        null,
        Status.ERROR.getValue(),
        error,
        null,
        null,
        null,
        null,
        null,
        null,
        null);
  }
}

```

--------------------------------------------------------------------------------
/src/test/java/com/arvindand/mcp/maven/service/MavenCentralServiceRepositoryIT.java:
--------------------------------------------------------------------------------

```java
package com.arvindand.mcp.maven.service;

import static org.junit.jupiter.api.Assertions.*;

import com.arvindand.mcp.maven.model.MavenArtifact;
import com.arvindand.mcp.maven.model.MavenCoordinate;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;

/**
 * Integration tests for MavenCentralService repository access functionality. Tests direct
 * maven-metadata.xml fetching from Maven Central repository.
 *
 * @author Arvind Menon
 * @since 1.4.0
 */
@SpringBootTest
@ActiveProfiles("test")
class MavenCentralServiceRepositoryIT {

  @Autowired private MavenCentralService mavenCentralService;

  @Test
  void testGetLatestVersionWithRepositoryAccess() {
    // Test with a well-known, stable artifact
    MavenCoordinate coordinate = new MavenCoordinate("junit", "junit", null, null, null);

    String latestVersion = mavenCentralService.getLatestVersion(coordinate);

    assertNotNull(latestVersion, "Latest version should not be null");
    assertFalse(latestVersion.trim().isEmpty(), "Latest version should not be empty");
  }

  @Test
  void testGetAllVersionsWithRepositoryAccess() {
    // Test with a well-known artifact that has multiple versions
    MavenCoordinate coordinate = new MavenCoordinate("org.slf4j", "slf4j-api", null, null, null);

    List<String> versions = mavenCentralService.getAllVersions(coordinate);

    assertNotNull(versions, "Versions list should not be null");
    assertFalse(versions.isEmpty(), "Versions list should not be empty");
    assertTrue(versions.size() >= 5, "Should have multiple versions available");

    // Verify versions are sorted in descending order (latest first)
    for (int i = 0; i < Math.min(versions.size() - 1, 3); i++) {
      String current = versions.get(i);
      String next = versions.get(i + 1);
      assertNotNull(current, "Version should not be null");
      assertNotNull(next, "Next version should not be null");
    }
  }

  @Test
  void testGetRecentVersionsWithAccurateTimestamps() {
    // Test with a well-known artifact
    MavenCoordinate coordinate = new MavenCoordinate("com.google.guava", "guava", null, null, null);

    List<MavenArtifact> versions =
        mavenCentralService.getRecentVersionsWithAccurateTimestamps(coordinate, 10);

    assertNotNull(versions, "Versions list should not be null");
    assertFalse(versions.isEmpty(), "Versions list should not be empty");
    assertEquals(10, versions.size(), "Should return the requested number of versions");

    for (MavenArtifact artifact : versions) {
      assertTrue(artifact.timestamp() > 0, "Timestamp should be valid");
    }
  }

  @Test
  void testCheckVersionExistsWithRepositoryAccess() {
    // Test with a specific known version
    MavenCoordinate coordinate = new MavenCoordinate("junit", "junit", null, null, null);

    // junit 4.13.2 is a well-known stable version
    boolean exists = mavenCentralService.checkVersionExists(coordinate, "4.13.2");
    assertTrue(exists, "junit 4.13.2 should exist");

    // Test with a non-existent version
    boolean notExists = mavenCentralService.checkVersionExists(coordinate, "999.999.999");
    assertFalse(notExists, "Non-existent version should return false");
  }

  @Test
  void testRepositoryAccessWithInvalidCoordinate() {
    // Test with a non-existent artifact
    MavenCoordinate invalidCoordinate =
        new MavenCoordinate("com.nonexistent", "invalid-artifact", null, null, null);

    String latestVersion = mavenCentralService.getLatestVersion(invalidCoordinate);
    assertNull(latestVersion, "Non-existent artifact should return null for latest version");

    List<String> versions = mavenCentralService.getAllVersions(invalidCoordinate);
    assertTrue(versions.isEmpty(), "Non-existent artifact should return empty versions list");

    boolean exists = mavenCentralService.checkVersionExists(invalidCoordinate, "1.0.0");
    assertFalse(exists, "Non-existent artifact should return false for version check");

    List<MavenArtifact> timestampedVersions =
        mavenCentralService.getRecentVersionsWithAccurateTimestamps(invalidCoordinate, 10);
    assertTrue(
        timestampedVersions.isEmpty(),
        "Non-existent artifact should return empty list for timestamped versions");
  }

  @Test
  void testRepositoryAccessWithSpringBootStarter() {
    // Test with a Spring Boot starter to verify complex groupId handling
    MavenCoordinate coordinate =
        new MavenCoordinate("org.springframework.boot", "spring-boot-starter", null, null, null);

    String latestVersion = mavenCentralService.getLatestVersion(coordinate);
    assertNotNull(latestVersion, "Spring Boot starter should have a latest version");

    List<String> versions = mavenCentralService.getAllVersions(coordinate);
    assertFalse(versions.isEmpty(), "Spring Boot starter should have multiple versions");
    assertTrue(versions.contains(latestVersion), "Latest version should be in versions list");
  }
}

```

--------------------------------------------------------------------------------
/src/test/java/com/arvindand/mcp/maven/MavenMcpServerIT.java:
--------------------------------------------------------------------------------

```java
package com.arvindand.mcp.maven;

import static org.assertj.core.api.Assertions.assertThat;

import com.arvindand.mcp.maven.model.MavenCoordinate;
import com.arvindand.mcp.maven.service.MavenCentralService;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.TestPropertySource;

/**
 * Integration tests for Maven MCP Server functionality.
 *
 * @author Arvind Menon
 * @since 0.1.0
 */
@SpringBootTest
@ActiveProfiles("test")
@TestPropertySource(properties = {"maven.central.timeout=PT10S", "maven.central.max-results=50"})
class MavenMcpServerIT {

  @Autowired private MavenCentralService mavenCentralService;

  /** Provides test data for the parameterized getLatestVersion test. */
  static Stream<Arguments> getLatestVersionTestData() {
    return Stream.of(
        Arguments.of("org.springframework", "spring-core", "Spring Core"),
        Arguments.of("junit", "junit", "JUnit"),
        Arguments.of("com.google.guava", "guava", "Google Guava"));
  }

  /**
   * Tests getting the latest version of various Maven artifacts. This test verifies the complete
   * integration with Maven Central API for different types of artifacts.
   */
  @ParameterizedTest(name = "Getting latest version for {2}")
  @MethodSource("getLatestVersionTestData")
  void testGetLatestVersion(String groupId, String artifactId, String displayName) {
    // Given
    MavenCoordinate coordinate = MavenCoordinate.of(groupId, artifactId);

    // When
    String latestVersion = mavenCentralService.getLatestVersion(coordinate);

    // Then - Version should be a valid version string (may contain dots, letters, numbers, etc.)
    assertThat(latestVersion).isNotNull().isNotEmpty().matches("^[\\w\\.\\-]+$");
    System.out.println(displayName + " latest version: " + latestVersion);
  }

  /**
   * Tests checking if a specific version exists for Spring Core. This tests the version existence
   * checking functionality.
   */
  @Test
  void testCheckVersionExists_SpringCore_ExistingVersion() {
    // Given
    MavenCoordinate coordinate = MavenCoordinate.of("org.springframework", "spring-core");

    // When - checking for version 6.0.0 which should exist
    boolean exists = mavenCentralService.checkVersionExists(coordinate, "6.0.0");

    // Then
    assertThat(exists).isTrue();
    System.out.println("Spring Core 6.0.0 exists: " + exists);
  }

  /**
   * Tests checking if a non-existing version exists. This tests error handling for non-existing
   * versions.
   */
  @Test
  void testCheckVersionExists_NonExistingVersion() {
    // Given
    MavenCoordinate coordinate = MavenCoordinate.of("org.springframework", "spring-core");

    // When - checking for a version that definitely doesn't exist
    boolean exists = mavenCentralService.checkVersionExists(coordinate, "999.999.999");

    // Then
    assertThat(exists).isFalse();
    System.out.println("Spring Core 999.999.999 exists: " + exists);
  }

  /**
   * Tests caching performance by making the same request twice. The second request should be
   * significantly faster due to caching.
   */
  @Test
  void testCaching_Performance() {
    // Given
    MavenCoordinate coordinate = MavenCoordinate.of("org.springframework", "spring-core");

    // When - first request (will hit Maven Central API)
    long startTime1 = System.nanoTime();
    String version1 = mavenCentralService.getLatestVersion(coordinate);
    long duration1 = System.nanoTime() - startTime1;

    // When - second request (should use cache)
    long startTime2 = System.nanoTime();
    String version2 = mavenCentralService.getLatestVersion(coordinate);
    long duration2 = System.nanoTime() - startTime2;

    // Then
    assertThat(version1).isEqualTo(version2);

    // Convert to milliseconds for readability
    long duration1Ms = duration1 / 1_000_000;
    long duration2Ms = duration2 / 1_000_000;

    System.out.println("First request took: " + duration1Ms + "ms");
    System.out.println("Second request took: " + duration2Ms + "ms (cached)");
    System.out.println("Performance improvement: " + (duration1Ms - duration2Ms) + "ms");
  }

  /**
   * Tests error handling for invalid group and artifact IDs. This verifies graceful handling of
   * non-existing artifacts with repository-first approach.
   */
  @Test
  void testErrorHandling_InvalidArtifact() {
    // Given
    MavenCoordinate coordinate =
        MavenCoordinate.of("com.nonexistent.invalid", "invalid-artifact-xyz");

    // When
    String latestVersion = mavenCentralService.getLatestVersion(coordinate);

    // Then - Repository-first approach returns null for non-existent artifacts
    assertThat(latestVersion).isNull();
    System.out.println("Non-existent artifact gracefully returns: " + latestVersion);
  }

  /**
   * Tests getting the latest version for a POM artifact (different packaging type). This ensures
   * our service can handle different Maven packaging types correctly.
   */
  @Test
  void testGetLatestVersion_PomPackaging() {
    // Given - Spring Boot Starter Parent is a POM artifact, not JAR
    MavenCoordinate coordinate =
        new MavenCoordinate(
            "org.springframework.boot", "spring-boot-starter-parent", null, "pom", null);

    // When
    String latestVersion = mavenCentralService.getLatestVersion(coordinate);

    // Then
    assertThat(latestVersion).isNotNull().isNotEmpty().contains(".");
    System.out.println("Spring Boot Starter Parent (POM) latest version: " + latestVersion);
  }
}

```

--------------------------------------------------------------------------------
/src/main/java/com/arvindand/mcp/maven/model/ReleasePatternAnalysis.java:
--------------------------------------------------------------------------------

```java
package com.arvindand.mcp.maven.model;

import java.time.Instant;
import java.util.List;

/**
 * Analysis of dependency release patterns and maintenance activity.
 *
 * @param dependency the dependency coordinate
 * @param versionsAnalyzed number of versions included in analysis
 * @param timeSpanMonths time span of analysis in months
 * @param averageDaysBetweenReleases average time between releases
 * @param releaseVelocity releases per month on average
 * @param maintenanceLevel classified maintenance activity level
 * @param releaseConsistency how consistent the release pattern is
 * @param lastReleaseDate date of the most recent release
 * @param nextReleasePrediction predicted timeframe for next release
 * @param recentReleases list of recent releases with timestamps
 * @param recommendation maintenance-based recommendation
 * @author Arvind Menon
 * @since 1.1.0
 */
public record ReleasePatternAnalysis(
    String dependency,
    int versionsAnalyzed,
    int timeSpanMonths,
    double averageDaysBetweenReleases,
    double releaseVelocity,
    MaintenanceLevel maintenanceLevel,
    ReleaseConsistency releaseConsistency,
    Instant lastReleaseDate,
    String nextReleasePrediction,
    List<ReleaseInfo> recentReleases,
    String recommendation) {

  /** Maintenance activity level classification. */
  public enum MaintenanceLevel {
    ACTIVE("active", "Frequent releases with consistent maintenance"),
    MODERATE("moderate", "Regular releases with good maintenance"),
    SLOW("slow", "Infrequent releases but still maintained"),
    INACTIVE("inactive", "Very rare releases, possible maintenance issues");

    private final String name;
    private final String description;

    MaintenanceLevel(String name, String description) {
      this.name = name;
      this.description = description;
    }

    public String getName() {
      return name;
    }

    public String getDescription() {
      return description;
    }

    /**
     * Classify maintenance level based on release velocity.
     *
     * @param releasesPerMonth average releases per month
     * @param daysSinceLastRelease days since most recent release
     * @return appropriate maintenance level
     */
    public static MaintenanceLevel classify(double releasesPerMonth, long daysSinceLastRelease) {
      // Factor in recency of last release
      if (daysSinceLastRelease > 365) {
        return INACTIVE;
      } else if (releasesPerMonth >= 2.0) {
        return ACTIVE;
      } else if (releasesPerMonth >= 0.5) {
        return MODERATE;
      } else if (releasesPerMonth >= 0.1) {
        return SLOW;
      } else {
        return INACTIVE;
      }
    }
  }

  /** Release pattern consistency classification. */
  public enum ReleaseConsistency {
    VERY_CONSISTENT("very_consistent", "Highly predictable release schedule"),
    CONSISTENT("consistent", "Generally predictable timing"),
    VARIABLE("variable", "Irregular but reasonable timing"),
    ERRATIC("erratic", "Unpredictable release patterns");

    private final String name;
    private final String description;

    ReleaseConsistency(String name, String description) {
      this.name = name;
      this.description = description;
    }

    public String getName() {
      return name;
    }

    public String getDescription() {
      return description;
    }

    /**
     * Classify release consistency based on variance in release intervals.
     *
     * @param averageDays average days between releases
     * @param maxInterval longest interval between releases
     * @param minInterval shortest interval between releases
     * @return appropriate consistency classification
     */
    public static ReleaseConsistency classify(
        double averageDays, long maxInterval, long minInterval) {
      if (averageDays == 0) return ERRATIC;

      double variance = (maxInterval - minInterval) / averageDays;

      if (variance <= 0.5) {
        return VERY_CONSISTENT;
      } else if (variance <= 1.0) {
        return CONSISTENT;
      } else if (variance <= 2.0) {
        return VARIABLE;
      } else {
        return ERRATIC;
      }
    }
  }

  /**
   * Information about a specific release.
   *
   * @param version the version string
   * @param releaseDate when this version was released
   * @param daysSincePrevious days since the previous release
   */
  public record ReleaseInfo(String version, Instant releaseDate, Long daysSincePrevious) {}

  /**
   * Generate recommendation based on maintenance analysis.
   *
   * @param maintenanceLevel the classified maintenance level
   * @return maintenance-based recommendation
   */
  public static String generateRecommendation(MaintenanceLevel maintenanceLevel) {

    return switch (maintenanceLevel) {
      case ACTIVE -> "Well-maintained dependency with active development - safe to use";
      case MODERATE -> "Regularly maintained - good choice for production use";
      case SLOW ->
          "Slowly maintained - monitor for updates and consider alternatives for critical projects";
      case INACTIVE -> "Minimal maintenance activity - evaluate alternatives and migration plan";
    };
  }

  /**
   * Predict next release timeframe based on historical patterns.
   *
   * @param averageDaysBetweenReleases average interval between releases
   * @param daysSinceLastRelease days since most recent release
   * @param consistency how consistent the release pattern is
   * @return predicted next release timeframe
   */
  public static String predictNextRelease(
      double averageDaysBetweenReleases,
      long daysSinceLastRelease,
      ReleaseConsistency consistency) {

    if (averageDaysBetweenReleases == 0) {
      return "Unable to predict - insufficient release history";
    }

    long expectedDays = Math.round(averageDaysBetweenReleases);
    long overdue = daysSinceLastRelease - expectedDays;

    if (consistency == ReleaseConsistency.ERRATIC) {
      return "Unpredictable release schedule";
    }

    if (overdue > expectedDays / 2) {
      return "Overdue - expected " + (overdue) + " days ago";
    } else if (overdue > 0) {
      return "Due soon - typically releases every " + expectedDays + " days";
    } else {
      long remaining = expectedDays - daysSinceLastRelease;
      if (remaining <= 7) {
        return "Expected within the next week";
      } else if (remaining <= 30) {
        return "Expected in " + remaining + " days";
      } else {
        return "Expected in " + (remaining / 30) + " months";
      }
    }
  }
}

```

--------------------------------------------------------------------------------
/src/test/java/com/arvindand/mcp/maven/service/MavenDependencyToolsIT.java:
--------------------------------------------------------------------------------

```java
package com.arvindand.mcp.maven.service;

import static com.arvindand.mcp.maven.TestHelpers.getSuccessData;
import static org.junit.jupiter.api.Assertions.*;

import com.arvindand.mcp.maven.model.*;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;

/**
 * Integration tests for Maven Dependency Tools using type-safe object assertions.
 *
 * <p>Tests the complete flow from MCP tool invocation to business logic validation, using proper
 * object assertions instead of fragile JSON string matching.
 *
 * @author Arvind Menon
 * @since 0.1.0
 */
@SpringBootTest
@ActiveProfiles("test")
class MavenDependencyToolsIT {

  @Autowired private MavenDependencyTools mavenDependencyTools;

  @Test
  void testGetLatestVersion() {
    ToolResponse response =
        mavenDependencyTools.get_latest_version(
            "org.springframework:spring-core", StabilityFilter.ALL);

    VersionsByType result = getSuccessData(response);
    assertEquals("org.springframework:spring-core", result.dependency());
    assertTrue(result.latestStable().isPresent() || result.totalVersions() > 0);

    if (result.latestStable().isPresent()) {
      VersionInfo stable = result.latestStable().get();
      assertEquals(VersionInfo.VersionType.STABLE, stable.type());
      assertNotNull(stable.version());
    }
  }

  @Test
  void testCheckVersionExists() {
    ToolResponse response = mavenDependencyTools.check_version_exists("junit:junit", "4.13.2");

    DependencyInfo result = getSuccessData(response);
    assertEquals("success", result.status());
    assertEquals("junit", result.groupId());
    assertEquals("junit", result.artifactId());
    assertEquals("4.13.2", result.version());
    assertTrue(result.exists());
    assertEquals("stable", result.type());
    assertTrue(result.isStable());
  }

  @Test
  void testCheckVersionExistsNotFound() {
    ToolResponse response = mavenDependencyTools.check_version_exists("junit:junit", "999.999.999");

    DependencyInfo result = getSuccessData(response);
    assertEquals("success", result.status());
    assertEquals("junit", result.groupId());
    assertEquals("junit", result.artifactId());
    assertEquals("999.999.999", result.version());
    assertFalse(result.exists());
  }

  @Test
  void testBulkCheckDependencies() {
    String dependencies = "org.springframework:spring-core,junit:junit";
    ToolResponse response =
        mavenDependencyTools.check_multiple_dependencies(dependencies, StabilityFilter.ALL);

    List<BulkCheckResult> results = getSuccessData(response);
    assertEquals(2, results.size());

    // Verify spring-core result
    BulkCheckResult springResult =
        results.stream()
            .filter(r -> r.dependency().contains("spring-core"))
            .findFirst()
            .orElseThrow(() -> new AssertionError("Expected spring-core result"));
    assertEquals("found", springResult.status());
    assertNotNull(springResult.version());

    // Verify junit result
    BulkCheckResult junitResult =
        results.stream()
            .filter(r -> r.dependency().contains("junit:junit"))
            .findFirst()
            .orElseThrow(() -> new AssertionError("Expected junit result"));
    assertEquals("found", junitResult.status());
    assertNotNull(junitResult.version());
  }

  @Test
  void testBulkCheckStableOnly() {
    String dependencies = "org.springframework:spring-core,com.fasterxml.jackson.core:jackson-core";
    ToolResponse response =
        mavenDependencyTools.check_multiple_dependencies(dependencies, StabilityFilter.STABLE_ONLY);

    List<BulkCheckResult> results = getSuccessData(response);
    assertEquals(2, results.size());

    // All results should be stable versions when stableOnly=true
    for (BulkCheckResult result : results) {
      assertEquals("found", result.status());
      assertEquals("stable", result.type());
      assertNotNull(result.version());
    }
  }

  @Test
  void testVersionComparison() {
    String currentDependencies = "junit:junit:4.12";
    ToolResponse response =
        mavenDependencyTools.compare_dependency_versions(currentDependencies, StabilityFilter.ALL);

    VersionComparison comparison = getSuccessData(response);
    assertNotNull(comparison.comparisonDate());
    assertNotNull(comparison.updateSummary());
    assertEquals(1, comparison.dependencies().size());

    var depResult = comparison.dependencies().get(0);
    assertEquals("junit:junit:4.12", depResult.dependency());
    assertEquals("4.12", depResult.currentVersion());
    assertEquals("success", depResult.status());
    assertNotNull(depResult.latestVersion());

    // Context7 guidance should be empty in test profile (disabled)
    // Note: Context7 might be enabled for this dependency, so just verify structure
    assertNotNull(depResult.context7Guidance());
  }

  @Test
  void testAnalyzeDependencyAge() {
    ToolResponse response = mavenDependencyTools.analyze_dependency_age("junit:junit", null);

    DependencyAge result = getSuccessData(response);
    assertEquals("junit:junit", result.dependency());
    assertNotNull(result.latestVersion());
    assertNotNull(result.ageClassification());
    assertTrue(result.daysSinceLastRelease() >= 0);
    assertNotNull(result.lastReleaseDate());
    assertNotNull(result.recommendation());

    // Context7 guidance should be empty in test profile (disabled)
    // Note: Context7 might be enabled for this dependency, so just verify structure
    assertNotNull(result.context7Guidance());
  }

  @Test
  void testProjectHealthAnalysis() {
    String dependencies = "junit:junit:4.12,org.slf4j:slf4j-api:1.7.30";
    ToolResponse response = mavenDependencyTools.analyze_project_health(dependencies, null, null);

    ProjectHealthAnalysis result = getSuccessData(response);
    assertNotNull(result.analysisDate());
    assertTrue(result.dependencyCount() >= 2);
    assertTrue(result.dependencies().size() >= 2);
    assertNotNull(result.ageDistribution());
    assertFalse(result.recommendations().isEmpty());

    // Verify individual dependency analyses
    for (var depHealth : result.dependencies()) {
      assertNotNull(depHealth.dependency());
      assertNotNull(depHealth.latestVersion());
      assertNotNull(depHealth.ageClassification());
      assertTrue(
          depHealth.healthScore() >= 0); // healthScore is primitive int, check for valid range
    }
  }

  @Test
  void testErrorHandling() {
    ToolResponse response =
        mavenDependencyTools.check_version_exists("invalid.group:nonexistent", "1.0.0");

    // Should still be success response, but with exists=false
    DependencyInfo result = getSuccessData(response);
    assertEquals("success", result.status());
    assertFalse(result.exists());
  }
}

```

--------------------------------------------------------------------------------
/src/main/java/com/arvindand/mcp/maven/config/NativeImageConfiguration.java:
--------------------------------------------------------------------------------

```java
package com.arvindand.mcp.maven.config;

import com.arvindand.mcp.maven.model.BulkCheckResult;
import com.arvindand.mcp.maven.model.Context7Guidance;
import com.arvindand.mcp.maven.model.DependencyAge;
import com.arvindand.mcp.maven.model.DependencyAgeAnalysis;
import com.arvindand.mcp.maven.model.DependencyInfo;
import com.arvindand.mcp.maven.model.MavenArtifact;
import com.arvindand.mcp.maven.model.MavenCoordinate;
import com.arvindand.mcp.maven.model.MavenMetadata;
import com.arvindand.mcp.maven.model.ProjectHealthAnalysis;
import com.arvindand.mcp.maven.model.ReleasePatternAnalysis;
import com.arvindand.mcp.maven.model.ToolResponse;
import com.arvindand.mcp.maven.model.VersionComparison;
import com.arvindand.mcp.maven.model.VersionInfo;
import com.arvindand.mcp.maven.model.VersionTimelineAnalysis;
import com.arvindand.mcp.maven.model.VersionsByType;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;

/**
 * Native image configuration for reflection hints. This configuration ensures that record classes
 * and their methods are accessible in native images, particularly for SpEL expression evaluation in
 * caching annotations and Jackson JSON serialization.
 *
 * @author Arvind Menon
 * @since 0.1.0
 */
@Configuration(proxyBeanMethods = false)
@ImportRuntimeHints(NativeImageConfiguration.MavenRecordHints.class)
public class NativeImageConfiguration {

  private NativeImageConfiguration() {
    // Prevent instantiation
  }

  /**
   * Runtime hints registrar for Maven record classes to ensure proper reflection access in native
   * images.
   */
  static class MavenRecordHints implements RuntimeHintsRegistrar {
    @Override
    public void registerHints(@NonNull RuntimeHints hints, @Nullable ClassLoader classLoader) {
      // Register all record classes with comprehensive reflection access
      registerRecordClass(hints, MavenCoordinate.class);
      registerRecordClass(hints, BulkCheckResult.class);
      registerRecordClass(hints, DependencyInfo.class);
      registerRecordClass(hints, VersionComparison.class);
      registerRecordClass(hints, VersionComparison.DependencyComparisonResult.class);
      registerRecordClass(hints, VersionComparison.UpdateSummary.class);
      registerRecordClass(hints, VersionInfo.class);

      // Register MavenArtifact for timestamp analysis compatibility
      registerRecordClass(hints, MavenArtifact.class);

      // Register MavenMetadata and its nested records for XML deserialization (v1.4.0)
      registerRecordClass(hints, MavenMetadata.class);
      registerRecordClass(hints, MavenMetadata.VersioningInfo.class);
      registerRecordClass(hints, MavenMetadata.VersionList.class);
      registerRecordClass(hints, MavenMetadata.SnapshotInfo.class);

      // Register VersionComparator record for version parsing
      registerRecordClass(
          hints, com.arvindand.mcp.maven.util.VersionComparator.VersionComponents.class);

      // Register new analytical intelligence record classes (v1.1.0)
      registerRecordClass(hints, DependencyAgeAnalysis.class);
      registerRecordClass(hints, ReleasePatternAnalysis.class);
      registerRecordClass(hints, ReleasePatternAnalysis.ReleaseInfo.class);
      registerRecordClass(hints, VersionTimelineAnalysis.class);
      registerRecordClass(hints, VersionTimelineAnalysis.TimelineEntry.class);
      registerRecordClass(hints, VersionTimelineAnalysis.VelocityTrend.class);
      registerRecordClass(hints, VersionTimelineAnalysis.StabilityPattern.class);
      registerRecordClass(hints, VersionTimelineAnalysis.RecentActivity.class);

      // Register Context7 integration record classes (v1.2.0)
      registerRecordClass(hints, Context7Guidance.class);
      registerRecordClass(hints, DependencyAge.class);

      // Register structured response record classes (v1.2.0)
      registerRecordClass(hints, ProjectHealthAnalysis.class);
      registerRecordClass(hints, ProjectHealthAnalysis.AgeDistribution.class);
      registerRecordClass(hints, ProjectHealthAnalysis.DependencyHealthAnalysis.class);
      registerRecordClass(hints, VersionsByType.class);

      registerRecordClass(hints, ToolResponse.class);
      registerRecordClass(hints, ToolResponse.Success.class);
      registerRecordClass(hints, ToolResponse.Error.class);

      // Register configuration properties record classes
      registerRecordClass(hints, MavenCentralProperties.class);
      registerRecordClass(hints, Context7Properties.class);

      // Register enum classes for Jackson serialization and reflection access
      registerEnumClass(hints, VersionInfo.VersionType.class);
      registerEnumClass(hints, BulkCheckResult.Status.class);

      // Register new analytical intelligence enum classes (v1.1.0)
      registerEnumClass(hints, DependencyAgeAnalysis.AgeClassification.class);
      registerEnumClass(hints, ReleasePatternAnalysis.MaintenanceLevel.class);
      registerEnumClass(hints, ReleasePatternAnalysis.ReleaseConsistency.class);
      registerEnumClass(hints, VersionTimelineAnalysis.TimelineEntry.ReleaseGap.class);
      registerEnumClass(hints, VersionTimelineAnalysis.VelocityTrend.TrendDirection.class);
      registerEnumClass(hints, VersionTimelineAnalysis.RecentActivity.ActivityLevel.class);
    }

    /**
     * Register a record class with all necessary reflection access for Jackson serialization and
     * SpEL expression evaluation.
     */
    private void registerRecordClass(RuntimeHints hints, Class<?> recordClass) {
      hints
          .reflection()
          .registerType(
              recordClass,
              typeHint ->
                  typeHint.withMembers(
                      MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS,
                      MemberCategory.INVOKE_PUBLIC_METHODS,
                      MemberCategory.DECLARED_FIELDS,
                      MemberCategory.PUBLIC_FIELDS,
                      MemberCategory.INTROSPECT_PUBLIC_CONSTRUCTORS,
                      MemberCategory.INTROSPECT_PUBLIC_METHODS,
                      MemberCategory.INTROSPECT_DECLARED_METHODS));
    }

    /**
     * Register an enum class with reflection access for Jackson serialization and native image
     * compatibility.
     */
    private void registerEnumClass(RuntimeHints hints, Class<?> enumClass) {
      hints
          .reflection()
          .registerType(
              enumClass,
              typeHint ->
                  typeHint.withMembers(
                      MemberCategory.PUBLIC_FIELDS,
                      MemberCategory.INVOKE_PUBLIC_METHODS,
                      MemberCategory.INTROSPECT_PUBLIC_METHODS,
                      MemberCategory.INTROSPECT_DECLARED_METHODS));
    }
  }
}

```

--------------------------------------------------------------------------------
/mvnw.cmd:
--------------------------------------------------------------------------------

```
<# : batch portion
@REM ----------------------------------------------------------------------------
@REM Licensed to the Apache Software Foundation (ASF) under one
@REM or more contributor license agreements.  See the NOTICE file
@REM distributed with this work for additional information
@REM regarding copyright ownership.  The ASF licenses this file
@REM to you under the Apache License, Version 2.0 (the
@REM "License"); you may not use this file except in compliance
@REM with the License.  You may obtain a copy of the License at
@REM
@REM    http://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@REM KIND, either express or implied.  See the License for the
@REM specific language governing permissions and limitations
@REM under the License.
@REM ----------------------------------------------------------------------------

@REM ----------------------------------------------------------------------------
@REM Apache Maven Wrapper startup batch script, version 3.3.2
@REM
@REM Optional ENV vars
@REM   MVNW_REPOURL - repo url base for downloading maven distribution
@REM   MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
@REM   MVNW_VERBOSE - true: enable verbose log; others: silence the output
@REM ----------------------------------------------------------------------------

@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
@SET __MVNW_CMD__=
@SET __MVNW_ERROR__=
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
@SET PSModulePath=
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
  IF "%%A"=="MVN_CMD" (set "__MVNW_CMD__=%%B") ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
)
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
@SET __MVNW_PSMODULEP_SAVE=
@SET __MVNW_ARG0_NAME__=
@SET MVNW_USERNAME=
@SET MVNW_PASSWORD=
@IF NOT "%__MVNW_CMD__%"=="" (call "%__MVNW_CMD__%" %* && exit /b 0)
@echo Cannot start maven from wrapper >&2 && exit /b 1
@GOTO :EOF
: end batch / begin powershell #>

$ErrorActionPreference = "Stop"
if ($env:MVNW_VERBOSE -eq "true") {
  $VerbosePreference = "Continue"
}

# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
if (!$distributionUrl) {
  Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
}

switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
  "maven-mvnd-*" {
    $USE_MVND = $true
    $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
    $MVN_CMD = "mvnd.cmd"
    break
  }
  default {
    $USE_MVND = $false
    $MVN_CMD = $script -replace '^mvnw','mvn'
    break
  }
}

# apply MVNW_REPOURL and calculate MAVEN_HOME
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
if ($env:MVNW_REPOURL) {
  $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" }
  $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')"
}
$distributionUrlName = $distributionUrl -replace '^.*/',''
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
$MAVEN_HOME_PARENT = "$env:USERPROFILE\.m2\wrapper\dists\$distributionUrlNameMain"
if ($env:MAVEN_USER_HOME) {
  $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME\wrapper\dists\$distributionUrlNameMain"
}
$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
$MAVEN_HOME = "$MAVEN_HOME_PARENT\$MAVEN_HOME_NAME"

if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
  Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
  Write-Output "MVN_CMD=$MAVEN_HOME\bin\$MVN_CMD"
  exit $?
}

if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
  Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
}

# prepare tmp dir
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
trap {
  if ($TMP_DOWNLOAD_DIR.Exists) {
    try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
    catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
  }
}

New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null

# Download and Install Apache Maven
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
Write-Verbose "Downloading from: $distributionUrl"
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"

$webclient = New-Object System.Net.WebClient
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
  $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
}
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null

# If specified, validate the SHA-256 sum of the Maven distribution zip file
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
if ($distributionSha256Sum) {
  if ($USE_MVND) {
    Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
  }
  Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
  if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
    Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
  }
}

# unzip and move
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null
try {
  Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
} catch {
  if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
    Write-Error "fail to move MAVEN_HOME"
  }
} finally {  try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
  catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
}

Write-Output "MVN_CMD=$MAVEN_HOME\bin\$MVN_CMD"

```

--------------------------------------------------------------------------------
/src/main/java/com/arvindand/mcp/maven/model/VersionTimelineAnalysis.java:
--------------------------------------------------------------------------------

```java
package com.arvindand.mcp.maven.model;

import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;

/**
 * Enhanced version timeline with temporal analysis and patterns.
 *
 * @param dependency the dependency coordinate
 * @param totalVersions total number of versions available
 * @param versionsReturned number of versions in this response
 * @param timeSpanMonths time span covered by the timeline
 * @param versionTimeline chronological list of versions with analysis
 * @param releaseVelocityTrend how release velocity has changed over time
 * @param stabilityPattern pattern of stable vs pre-release versions
 * @param recentActivity summary of recent release activity
 * @param insights key insights from timeline analysis
 * @author Arvind Menon
 * @since 1.1.0
 */
public record VersionTimelineAnalysis(
    String dependency,
    int totalVersions,
    int versionsReturned,
    int timeSpanMonths,
    List<TimelineEntry> versionTimeline,
    VelocityTrend releaseVelocityTrend,
    StabilityPattern stabilityPattern,
    RecentActivity recentActivity,
    List<String> insights) {

  /**
   * Individual version entry in the timeline.
   *
   * @param version the version string
   * @param versionType type classification of this version
   * @param releaseDate when this version was released
   * @param relativeTime human-readable relative time (e.g., "2 months ago")
   * @param daysSincePrevious days since the previous version
   * @param isBreakingChange whether this appears to be a breaking change
   * @param releaseGap gap classification for this release
   */
  public record TimelineEntry(
      String version,
      VersionInfo.VersionType versionType,
      Instant releaseDate,
      String relativeTime,
      Long daysSincePrevious,
      boolean isBreakingChange,
      ReleaseGap releaseGap) {

    /** Classification of gaps between releases. */
    public enum ReleaseGap {
      RAPID("rapid", "Released very quickly after previous"),
      NORMAL("normal", "Normal time interval"),
      SLOW("slow", "Longer than usual interval"),
      MAJOR_GAP("major_gap", "Significant gap in releases");

      private final String name;
      private final String description;

      ReleaseGap(String name, String description) {
        this.name = name;
        this.description = description;
      }

      public String getName() {
        return name;
      }

      public String getDescription() {
        return description;
      }

      /** Classify release gap based on days since previous and average interval. */
      public static ReleaseGap classify(long daysSincePrevious, double averageInterval) {
        if (averageInterval == 0 || daysSincePrevious <= 0) {
          return NORMAL;
        }

        double ratio = daysSincePrevious / averageInterval;
        if (ratio <= 0.3) {
          return RAPID;
        } else if (ratio <= 1.5) {
          return NORMAL;
        } else if (ratio <= 3.0) {
          return SLOW;
        } else {
          return MAJOR_GAP;
        }
      }
    }
  }

  /**
   * Analysis of how release velocity has changed over time.
   *
   * @param trend the overall trend direction
   * @param description human-readable description of the trend
   * @param recentVelocity releases per month in recent period
   * @param historicalVelocity releases per month historically
   * @param changePercentage percentage change in velocity
   */
  public record VelocityTrend(
      TrendDirection trend,
      String description,
      double recentVelocity,
      double historicalVelocity,
      double changePercentage) {

    public enum TrendDirection {
      ACCELERATING("accelerating", "Release frequency is increasing"),
      STABLE("stable", "Release frequency is consistent"),
      DECLINING("declining", "Release frequency is decreasing"),
      ERRATIC("erratic", "Release frequency varies significantly");

      private final String name;
      private final String description;

      TrendDirection(String name, String description) {
        this.name = name;
        this.description = description;
      }

      public String getName() {
        return name;
      }

      public String getDescription() {
        return description;
      }
    }
  }

  /**
   * Pattern analysis of stable vs pre-release versions.
   *
   * @param stablePercentage percentage of versions that are stable
   * @param prereleasePattern how pre-releases are typically used
   * @param stableReleasePattern typical pattern for stable releases
   * @param recommendation recommendation based on stability patterns
   */
  public record StabilityPattern(
      double stablePercentage,
      String prereleasePattern,
      String stableReleasePattern,
      String recommendation) {}

  /**
   * Summary of recent release activity.
   *
   * @param releasesLastMonth number of releases in the last month
   * @param releasesLastQuarter number of releases in the last quarter
   * @param activityLevel classified activity level
   * @param lastReleaseAge days since the last release
   * @param activityDescription human-readable activity description
   */
  public record RecentActivity(
      int releasesLastMonth,
      int releasesLastQuarter,
      ActivityLevel activityLevel,
      long lastReleaseAge,
      String activityDescription) {

    public enum ActivityLevel {
      VERY_ACTIVE("very_active", "Multiple releases per month"),
      ACTIVE("active", "Regular monthly releases"),
      MODERATE("moderate", "Quarterly releases"),
      LOW("low", "Infrequent releases"),
      DORMANT("dormant", "No recent releases");

      private final String name;
      private final String description;

      ActivityLevel(String name, String description) {
        this.name = name;
        this.description = description;
      }

      public String getName() {
        return name;
      }

      public String getDescription() {
        return description;
      }

      public static ActivityLevel classify(int releasesLastMonth, int releasesLastQuarter) {
        if (releasesLastMonth >= 3) {
          return VERY_ACTIVE;
        } else if (releasesLastMonth >= 1) {
          return ACTIVE;
        } else if (releasesLastQuarter >= 2) {
          return MODERATE;
        } else if (releasesLastQuarter >= 1) {
          return LOW;
        } else {
          return DORMANT;
        }
      }
    }
  }

  /** Format relative time description. */
  public static String formatRelativeTime(Instant releaseDate, Instant now) {
    long days = ChronoUnit.DAYS.between(releaseDate, now);

    if (days == 0) {
      return "today";
    } else if (days == 1) {
      return "yesterday";
    } else if (days <= 7) {
      return days + " days ago";
    } else if (days <= 30) {
      return (days / 7) + " weeks ago";
    } else if (days <= 365) {
      return (days / 30) + " months ago";
    } else {
      long years = days / 365;
      long months = (days % 365) / 30;
      if (months == 0) {
        return years + " years ago";
      } else {
        return years + " years, " + months + " months ago";
      }
    }
  }
}

```

--------------------------------------------------------------------------------
/src/test/java/com/arvindand/mcp/maven/service/MavenDependencyToolsPerformanceIT.java:
--------------------------------------------------------------------------------

```java
package com.arvindand.mcp.maven.service;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.arvindand.mcp.maven.model.StabilityFilter;
import com.arvindand.mcp.maven.model.ToolResponse;
import java.time.Duration;
import java.time.Instant;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;

/**
 * Performance tests for Maven Dependency Tools to validate that our optimizations actually improve
 * performance rather than hurt it.
 *
 * @author Arvind Menon
 * @since 0.1.0
 */
@SpringBootTest
@ActiveProfiles("test")
class MavenDependencyToolsPerformanceIT {

  @Autowired private MavenDependencyTools mavenDependencyTools;

  private static final String SMALL_DEPENDENCY_LIST = "org.springframework:spring-core,junit:junit";

  private static final String MEDIUM_DEPENDENCY_LIST =
      "org.springframework:spring-core,junit:junit,com.fasterxml.jackson.core:jackson-core,"
          + "org.apache.commons:commons-lang3,com.google.guava:guava";

  private static final String LARGE_DEPENDENCY_LIST =
      "org.springframework:spring-core,junit:junit,com.fasterxml.jackson.core:jackson-core,"
          + "org.apache.commons:commons-lang3,com.google.guava:guava,org.slf4j:slf4j-api,"
          + "ch.qos.logback:logback-classic,org.apache.httpcomponents:httpclient,"
          + "com.squareup.okhttp3:okhttp,org.apache.maven:maven-core";

  @Test
  void testSmallBulkCheckLatestPerformance() {
    Instant start = Instant.now();
    ToolResponse resp =
        mavenDependencyTools.check_multiple_dependencies(
            SMALL_DEPENDENCY_LIST, StabilityFilter.ALL);
    Duration duration = Duration.between(start, Instant.now());

    System.out.println("Small bulk check (2 deps) took: " + duration.toMillis() + "ms");
    assertNotNull(resp);
    assertTrue(duration.toSeconds() < 10, "Small bulk check should complete in under 10 seconds");
  }

  @Test
  void testMediumBulkCheckLatestPerformance() {
    Instant start = Instant.now();
    ToolResponse resp =
        mavenDependencyTools.check_multiple_dependencies(
            MEDIUM_DEPENDENCY_LIST, StabilityFilter.ALL);
    Duration duration = Duration.between(start, Instant.now());

    System.out.println("Medium bulk check (5 deps) took: " + duration.toMillis() + "ms");
    assertNotNull(resp);
    assertTrue(duration.toSeconds() < 20, "Medium bulk check should complete in under 20 seconds");
  }

  @Test
  void testLargeBulkCheckLatestPerformance() {
    Instant start = Instant.now();
    ToolResponse resp =
        mavenDependencyTools.check_multiple_dependencies(
            LARGE_DEPENDENCY_LIST, StabilityFilter.ALL);
    Duration duration = Duration.between(start, Instant.now());

    System.out.println("Large bulk check (10 deps) took: " + duration.toMillis() + "ms");
    assertNotNull(resp);
    assertTrue(duration.toSeconds() < 40, "Large bulk check should complete in under 40 seconds");
  }

  @Test
  void testBulkStablePerformance() {
    Instant start = Instant.now();
    ToolResponse resp =
        mavenDependencyTools.check_multiple_dependencies(
            MEDIUM_DEPENDENCY_LIST, StabilityFilter.STABLE_ONLY);
    Duration duration = Duration.between(start, Instant.now());

    System.out.println("Bulk stable check (5 deps) took: " + duration.toMillis() + "ms");
    assertNotNull(resp);
    assertTrue(duration.toSeconds() < 20, "Bulk stable check should complete in under 20 seconds");
  }

  @Test
  void testCompareVersionsPerformance() {
    String currentDependencies =
        "org.springframework:spring-core:5.0.0,junit:junit:4.10,"
            + "com.fasterxml.jackson.core:jackson-core:2.10.0";

    Instant start = Instant.now();
    ToolResponse resp =
        mavenDependencyTools.compare_dependency_versions(currentDependencies, StabilityFilter.ALL);
    Duration duration = Duration.between(start, Instant.now());

    System.out.println("Version comparison (3 deps) took: " + duration.toMillis() + "ms");
    assertNotNull(resp);
    assertTrue(duration.toSeconds() < 15, "Version comparison should complete in under 15 seconds");
  }

  @Test
  void testIndividualCallPerformance() {
    // Test that individual calls are reasonably fast
    Instant start = Instant.now();
    ToolResponse resp =
        mavenDependencyTools.get_latest_version(
            "org.springframework:spring-core", StabilityFilter.ALL);
    Duration duration = Duration.between(start, Instant.now());

    System.out.println("Individual get_latest_version took: " + duration.toMillis() + "ms");
    assertNotNull(resp);
    assertTrue(duration.toSeconds() < 5, "Individual call should complete in under 5 seconds");
  }

  @Test
  void testCachingEffectiveness() {
    // Use a unique dependency that's unlikely to be cached from other tests
    String uniqueDependency = "com.github.ben-manes.caffeine:caffeine";

    // Use nanosecond precision for more accurate timing
    long start1 = System.nanoTime();
    ToolResponse r1 =
        mavenDependencyTools.get_latest_version(uniqueDependency, StabilityFilter.ALL);
    long duration1Nanos = System.nanoTime() - start1;

    // Second call should be faster (cached)
    long start2 = System.nanoTime();
    ToolResponse r2 =
        mavenDependencyTools.get_latest_version(uniqueDependency, StabilityFilter.ALL);
    long duration2Nanos = System.nanoTime() - start2;

    // Convert to milliseconds for display
    long duration1Ms = duration1Nanos / 1_000_000;
    long duration2Ms = duration2Nanos / 1_000_000;

    System.out.println("First call (no cache): " + duration1Ms + "ms (" + duration1Nanos + "ns)");
    System.out.println("Second call (cached): " + duration2Ms + "ms (" + duration2Nanos + "ns)");

    assertNotNull(r1);
    assertNotNull(r2);
    // Note: ToolResponse objects may not be equal due to timestamps, but they should both be
    // successful

    // More robust caching check:
    // 1. If both calls are very fast (< 1ms each), assume caching is working efficiently
    // 2. Otherwise, cached call should be faster or at least not significantly slower
    if (duration1Ms == 0 && duration2Ms == 0) {
      // Both calls completed in under 1ms - cache is working very efficiently
      System.out.println(
          "Cache performance: Both calls completed in under 1ms - excellent cache performance");
      assertTrue(
          duration2Nanos <= duration1Nanos * 2,
          "Even with sub-millisecond timing, cached call should not be significantly slower");
    } else {
      // Standard timing comparison when we can measure meaningful differences
      assertTrue(
          duration2Ms <= duration1Ms,
          "Cached call should be faster than or equal to uncached call");

      if (duration2Ms < duration1Ms) {
        System.out.println(
            "Cache performance: Cached call was " + (duration1Ms - duration2Ms) + "ms faster");
      } else {
        System.out.println(
            "Cache performance: Both calls took similar time, indicating efficient caching");
      }
    }
  }
}

```

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

```markdown
# Corporate Certificate Guide

This guide explains how to build custom Docker images with your corporate SSL certificates for environments with SSL inspection/MITM proxies.

## Problem

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:

1. **Use the `-noc7` image variant** (simplest - no Context7 integration)
2. **Build a custom image with your corporate certificates** (includes Context7 with custom certs)

This guide covers option 2.

## Solution: Custom Certificate Binding

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.

## Prerequisites

- Docker installed and running
- Java 24
- Maven 3.9+
- Your corporate CA certificate(s) in `.crt` or `.pem` format

## Step-by-Step Instructions

### 1. Prepare Certificate Directory

Create a directory structure for your certificates:

```bash
mkdir certs
cd certs
```

Create a `type` file (required by Paketo buildpacks):

```bash
echo "ca-certificates" > type
```

Add your corporate certificate(s) to this directory:

```bash
# Copy your corporate CA certificate(s)
cp /path/to/your/corporate-ca.crt .
# You can add multiple certificates
cp /path/to/your/corporate-ca2.crt .
```

**Important:** Only include `.crt` or `.pem` certificate files. Do NOT include private key files (`.key`, `.pem` with private keys).

Your `certs/` directory should look like:

``` plaintext
certs/
├── type
├── corporate-ca.crt
└── corporate-ca2.crt (optional)
```

### 2. Configure Maven Plugin

Edit your `pom.xml` to add certificate bindings to the Spring Boot Maven plugin:

```xml
<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <image>
            <env>
                <BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE>
            </env>
            <bindings>
                <!-- Bind your certs directory to the buildpack's certificate location -->
                <binding>${project.basedir}/certs:/platform/bindings/ca-certificates</binding>
            </bindings>
        </image>
    </configuration>
</plugin>
```

### 3. Build Native Image

Build your custom native image with certificates and Context7 enabled:

```bash
./mvnw clean package -DskipTests
SPRING_PROFILES_ACTIVE=docker ./mvnw -Pnative spring-boot:build-image \
  -Dspring-boot.build-image.imageName=my-maven-tools-mcp:corporate
```

**Build time:** 10-15 minutes for native image compilation

**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).

### 4. Verify Certificate Inclusion

You can verify that the buildpack processed your certificates by checking the build output:

``` plaintext
[creator]     Paketo Buildpack for CA Certificates 3.10.4
[creator]       https://github.com/paketo-buildpacks/ca-certificates
[creator]       Launch Helper: Contributing to layer
[creator]       CA Certificates: Contributing to layer
[creator]         Added 1 additional CA certificate(s) to system truststore
```

### 5. Configure Claude Desktop

Update your Claude Desktop configuration to use the custom image:

**macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
**Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
**Linux:** `~/.config/Claude/claude_desktop_config.json`

```json
{
  "mcpServers": {
    "maven-tools": {
      "command": "docker",
      "args": [
        "run", "-i", "--rm",
        "my-maven-tools-mcp:corporate"
      ]
    }
  }
}
```

### 6. Test the Image

Test your custom image:

```bash
# Quick test - should show MCP initialization
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | \
  docker run -i --rm my-maven-tools-mcp:corporate
```

You should see a JSON-RPC response without SSL errors.

## How It Works

1. **Build-time Injection:** The Maven plugin binds your `certs/` directory to `/platform/bindings/ca-certificates` inside the build container
2. **Buildpack Processing:** The Paketo CA Certificates buildpack detects the binding and adds your certificates to the JVM truststore
3. **Native Compilation:** GraalVM native-image compiles the application with the updated truststore
4. **Runtime:** The native image trusts your corporate certificates without any runtime configuration

## Profiles Explained

- `docker`: Disables Spring Boot banner (required for MCP protocol)

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.

If you don't need Context7 at all, skip this custom build and use the pre-built `latest-noc7` image instead.

## Troubleshooting

### Build fails with "failed to parse certificate"

**Problem:** You likely included a private key file (`.key` or `.pem` with private keys) in the `certs/` directory.

**Solution:** Remove all private key files. Only include certificate files (`.crt` or certificate-only `.pem` files).

### Image still fails to connect to Context7

**Problem:** Your corporate proxy blocks `mcp.context7.com` entirely (domain/IP blocking, not just SSL inspection).

**Solution:** If the domain is blocked, custom certificates won't help. Use the pre-built `-noc7` image variant instead:

```bash
docker pull arvindand/maven-tools-mcp:latest-noc7
```

This image has no Context7 integration and doesn't attempt any outbound connections.

### Build takes longer than expected

**Normal:** Native image compilation takes 10-15 minutes. This is expected for GraalVM native images.

### Certificate not being picked up

**Check:**

1. Ensure `type` file contains exactly: `ca-certificates`
2. Verify certificate files are in `.crt` or `.pem` format
3. Check that the binding path in `pom.xml` is correct: `${project.basedir}/certs:/platform/bindings/ca-certificates`
4. Look for the CA Certificates buildpack output in the build logs

## Alternative: Use Pre-built `-noc7` Image

If you don't need Context7 integration, the simplest solution is to use the pre-built `-noc7` image variant:

```json
{
  "mcpServers": {
    "maven-tools": {
      "command": "docker",
      "args": [
        "run", "-i", "--rm",
        "arvindand/maven-tools-mcp:latest-noc7"
      ]
    }
  }
}
```

This image:

- ✅ Has no Context7 integration (no outbound connections to `mcp.context7.com`)
- ✅ Works in environments with SSL inspection
- ✅ Requires no custom build
- ✅ Provides all Maven dependency analysis tools

## References

- [Spring Boot Maven Plugin - Build Image](https://docs.spring.io/spring-boot/maven-plugin/build-image.html)
- [Paketo CA Certificates Buildpack](https://github.com/paketo-buildpacks/ca-certificates)
- [Paketo Service Bindings Specification](https://github.com/buildpacks/spec/blob/main/extensions/bindings.md)

```

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

```yaml
name: Build and Publish Multi-Architecture Docker Image

on:
  push:
    branches: [main, develop]
    tags: ["v*"]
  pull_request:
    branches: [main]

env:
  DOCKER_HUB_REPOSITORY: arvindand/maven-tools-mcp

jobs:
  build-amd64:
    runs-on: ubuntu-latest
    if: github.event_name != 'pull_request'
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Java 24
        uses: actions/setup-java@v4
        with:
          java-version: "24"
          distribution: "temurin"

      - name: Cache Maven dependencies
        uses: actions/cache@v4
        with:
          path: ~/.m2
          key: ${{ runner.os }}-amd64-m2-${{ hashFiles('**/pom.xml') }}
          restore-keys: ${{ runner.os }}-amd64-m2

      - name: Make mvnw executable
        run: chmod +x ./mvnw

      - name: Build with Maven
        run: ./mvnw clean package -DskipTests

      - name: Log in to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_HUB_USERNAME }}
          password: ${{ secrets.DOCKER_HUB_TOKEN }}

      - name: Build and push AMD64 images (with and without Context7)
        run: |
          PROJECT_VERSION=$(./mvnw help:evaluate -Dexpression=project.version -q -DforceStdout 2>/dev/null || echo "1.4.0")

          # Build AMD64 native image WITH Context7
          SPRING_PROFILES_ACTIVE=docker ./mvnw -Pnative spring-boot:build-image \
            -Dspring-boot.build-image.imageName=arvindand/maven-tools-mcp:${PROJECT_VERSION}-amd64 \
            -Dspring-boot.build-image.env.BP_NATIVE_IMAGE_BUILD_ARGUMENTS="-march=x86-64-v2 --no-fallback" \
            -Dspring-boot.build-image.publish=true \
            -Ddocker.publishRegistry.username="${{ secrets.DOCKER_HUB_USERNAME }}" \
            -Ddocker.publishRegistry.password="${{ secrets.DOCKER_HUB_TOKEN }}"

          # Build AMD64 native image WITHOUT Context7 (noc7 variant)
          SPRING_PROFILES_ACTIVE=docker,no-context7 ./mvnw -Pnative spring-boot:build-image \
            -Dspring-boot.build-image.imageName=arvindand/maven-tools-mcp:${PROJECT_VERSION}-noc7-amd64 \
            -Dspring-boot.build-image.env.BP_NATIVE_IMAGE_BUILD_ARGUMENTS="-march=x86-64-v2 --no-fallback" \
            -Dspring-boot.build-image.publish=true \
            -Ddocker.publishRegistry.username="${{ secrets.DOCKER_HUB_USERNAME }}" \
            -Ddocker.publishRegistry.password="${{ secrets.DOCKER_HUB_TOKEN }}"

  build-arm64:
    runs-on: ubuntu-24.04-arm # Free ARM64 runners for public repos
    if: github.event_name != 'pull_request'
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Java 24
        uses: actions/setup-java@v4
        with:
          java-version: "24"
          distribution: "temurin"

      - name: Cache Maven dependencies
        uses: actions/cache@v4
        with:
          path: ~/.m2
          key: ${{ runner.os }}-arm64-m2-${{ hashFiles('**/pom.xml') }}
          restore-keys: ${{ runner.os }}-arm64-m2

      - name: Make mvnw executable
        run: chmod +x ./mvnw

      - name: Build with Maven
        run: ./mvnw clean package -DskipTests

      - name: Log in to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_HUB_USERNAME }}
          password: ${{ secrets.DOCKER_HUB_TOKEN }}

      - name: Build and push ARM64 images (with and without Context7)
        run: |
          PROJECT_VERSION=$(./mvnw help:evaluate -Dexpression=project.version -q -DforceStdout 2>/dev/null || echo "1.4.0")

          # Build ARM64 native image WITH Context7
          SPRING_PROFILES_ACTIVE=docker ./mvnw -Pnative spring-boot:build-image \
            -Dspring-boot.build-image.imageName=arvindand/maven-tools-mcp:${PROJECT_VERSION}-arm64 \
            -Dspring-boot.build-image.publish=true \
            -Ddocker.publishRegistry.username="${{ secrets.DOCKER_HUB_USERNAME }}" \
            -Ddocker.publishRegistry.password="${{ secrets.DOCKER_HUB_TOKEN }}"

          # Build ARM64 native image WITHOUT Context7 (noc7 variant)
          SPRING_PROFILES_ACTIVE=docker,no-context7 ./mvnw -Pnative spring-boot:build-image \
            -Dspring-boot.build-image.imageName=arvindand/maven-tools-mcp:${PROJECT_VERSION}-noc7-arm64 \
            -Dspring-boot.build-image.publish=true \
            -Ddocker.publishRegistry.username="${{ secrets.DOCKER_HUB_USERNAME }}" \
            -Ddocker.publishRegistry.password="${{ secrets.DOCKER_HUB_TOKEN }}"

  create-manifest:
    runs-on: ubuntu-latest
    needs: [build-amd64, build-arm64]
    if: github.event_name != 'pull_request'
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Make mvnw executable
        run: chmod +x ./mvnw

      - name: Log in to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_HUB_USERNAME }}
          password: ${{ secrets.DOCKER_HUB_TOKEN }}

      - name: Create and push multi-architecture manifests
        run: |
          PROJECT_VERSION=$(./mvnw help:evaluate -Dexpression=project.version -q -DforceStdout 2>/dev/null || echo "1.4.0")

          # Create multi-architecture manifest for version tag (WITH Context7)
          docker manifest create arvindand/maven-tools-mcp:${PROJECT_VERSION} \
            --amend arvindand/maven-tools-mcp:${PROJECT_VERSION}-amd64 \
            --amend arvindand/maven-tools-mcp:${PROJECT_VERSION}-arm64
          docker manifest push arvindand/maven-tools-mcp:${PROJECT_VERSION}

          # Create multi-architecture manifest for latest tag (WITH Context7)
          docker manifest create arvindand/maven-tools-mcp:latest \
            --amend arvindand/maven-tools-mcp:${PROJECT_VERSION}-amd64 \
            --amend arvindand/maven-tools-mcp:${PROJECT_VERSION}-arm64
          docker manifest push arvindand/maven-tools-mcp:latest

          # Create multi-architecture manifest for noc7 version tag (WITHOUT Context7)
          docker manifest create arvindand/maven-tools-mcp:${PROJECT_VERSION}-noc7 \
            --amend arvindand/maven-tools-mcp:${PROJECT_VERSION}-noc7-amd64 \
            --amend arvindand/maven-tools-mcp:${PROJECT_VERSION}-noc7-arm64
          docker manifest push arvindand/maven-tools-mcp:${PROJECT_VERSION}-noc7

          # Create multi-architecture manifest for latest-noc7 tag (WITHOUT Context7)
          docker manifest create arvindand/maven-tools-mcp:latest-noc7 \
            --amend arvindand/maven-tools-mcp:${PROJECT_VERSION}-noc7-amd64 \
            --amend arvindand/maven-tools-mcp:${PROJECT_VERSION}-noc7-arm64
          docker manifest push arvindand/maven-tools-mcp:latest-noc7

  test-pr:
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request'
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Java 24
        uses: actions/setup-java@v4
        with:
          java-version: "24"
          distribution: "temurin"

      - name: Cache Maven dependencies
        uses: actions/cache@v4
        with:
          path: ~/.m2
          key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
          restore-keys: ${{ runner.os }}-m2

      - name: Make mvnw executable
        run: chmod +x ./mvnw

      - name: Build and test PR
        run: |
          ./mvnw clean package -DskipTests

          # Build with Context7 (default)
          SPRING_PROFILES_ACTIVE=docker ./mvnw -Pnative spring-boot:build-image \
            -Dspring-boot.build-image.imageName=maven-tools-mcp:pr-${{ github.event.number }}

          # Build without Context7 (noc7 variant)
          SPRING_PROFILES_ACTIVE=docker,no-context7 ./mvnw -Pnative spring-boot:build-image \
            -Dspring-boot.build-image.imageName=maven-tools-mcp:pr-${{ github.event.number }}-noc7

          # Test both images
          echo "Testing image with Context7..."
          timeout 30s docker run --rm maven-tools-mcp:pr-${{ github.event.number }} --help || true

          echo "Testing image without Context7..."
          timeout 30s docker run --rm maven-tools-mcp:pr-${{ github.event.number }}-noc7 --help || true

```

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

```java
package com.arvindand.mcp.maven.service;

import static com.arvindand.mcp.maven.config.CacheConstants.*;

import com.arvindand.mcp.maven.config.MavenCentralProperties;
import com.arvindand.mcp.maven.model.MavenArtifact;
import com.arvindand.mcp.maven.model.MavenCoordinate;
import com.arvindand.mcp.maven.model.MavenMetadata;
import com.arvindand.mcp.maven.util.VersionComparator;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import io.github.resilience4j.ratelimiter.annotation.RateLimiter;
import io.github.resilience4j.retry.annotation.Retry;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;

/**
 * Service for interacting with Maven Central via direct repository metadata access. Fetches
 * maven-metadata.xml files directly for accurate version information.
 *
 * @author Arvind Menon
 * @since 0.1.0
 */
@Service
public class MavenCentralService {

  private static final Logger logger = LoggerFactory.getLogger(MavenCentralService.class);
  private static final String METADATA_FETCH_ERROR_MSG =
      "Repository metadata fetch failed for {}:{}";
  private static final int ACCURATE_TIMESTAMP_VERSION_LIMIT = 30;
  private final RestClient restClient;
  private final XmlMapper xmlMapper;
  private final MavenCentralProperties properties;
  private final VersionComparator versionComparator;
  private final ExecutorService virtualThreadExecutor = Executors.newVirtualThreadPerTaskExecutor();

  public MavenCentralService(MavenCentralProperties properties, RestClient mavenCentralRestClient) {
    this.properties = properties;
    this.restClient = mavenCentralRestClient;
    this.xmlMapper = new XmlMapper();
    this.versionComparator = new VersionComparator();
  }

  /**
   * Gets the latest version for a Maven coordinate. Leverages cached results from getAllVersions()
   * for efficiency.
   *
   * @param coordinate the Maven coordinate
   * @return the latest version or null if not found
   */
  public String getLatestVersion(MavenCoordinate coordinate) {
    List<String> versions = getAllVersions(coordinate);
    return versions.isEmpty() ? null : versions.get(0);
  }

  /**
   * Checks if a specific version exists for a Maven coordinate.
   *
   * @param coordinate the Maven coordinate
   * @param version the version to check
   * @return true if the version exists
   */
  @Cacheable(
      value = MAVEN_VERSION_CHECKS,
      key =
          "#coordinate.groupId() + ':' + #coordinate.artifactId() + ':' + #version + ':' +"
              + " (#coordinate.packaging() ?: 'jar')")
  public boolean checkVersionExists(MavenCoordinate coordinate, String version) {
    try {
      Optional<MavenMetadata> metadata = fetchRepositoryMetadata(coordinate);
      if (metadata.isPresent() && metadata.get().hasValidVersioning()) {
        return metadata.get().versioning().getVersionStrings().contains(version);
      }
      return false;
    } catch (Exception e) {
      logger.debug(METADATA_FETCH_ERROR_MSG, coordinate.groupId(), coordinate.artifactId(), e);
      return false;
    }
  }

  /**
   * Gets all available versions for a Maven coordinate.
   *
   * @param coordinate the Maven coordinate
   * @return list of all versions, sorted by version descending
   */
  @Cacheable(
      value = MAVEN_ALL_VERSIONS,
      key =
          "#coordinate.groupId() + ':' + #coordinate.artifactId() + ':' + (#coordinate.packaging()"
              + " ?: 'jar')")
  @CircuitBreaker(name = "maven-central", fallbackMethod = "getAllVersionsFallback")
  @Retry(name = "maven-central")
  @RateLimiter(name = "maven-central")
  public List<String> getAllVersions(MavenCoordinate coordinate) {
    return fetchAllVersionsInternal(coordinate);
  }

  @SuppressWarnings("unused") // Used via @CircuitBreaker fallbackMethod
  private List<String> getAllVersionsFallback(MavenCoordinate coordinate, Exception ex) {
    logger.warn(
        "Circuit breaker fallback for {}:{} - {}",
        coordinate.groupId(),
        coordinate.artifactId(),
        ex.getMessage());
    return Collections.emptyList();
  }

  /**
   * Gets all available versions with accurate timestamps for the most recent versions.
   *
   * @param coordinate the Maven coordinate
   * @return list of artifacts with accurate timestamp information for recent versions
   */
  public List<MavenArtifact> getAllVersionsWithTimestamps(MavenCoordinate coordinate) {
    return getRecentVersionsWithAccurateTimestamps(coordinate, ACCURATE_TIMESTAMP_VERSION_LIMIT);
  }

  /**
   * Gets version information with accurate timestamps for the specified number of recent versions.
   *
   * @param coordinate the Maven coordinate
   * @param maxVersions maximum number of versions to retrieve
   * @return list of recent artifacts with accurate timestamp information
   */
  @Cacheable(
      value = MAVEN_ACCURATE_HISTORICAL_DATA,
      key =
          "#coordinate.groupId() + ':' + #coordinate.artifactId() + ':' + #maxVersions + ':' +"
              + " (#coordinate.packaging() ?: 'jar')")
  public List<MavenArtifact> getRecentVersionsWithAccurateTimestamps(
      MavenCoordinate coordinate, int maxVersions) {
    List<String> allVersions = getAllVersions(coordinate);
    List<String> recentVersions = allVersions.stream().limit(maxVersions).toList();

    List<CompletableFuture<MavenArtifact>> futures =
        recentVersions.stream()
            .map(
                version ->
                    CompletableFuture.supplyAsync(
                        () -> fetchArtifactWithTimestamp(coordinate, version),
                        virtualThreadExecutor))
            .toList();

    return futures.stream()
        .map(CompletableFuture::join)
        .filter(java.util.Objects::nonNull)
        .sorted((a, b) -> versionComparator.reversed().compare(a.version(), b.version()))
        .toList();
  }

  private MavenArtifact fetchArtifactWithTimestamp(MavenCoordinate coordinate, String version) {
    String pomUrl = buildPomUrl(coordinate, version);
    try {
      long timestamp =
          restClient
              .head()
              .uri(pomUrl)
              .retrieve()
              .toBodilessEntity()
              .getHeaders()
              .getLastModified();

      return new MavenArtifact(
          coordinate.groupId() + ":" + coordinate.artifactId() + ":" + version,
          coordinate.groupId(),
          coordinate.artifactId(),
          version,
          coordinate.packaging() != null ? coordinate.packaging() : "jar",
          timestamp);
    } catch (Exception e) {
      logger.debug("Failed to fetch timestamp for {}:{}", pomUrl, e.getMessage());
      return null;
    }
  }

  /**
   * Internal method to fetch all versions without caching (used by cacheable public methods).
   *
   * @param coordinate the Maven coordinate
   * @return list of all versions, sorted by version descending
   */
  private List<String> fetchAllVersionsInternal(MavenCoordinate coordinate) {
    try {
      Optional<MavenMetadata> metadata = fetchRepositoryMetadata(coordinate);
      if (metadata.isPresent() && metadata.get().hasValidVersioning()) {
        List<String> versions = metadata.get().versioning().getVersionStrings();
        return versions.stream()
            .sorted(versionComparator.reversed())
            .limit(properties.maxResults())
            .toList();
      }
      return Collections.emptyList();
    } catch (Exception e) {
      logger.debug(METADATA_FETCH_ERROR_MSG, coordinate.groupId(), coordinate.artifactId(), e);
      return Collections.emptyList();
    }
  }

  /**
   * Fetches maven-metadata.xml from the repository for the given coordinate.
   *
   * @param coordinate the Maven coordinate
   * @return optional containing metadata if found and parseable
   */
  @CircuitBreaker(name = "maven-central")
  @Retry(name = "maven-central")
  @RateLimiter(name = "maven-central")
  private Optional<MavenMetadata> fetchRepositoryMetadata(MavenCoordinate coordinate) {
    try {
      String metadataUrl = buildMetadataUrl(coordinate);
      logger.debug("Fetching metadata from: {}", metadataUrl);

      String xmlContent = restClient.get().uri(metadataUrl).retrieve().body(String.class);

      if (xmlContent != null && !xmlContent.trim().isEmpty()) {
        MavenMetadata metadata = xmlMapper.readValue(xmlContent, MavenMetadata.class);
        return Optional.of(metadata);
      }

      return Optional.empty();
    } catch (Exception e) {
      logger.debug(
          "Failed to fetch metadata for {}:{}: {}",
          coordinate.groupId(),
          coordinate.artifactId(),
          e.getMessage());
      return Optional.empty();
    }
  }

  /**
   * Builds the URL for maven-metadata.xml for the given coordinate.
   *
   * @param coordinate the Maven coordinate
   * @return the metadata URL
   */
  private String buildMetadataUrl(MavenCoordinate coordinate) {
    String groupPath = coordinate.groupId().replace('.', '/');
    return String.format(
        "%s/%s/%s/maven-metadata.xml",
        properties.repositoryBaseUrl(), groupPath, coordinate.artifactId());
  }

  private String buildPomUrl(MavenCoordinate coordinate, String version) {
    String groupPath = coordinate.groupId().replace('.', '/');
    return String.format(
        "%s/%s/%s/%s/%s-%s.pom",
        properties.repositoryBaseUrl(),
        groupPath,
        coordinate.artifactId(),
        version,
        coordinate.artifactId(),
        version);
  }
}

```

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

```
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.5.6</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.arvindand.mcp</groupId>
    <artifactId>maven-tools-mcp</artifactId>
    <version>1.5.1</version>
    <name>maven-tools-mcp</name>
    <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>
    <url>https://github.com/arvindand/maven-tools-mcp</url>
    <licenses>
        <license>
            <name>MIT License</name>
            <url>https://opensource.org/licenses/MIT</url>
        </license>
    </licenses>
    <developers>
        <developer>
            <name>Arvind Menon</name>
            <email>[email protected]</email>
        </developer>
    </developers>
    <scm>
        <connection>scm:git:git://github.com/arvindand/maven-tools-mcp.git</connection>
        <developerConnection>scm:git:ssh://github.com:arvindand/maven-tools-mcp.git</developerConnection>
        <tag>HEAD</tag>
        <url>https://github.com/arvindand/maven-tools-mcp</url>
    </scm>
    <properties>
        <java.version>24</java.version>
        <spring-ai.version>1.1.0-M3</spring-ai.version>
        <fmt-maven-plugin.version>2.29</fmt-maven-plugin.version>
        <maven-artifact.version>3.9.11</maven-artifact.version>
        <okhttp.version>5.2.1</okhttp.version>
        <resilience4j.version>2.3.0</resilience4j.version>
        <!-- Test execution properties -->
        <skipUTs>false</skipUTs>
        <skipITs>false</skipITs>
    </properties>
    <dependencies>
        <!-- MCP Server -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-mcp-server</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-mcp-client-webflux</artifactId>
        </dependency>
        <!-- Caching support -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>

        <!-- Caffeine cache implementation -->
        <dependency>
            <groupId>com.github.ben-manes.caffeine</groupId>
            <artifactId>caffeine</artifactId>
        </dependency>

        <!-- Maven version comparison library -->
        <dependency>
            <groupId>org.apache.maven</groupId>
            <artifactId>maven-artifact</artifactId>
            <version>${maven-artifact.version}</version>
        </dependency>

        <!-- Jackson for JSON processing -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-json</artifactId>
        </dependency>

        <!-- Jackson JDK8 module for Optional support -->
        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jdk8</artifactId>
        </dependency>

        <!-- Jackson JSR310 module for Java 8 time types -->
        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
        </dependency>

        <!-- Jackson XML module for maven-metadata.xml parsing -->
        <dependency>
            <groupId>com.fasterxml.jackson.dataformat</groupId>
            <artifactId>jackson-dataformat-xml</artifactId>
        </dependency>

        <!-- OkHttp for HTTP/2 and connection pooling -->
        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp-jvm</artifactId>
            <version>${okhttp.version}</version>
        </dependency>

        <!-- Resilience4j for circuit breaker, retry, rate limiter -->
        <dependency>
            <groupId>io.github.resilience4j</groupId>
            <artifactId>resilience4j-spring-boot3</artifactId>
            <version>${resilience4j.version}</version>
        </dependency>

        <!-- AOP for @CircuitBreaker, @Retry, @RateLimiter annotations -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <!-- Test dependencies -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.ai</groupId>
                <artifactId>spring-ai-bom</artifactId>
                <version>${spring-ai.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <profiles>
        <!-- CI Profile: Only unit tests, no integration tests -->
        <profile>
            <id>ci</id>
            <properties>
                <skipITs>true</skipITs>
                <skipUTs>false</skipUTs>
            </properties>
        </profile>

        <!-- Integration Profile: Run integration tests only -->
        <profile>
            <id>integration</id>
            <properties>
                <skipITs>false</skipITs>
                <skipUTs>true</skipUTs>
            </properties>
        </profile>

        <!-- Full Profile: Run all tests -->
        <profile>
            <id>full</id>
            <properties>
                <skipITs>false</skipITs>
                <skipUTs>false</skipUTs>
            </properties>
        </profile>
    </profiles>

    <build>
        <plugins>
            <plugin>
                <groupId>org.graalvm.buildtools</groupId>
                <artifactId>native-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <image>
                        <env>
                            <BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE>
                        </env>
                    </image>
                    <docker>
                        <publishRegistry>
                            <username>${docker.publishRegistry.username}</username>
                            <password>${docker.publishRegistry.password}</password>
                            <url>https://index.docker.io/v1/</url>
                        </publishRegistry>
                    </docker>
                </configuration>
            </plugin>

            <!-- Surefire for Unit Tests -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <configuration>
                    <!-- Run only unit tests (exclude integration tests) -->
                    <excludes>
                        <exclude>**/*IntegrationTest.java</exclude>
                        <exclude>**/*IT.java</exclude>
                    </excludes>
                    <!-- Prevent hanging issues in CI -->
                    <forkedProcessTimeoutInSeconds>60</forkedProcessTimeoutInSeconds>
                    <forkedProcessExitTimeoutInSeconds>30</forkedProcessExitTimeoutInSeconds>
                </configuration>
            </plugin>
            <!-- Failsafe for Integration Tests -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-failsafe-plugin</artifactId>
                <configuration>
                    <!-- Run only integration tests -->
                    <includes>
                        <include>**/*IntegrationTest.java</include>
                        <include>**/*IT.java</include>
                    </includes>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>integration-test</goal>
                            <goal>verify</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <!-- Spotify fmt plugin for Google Java Format -->
            <plugin>
                <groupId>com.spotify.fmt</groupId>
                <artifactId>fmt-maven-plugin</artifactId>
                <version>${fmt-maven-plugin.version}</version>
                <configuration>
                    <!-- Format source and test files -->
                    <sourceDirectory>src/main/java</sourceDirectory>
                    <testSourceDirectory>src/test/java</testSourceDirectory>
                    <!-- Use Google Java Format style -->
                    <style>google</style>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>check</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
    <repositories>
        <repository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
            <releases>
                <enabled>false</enabled>
            </releases>
        </repository>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>

</project>
```

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

```java
package com.arvindand.mcp.maven.util;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import com.arvindand.mcp.maven.model.MavenCoordinate;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;

/**
 * Comprehensive unit tests for MavenCoordinateParser.
 *
 * @author Arvind Menon
 * @since 0.1.0
 */
class MavenCoordinateParserTest {

  /** Test data for valid coordinate parsing. */
  private static Stream<Arguments> validCoordinateTestData() {
    return Stream.of(
        // Basic groupId:artifactId
        Arguments.of(
            "org.springframework:spring-core",
            "org.springframework",
            "spring-core",
            null,
            null,
            null),

        // With version
        Arguments.of(
            "org.springframework:spring-core:6.1.4",
            "org.springframework",
            "spring-core",
            "6.1.4",
            null,
            null),

        // With version and packaging
        Arguments.of(
            "org.springframework:spring-core:6.1.4:jar",
            "org.springframework",
            "spring-core",
            "6.1.4",
            "jar",
            null),

        // With version, packaging, and classifier
        Arguments.of(
            "org.springframework:spring-core:6.1.4:jar:sources",
            "org.springframework",
            "spring-core",
            "6.1.4",
            "jar",
            "sources"),

        // POM packaging
        Arguments.of(
            "org.springframework.boot:spring-boot-starter-parent:3.2.0:pom",
            "org.springframework.boot",
            "spring-boot-starter-parent",
            "3.2.0",
            "pom",
            null),

        // With classifier but no explicit packaging (empty packaging becomes null)
        Arguments.of(
            "org.springframework:spring-core:6.1.4::sources",
            "org.springframework",
            "spring-core",
            "6.1.4",
            null,
            "sources"),

        // Complex version strings
        Arguments.of(
            "com.example:test-artifact:1.0.0-RC1",
            "com.example",
            "test-artifact",
            "1.0.0-RC1",
            null,
            null),

        // Long groupId with many segments
        Arguments.of(
            "com.company.department.team:artifact-name:2.5.1",
            "com.company.department.team",
            "artifact-name",
            "2.5.1",
            null,
            null),

        // Artifact with numbers and hyphens
        Arguments.of(
            "org.apache.commons:commons-lang3:3.12.0",
            "org.apache.commons",
            "commons-lang3",
            "3.12.0",
            null,
            null),

        // Snapshot version
        Arguments.of(
            "com.example:test:1.0.0-SNAPSHOT:jar",
            "com.example",
            "test",
            "1.0.0-SNAPSHOT",
            "jar",
            null));
  }

  @ParameterizedTest(name = "Parse {0}")
  @MethodSource("validCoordinateTestData")
  void testParse_ValidCoordinates(
      String coordinateString,
      String expectedGroupId,
      String expectedArtifactId,
      String expectedVersion,
      String expectedPackaging,
      String expectedClassifier) {

    // When
    MavenCoordinate result = MavenCoordinateParser.parse(coordinateString);

    // Then
    assertThat(result.groupId()).isEqualTo(expectedGroupId);
    assertThat(result.artifactId()).isEqualTo(expectedArtifactId);
    assertThat(result.version()).isEqualTo(expectedVersion);
    assertThat(result.packaging()).isEqualTo(expectedPackaging);
    assertThat(result.classifier()).isEqualTo(expectedClassifier);
  }

  @ParameterizedTest
  @ValueSource(
      strings = {
        "invalid", // Too few parts
        ":", // Empty parts
        ":::", // Only separators
        "group:", // Missing artifactId
        ":artifact", // Missing groupId
        "", // Empty string
        "   ", // Whitespace only
        "group:artifact:version:packaging:classifier:extra", // Too many parts
        "group::version", // Missing artifactId (empty)
        ":artifact:version" // Missing groupId (empty)
      })
  void testParse_InvalidCoordinates(String invalidCoordinate) {
    // When & Then - Expect either invalid format or empty groupId/artifactId messages
    assertThatThrownBy(() -> MavenCoordinateParser.parse(invalidCoordinate))
        .isInstanceOf(IllegalArgumentException.class)
        .hasMessageMatching(
            ".*(Invalid Maven coordinate format|Dependency string cannot be null or empty|GroupId and artifactId cannot be empty).*");
  }

  @Test
  void testParse_NullInput() {
    // When & Then
    assertThatThrownBy(() -> MavenCoordinateParser.parse(null))
        .isInstanceOf(IllegalArgumentException.class)
        .hasMessageContaining("Dependency string cannot be null or empty");
  }

  @Test
  void testParse_WithWhitespace() {
    // Given
    String coordinateWithSpaces = "  org.springframework : spring-core : 6.1.4  ";

    // When
    MavenCoordinate result = MavenCoordinateParser.parse(coordinateWithSpaces);

    // Then - Should trim whitespace from each part
    assertThat(result.groupId()).isEqualTo("org.springframework");
    assertThat(result.artifactId()).isEqualTo("spring-core");
    assertThat(result.version()).isEqualTo("6.1.4");
  }

  @Test
  void testParse_EmptyVersionString() {
    // Given - Empty version part
    String coordinate = "org.springframework:spring-core:";

    // When
    MavenCoordinate result = MavenCoordinateParser.parse(coordinate);

    // Then - Empty version should be converted to null
    assertThat(result.groupId()).isEqualTo("org.springframework");
    assertThat(result.artifactId()).isEqualTo("spring-core");
    assertThat(result.version()).isNull();
  }

  @Test
  void testParse_EmptyPackagingString() {
    // Given - Empty packaging part
    String coordinate = "org.springframework:spring-core:6.1.4:";

    // When
    MavenCoordinate result = MavenCoordinateParser.parse(coordinate);

    // Then - Empty packaging should be converted to null
    assertThat(result.groupId()).isEqualTo("org.springframework");
    assertThat(result.artifactId()).isEqualTo("spring-core");
    assertThat(result.version()).isEqualTo("6.1.4");
    assertThat(result.packaging()).isNull();
  }

  @Test
  void testParse_EmptyClassifierString() {
    // Given - Empty classifier part
    String coordinate = "org.springframework:spring-core:6.1.4:jar:";

    // When
    MavenCoordinate result = MavenCoordinateParser.parse(coordinate);

    // Then - Empty classifier should be converted to null
    assertThat(result.groupId()).isEqualTo("org.springframework");
    assertThat(result.artifactId()).isEqualTo("spring-core");
    assertThat(result.version()).isEqualTo("6.1.4");
    assertThat(result.packaging()).isEqualTo("jar");
    assertThat(result.classifier()).isNull();
  }

  @Test
  void testParse_SpecialCharactersInNames() {
    // Given - Coordinates with special characters that should be valid
    String coordinate = "com.example-company:my-artifact_name:1.0.0-beta.1:jar";

    // When
    MavenCoordinate result = MavenCoordinateParser.parse(coordinate);

    // Then
    assertThat(result.groupId()).isEqualTo("com.example-company");
    assertThat(result.artifactId()).isEqualTo("my-artifact_name");
    assertThat(result.version()).isEqualTo("1.0.0-beta.1");
    assertThat(result.packaging()).isEqualTo("jar");
  }

  @Test
  void testParse_NumericVersions() {
    // Given - Various numeric version formats
    String coordinate1 = "com.example:test:1:jar";
    String coordinate2 = "com.example:test:1.0:jar";
    String coordinate3 = "com.example:test:1.0.0:jar";

    // When
    MavenCoordinate result1 = MavenCoordinateParser.parse(coordinate1);
    MavenCoordinate result2 = MavenCoordinateParser.parse(coordinate2);
    MavenCoordinate result3 = MavenCoordinateParser.parse(coordinate3);

    // Then
    assertThat(result1.version()).isEqualTo("1");
    assertThat(result2.version()).isEqualTo("1.0");
    assertThat(result3.version()).isEqualTo("1.0.0");
  }

  @Test
  void testParse_ConsistencyWithToCoordinateString() {
    // Given - A coordinate string
    String originalCoordinate = "org.springframework:spring-core:6.1.4:jar:sources";

    // When - Parse and convert back to string
    MavenCoordinate parsed = MavenCoordinateParser.parse(originalCoordinate);
    String reconstructed = parsed.toCoordinateString();

    // Then - Should be identical
    assertThat(reconstructed).isEqualTo(originalCoordinate);
  }

  @Test
  void testParse_MinimalCoordinate() {
    // Given - Minimal valid coordinate
    String coordinate = "g:a";

    // When
    MavenCoordinate result = MavenCoordinateParser.parse(coordinate);

    // Then
    assertThat(result.groupId()).isEqualTo("g");
    assertThat(result.artifactId()).isEqualTo("a");
    assertThat(result.version()).isNull();
    assertThat(result.packaging()).isNull();
    assertThat(result.classifier()).isNull();
  }

  @Test
  void testParse_RealWorldExamples() {
    // Test with real-world Maven coordinates

    // Spring Boot Starter
    MavenCoordinate springBoot =
        MavenCoordinateParser.parse("org.springframework.boot:spring-boot-starter:3.2.0");
    assertThat(springBoot.groupId()).isEqualTo("org.springframework.boot");
    assertThat(springBoot.artifactId()).isEqualTo("spring-boot-starter");
    assertThat(springBoot.version()).isEqualTo("3.2.0");

    // Jackson with classifier
    MavenCoordinate jackson =
        MavenCoordinateParser.parse("com.fasterxml.jackson.core:jackson-core:2.15.2:jar:sources");
    assertThat(jackson.groupId()).isEqualTo("com.fasterxml.jackson.core");
    assertThat(jackson.artifactId()).isEqualTo("jackson-core");
    assertThat(jackson.version()).isEqualTo("2.15.2");
    assertThat(jackson.packaging()).isEqualTo("jar");
    assertThat(jackson.classifier()).isEqualTo("sources");

    // Apache Commons
    MavenCoordinate commons =
        MavenCoordinateParser.parse("org.apache.commons:commons-lang3:3.12.0");
    assertThat(commons.groupId()).isEqualTo("org.apache.commons");
    assertThat(commons.artifactId()).isEqualTo("commons-lang3");
    assertThat(commons.version()).isEqualTo("3.12.0");
  }

  @Test
  void testParse_EdgeCaseVersionFormats() {
    // Test various version formats that should be valid
    String[] validVersions = {
      "1",
      "1.0",
      "1.0.0",
      "1.0.0-SNAPSHOT",
      "1.0.0-RC1",
      "1.0.0.RELEASE",
      "1.0.0.Final",
      "2021.0.0",
      "1.0.0-alpha.1",
      "1.0.0+build.1"
    };

    for (String version : validVersions) {
      String coordinate = "com.example:test:" + version;
      MavenCoordinate result = MavenCoordinateParser.parse(coordinate);
      assertThat(result.version()).isEqualTo(version);
    }
  }
}

```
Page 1/2FirstPrevNextLast