From 64d3ef26cb8ae5a59c3c24554d33be24f98fd111 Mon Sep 17 00:00:00 2001 From: adityamparikh Date: Sun, 26 Oct 2025 20:18:37 -0400 Subject: [PATCH 1/6] Add Docker support with Jib and GitHub Actions CI/CD --- .github/workflows/build-and-publish.yml | 309 ++++++++++++++++++++++++ README.md | 273 ++++++++++++++++++++- build.gradle.kts | 126 ++++++++++ gradle/libs.versions.toml | 4 +- 4 files changed, 704 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/build-and-publish.yml diff --git a/.github/workflows/build-and-publish.yml b/.github/workflows/build-and-publish.yml new file mode 100644 index 0000000..c9e805b --- /dev/null +++ b/.github/workflows/build-and-publish.yml @@ -0,0 +1,309 @@ +# GitHub Actions Workflow: Build and Publish +# =========================================== +# +# This workflow builds the Solr MCP Server project and publishes Docker images +# to both GitHub Container Registry (GHCR) and Docker Hub. +# +# Workflow Triggers: +# ------------------ +# 1. Push to 'main' branch - Builds, tests, and publishes Docker images +# 2. Version tags (v*) - Builds and publishes release images with version tags +# 3. Pull requests to 'main' - Only builds and tests (no publishing) +# 4. Manual trigger via workflow_dispatch +# +# Jobs: +# ----- +# 1. build: Compiles the JAR, runs tests, and uploads artifacts +# 2. publish-docker: Publishes multi-platform Docker images using Jib +# +# Published Images: +# ---------------- +# - GitHub Container Registry: ghcr.io/OWNER/solr-mcp-server:TAG +# - Docker Hub: DOCKERHUB_USERNAME/solr-mcp-server:TAG +# +# Image Tagging Strategy: +# ---------------------- +# - Main branch: VERSION-SHORT_SHA (e.g., 0.0.1-SNAPSHOT-a1b2c3d) + latest +# - Version tags: VERSION (e.g., 1.0.0) + latest +# +# Required Secrets (for Docker Hub): +# ---------------------------------- +# - DOCKERHUB_USERNAME: Your Docker Hub username +# - DOCKERHUB_TOKEN: Docker Hub access token (https://hub.docker.com/settings/security) +# +# Note: GitHub Container Registry uses GITHUB_TOKEN automatically (no setup needed) + +name: Build and Publish + +on: + push: + branches: + - main + tags: + - 'v*' # Trigger on version tags like v1.0.0, v2.1.3, etc. + pull_request: + branches: + - main + workflow_dispatch: # Allow manual workflow runs from GitHub UI + +env: + JAVA_VERSION: '25' + JAVA_DISTRIBUTION: 'temurin' + +jobs: + # ============================================================================ + # Job 1: Build JAR + # ============================================================================ + # This job compiles the project, runs tests, and generates build artifacts. + # It runs on all triggers (push, PR, tags, manual). + # + # Outputs: + # - Spring Boot JAR with all dependencies (fat JAR) + # - Plain JAR without dependencies + # - JUnit test results + # - JaCoCo code coverage reports + # ============================================================================ + build: + name: Build JAR + runs-on: ubuntu-latest + + steps: + # Checkout the repository code + - name: Checkout code + uses: actions/checkout@v4 + + # Set up Java Development Kit + # Uses Temurin (Eclipse Adoptium) distribution of OpenJDK 25 + # Gradle cache is enabled to speed up subsequent builds + - name: Set up JDK ${{ env.JAVA_VERSION }} + uses: actions/setup-java@v4 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: ${{ env.JAVA_DISTRIBUTION }} + cache: 'gradle' + + # Make the Gradle wrapper executable + # Required on Unix-based systems (Linux, macOS) + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + # Build the project with Gradle + # This runs: compilation, tests, spotless formatting, error-prone checks, + # JaCoCo coverage, and creates the JAR files + - name: Build with Gradle + run: ./gradlew build + + # Upload the compiled JAR files as workflow artifacts + # These can be downloaded from the GitHub Actions UI + # Artifacts are retained for 7 days + - name: Upload JAR artifact + uses: actions/upload-artifact@v4 + with: + name: solr-mcp-server-jar + path: build/libs/solr-mcp-server-*.jar + retention-days: 7 + + # Upload JUnit test results + # if: always() ensures this runs even if the build fails + # This allows viewing test results for failed builds + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results + path: build/test-results/ + retention-days: 7 + + # Upload JaCoCo code coverage report + # if: always() ensures this runs even if tests fail + # Coverage reports help identify untested code paths + - name: Upload coverage report + if: always() + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: build/reports/jacoco/ + retention-days: 7 + + # ============================================================================ + # Job 2: Publish Docker Images + # ============================================================================ + # This job builds multi-platform Docker images using Jib and publishes them + # to GitHub Container Registry (GHCR) and Docker Hub. + # + # This job: + # - Only runs after 'build' job succeeds (needs: build) + # - Skips for pull requests (only runs on push to main and tags) + # - Uses Jib to build without requiring Docker daemon + # - Supports multi-platform: linux/amd64 and linux/arm64 + # - Publishes to both GHCR (always) and Docker Hub (if secrets configured) + # + # Security Note: + # - Secrets are passed to Jib CLI arguments for authentication + # - This is required for registry authentication and is handled securely + # - GitHub Actions masks secret values in logs automatically + # ============================================================================ + publish-docker: + name: Publish Docker Images + runs-on: ubuntu-latest + needs: build # Wait for build job to complete successfully + if: github.event_name != 'pull_request' # Skip for PRs + + # Grant permissions for GHCR publishing + # contents:read - Read repository contents + # packages:write - Publish to GitHub Container Registry + permissions: + contents: read + packages: write + + steps: + # Checkout the repository code + - name: Checkout code + uses: actions/checkout@v4 + + # Set up Java for running Jib + # Jib doesn't require Docker but needs Java to run + - name: Set up JDK ${{ env.JAVA_VERSION }} + uses: actions/setup-java@v4 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: ${{ env.JAVA_DISTRIBUTION }} + cache: 'gradle' + + # Make Gradle wrapper executable + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + # Extract version and determine image tags + # Outputs: + # - version: Project version from build.gradle.kts + # - tags: Comma-separated list of Docker tags to apply + # - is_release: Whether this is a release build (from version tag) + - name: Extract metadata + id: meta + run: | + # Get version from build.gradle.kts + VERSION=$(grep '^version = ' build.gradle.kts | sed 's/version = "\(.*\)"/\1/') + echo "version=$VERSION" >> $GITHUB_OUTPUT + + # Determine image tags based on trigger type + if [[ "${{ github.ref }}" == refs/tags/v* ]]; then + # For version tags (e.g., v1.0.0), use semantic version + TAG_VERSION=${GITHUB_REF#refs/tags/v} + echo "tags=$TAG_VERSION,latest" >> $GITHUB_OUTPUT + echo "is_release=true" >> $GITHUB_OUTPUT + else + # For main branch, append short commit SHA for traceability + SHORT_SHA=$(echo ${{ github.sha }} | cut -c1-7) + echo "tags=$VERSION-$SHORT_SHA,latest" >> $GITHUB_OUTPUT + echo "is_release=false" >> $GITHUB_OUTPUT + fi + + # Authenticate to GitHub Container Registry + # Uses built-in GITHUB_TOKEN (no configuration needed) + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Authenticate to Docker Hub + # Requires DOCKERHUB_USERNAME and DOCKERHUB_TOKEN secrets + # This step will fail silently if secrets are not configured + # Create a Docker Hub access token, then add two GitHub Actions secrets named `DOCKERHUB_USERNAME` and `DOCKERHUB_TOKEN`. + # + # Steps (web UI) + # - Create Docker Hub token: + # - Visit `https://hub.docker.com` + # - Account → Settings → Security → New Access Token + # - Copy the generated token (you can’t view it again). + # - Add secrets to the repository: + # - In GitHub, open the repo → `Settings` → `Secrets and variables` → `Actions` → `New repository secret` + # - Add secret `DOCKERHUB_USERNAME` with your Docker Hub username. + # - Add secret `DOCKERHUB_TOKEN` with the token from Docker Hub. + # + # Optional + # - To make secrets available to multiple repos, add them at the organization level: Org → `Settings` → `Secrets and variables` → `Actions`. + # - You can also add environment-level secrets if you use GitHub Environments. + # + # CLI example (GitHub CLI) + # ```bash + # gh secret set DOCKERHUB_USERNAME --body "your-docker-username" + # gh secret set DOCKERHUB_TOKEN --body "your-docker-access-token" + # ``` + # + # Note: `GITHUB_TOKEN` is provided automatically for GHCR; do not store it manually. + # - name: Log in to Docker Hub + # uses: docker/login-action@v3 + # with: + # username: ${{ secrets.DOCKERHUB_USERNAME }} + # password: ${{ secrets.DOCKERHUB_TOKEN }} + + # Convert repository owner to lowercase + # Required because container registry names must be lowercase + # Example: "Apache" -> "apache" + - name: Determine repository owner (lowercase) + id: repo + run: | + echo "owner_lc=$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT + + # Build and publish images to GitHub Container Registry + # Uses Jib Gradle plugin to build multi-platform images + # Jib creates optimized, layered images without Docker daemon + # Each tag is built and pushed separately + - name: Build and publish to GitHub Container Registry + run: | + TAGS="${{ steps.meta.outputs.tags }}" + IFS=',' read -ra TAG_ARRAY <<< "$TAGS" + + # Build and push each tag to GHCR + # Jib automatically handles multi-platform builds (amd64, arm64) + for TAG in "${TAG_ARRAY[@]}"; do + echo "Building and pushing ghcr.io/${{ steps.repo.outputs.owner_lc }}/solr-mcp-server:$TAG" + ./gradlew jib \ + -Djib.to.image=ghcr.io/${{ steps.repo.outputs.owner_lc }}/solr-mcp-server:$TAG \ + -Djib.to.auth.username=${{ github.actor }} \ + -Djib.to.auth.password=${{ secrets.GITHUB_TOKEN }} + done + + # Build and publish images to Docker Hub + # Only runs if Docker Hub secrets are configured + # Gracefully skips if secrets are not available + - name: Build and publish to Docker Hub + if: secrets.DOCKERHUB_USERNAME != '' && secrets.DOCKERHUB_TOKEN != '' + run: | + TAGS="${{ steps.meta.outputs.tags }}" + IFS=',' read -ra TAG_ARRAY <<< "$TAGS" + + # Build and push each tag to Docker Hub + for TAG in "${TAG_ARRAY[@]}"; do + echo "Building and pushing ${{ secrets.DOCKERHUB_USERNAME }}/solr-mcp-server:$TAG" + ./gradlew jib \ + -Djib.to.image=${{ secrets.DOCKERHUB_USERNAME }}/solr-mcp-server:$TAG \ + -Djib.to.auth.username=${{ secrets.DOCKERHUB_USERNAME }} \ + -Djib.to.auth.password=${{ secrets.DOCKERHUB_TOKEN }} + done + + # Create a summary of published images + # Displayed in the GitHub Actions workflow summary page + # Makes it easy to see which images were published and their tags + - name: Summary + run: | + echo "### Docker Images Published :rocket:" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "#### GitHub Container Registry" >> $GITHUB_STEP_SUMMARY + TAGS="${{ steps.meta.outputs.tags }}" + IFS=',' read -ra TAG_ARRAY <<< "$TAGS" + for TAG in "${TAG_ARRAY[@]}"; do + echo "- \`ghcr.io/${{ steps.repo.outputs.owner_lc }}/solr-mcp-server:$TAG\`" >> $GITHUB_STEP_SUMMARY + done + + # Only show Docker Hub section if secrets are configured + if [[ "${{ secrets.DOCKERHUB_USERNAME }}" != "" ]]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "#### Docker Hub" >> $GITHUB_STEP_SUMMARY + for TAG in "${TAG_ARRAY[@]}"; do + echo "- \`${{ secrets.DOCKERHUB_USERNAME }}/solr-mcp-server:$TAG\`" >> $GITHUB_STEP_SUMMARY + done + fi \ No newline at end of file diff --git a/README.md b/README.md index 78188a3..457b243 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,18 @@ +[![Project Status: Incubating](https://img.shields.io/badge/status-incubating-yellow.svg)](https://github.com/apache/solr-mcp) + # Solr MCP Server -A Spring AI Model Context Protocol (MCP) server that provides tools for interacting with Apache Solr. This server enables AI assistants like Claude to search, index, and manage Solr collections through the MCP protocol. +A Spring AI Model Context Protocol (MCP) server that provides tools for interacting with Apache Solr. This server +enables AI assistants like Claude to search, index, and manage Solr collections through the MCP protocol. ## Overview -This project provides a set of tools that allow AI assistants to interact with Apache Solr, a powerful open-source search platform. By implementing the Spring AI MCP protocol, these tools can be used by any MCP-compatible client, including Claude Desktop. The project uses SolrJ, the official Java client for Solr, to communicate with Solr instances. +This project provides a set of tools that allow AI assistants to interact with Apache Solr, a powerful open-source +search platform. By implementing the Spring AI MCP protocol, these tools can be used by any MCP-compatible client, +including Claude Desktop. The project uses SolrJ, the official Java client for Solr, to communicate with Solr instances. The server provides the following capabilities: + - Search Solr collections with advanced query options - Index documents into Solr collections - Manage and monitor Solr collections @@ -43,6 +49,7 @@ docker-compose up -d ``` This will start a Solr instance in SolrCloud mode with ZooKeeper and create two sample collections: + - `books` - A collection with sample book data - `films` - A collection with sample film data @@ -67,6 +74,127 @@ The build produces two JAR files in `build/libs/`: - `solr-mcp-server-0.0.1-SNAPSHOT.jar` - Executable JAR with all dependencies (fat JAR) - `solr-mcp-server-0.0.1-SNAPSHOT-plain.jar` - Plain JAR without dependencies +### 4. Building Docker Images (Optional) + +This project uses [Jib](https://github.com/GoogleContainerTools/jib) to build optimized Docker images without requiring +Docker installed. Jib creates layered images for faster rebuilds and smaller image sizes. + +#### Option 1: Build to Docker Daemon (Recommended) + +Build directly to your local Docker daemon (requires Docker installed): + +```bash +./gradlew jibDockerBuild +``` + +This creates a local Docker image: `solr-mcp-server:0.0.1-SNAPSHOT` + +Verify the image: + +```bash +docker images | grep solr-mcp-server +``` + +#### Option 2: Build to Tar File (No Docker Required) + +Build to a tar file without Docker installed: + +```bash +./gradlew jibBuildTar +``` + +This creates `build/jib-image.tar`. Load it into Docker: + +```bash +docker load < build/jib-image.tar +``` + +#### Option 3: Push to Docker Hub + +Authenticate with Docker Hub and push: + +```bash +# Login to Docker Hub +docker login + +# Build and push +./gradlew jib -Djib.to.image=YOUR_DOCKERHUB_USERNAME/solr-mcp-server:0.0.1-SNAPSHOT +``` + +#### Option 4: Push to GitHub Container Registry + +Authenticate with GitHub Container Registry and push: + +```bash +# Create a Personal Access Token (classic) with write:packages scope at: +# https://github.com/settings/tokens + +# Login to GitHub Container Registry +export GITHUB_TOKEN=YOUR_GITHUB_TOKEN +echo $GITHUB_TOKEN | docker login ghcr.io -u YOUR_GITHUB_USERNAME --password-stdin + +# Build and push +./gradlew jib -Djib.to.image=ghcr.io/YOUR_GITHUB_USERNAME/solr-mcp-server:0.0.1-SNAPSHOT +``` + +#### Multi-Platform Support + +The Docker images are built with multi-platform support for: + +- `linux/amd64` (Intel/AMD 64-bit) +- `linux/arm64` (Apple Silicon M1/M2/M3) + +#### Automated Builds with GitHub Actions + +This project includes a GitHub Actions workflow that automatically builds and publishes Docker images to both GitHub +Container Registry and Docker Hub. + +**Triggers:** + +- Push to `main` branch - Builds and publishes images tagged with `version-SHA` and `latest` +- Version tags (e.g., `v1.0.0`) - Builds and publishes images tagged with the version number and `latest` +- Pull requests - Builds and tests only (no publishing) + +**Published Images:** + +- GitHub Container Registry: `ghcr.io/OWNER/solr-mcp-server:TAG` +- Docker Hub: `DOCKERHUB_USERNAME/solr-mcp-server:TAG` + +**Setup for Docker Hub Publishing:** + +To enable Docker Hub publishing, configure these repository secrets: + +1. Go to your GitHub repository Settings > Secrets and variables > Actions +2. Add the following secrets: + - `DOCKERHUB_USERNAME`: Your Docker Hub username + - `DOCKERHUB_TOKEN`: Docker Hub access token (create at https://hub.docker.com/settings/security) + +**Note:** GitHub Container Registry publishing works automatically using the `GITHUB_TOKEN` provided by GitHub Actions. + +#### Running the Docker Container + +Run the container with STDIO mode: + +```bash +docker run -i --rm solr-mcp-server:0.0.1-SNAPSHOT +``` + +Or with custom Solr URL: + +```bash +docker run -i --rm \ + -e SOLR_URL=http://your-solr-host:8983/solr/ \ + solr-mcp-server:0.0.1-SNAPSHOT +``` + +**Note for Linux users:** If you need to connect to Solr running on the host machine, add the `--add-host` flag: + +```bash +docker run -i --rm \ + --add-host=host.docker.internal:host-gateway \ + solr-mcp-server:0.0.1-SNAPSHOT +``` + ## Project Structure The codebase follows a clean, modular architecture organized by functionality: @@ -108,7 +236,7 @@ src/main/java/org/apache/solr/mcp/server/ - **Configuration**: Spring Boot configuration using properties files - `application.properties` - Default configuration - `application-stdio.properties` - STDIO transport profile - - `application-http.properties` - HTTP transport profile + - `application-http.properties` - HTTP transport profile - **Document Creators**: Strategy pattern implementation for parsing different document formats - Automatically sanitizes field names to comply with Solr schema requirements @@ -194,7 +322,9 @@ Parameters: ## Adding to Claude Desktop -To add this MCP server to Claude Desktop: +You can add this MCP server to Claude Desktop using either the JAR file or Docker container. + +### Option 1: Using JAR File 1. Build the project as a standalone JAR: @@ -220,13 +350,118 @@ To add this MCP server to Claude Desktop: "PROFILES": "stdio" } } - } + } } ``` **Note:** Replace `/absolute/path/to/solr-mcp-server` with the actual path to your project directory. -### 4. Restart Claude Desktop & Invoke +### Option 2: Using Docker Container + +1. Build the Docker image: + +```bash +./gradlew jibDockerBuild +``` + +2. In Claude Desktop, go to Settings > Developer > Edit Config + +3. Add the following configuration to your MCP settings: + +```json +{ + "mcpServers": { + "solr-search-mcp": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "solr-mcp-server:0.0.1-SNAPSHOT" + ], + "env": { + "SOLR_URL": "http://localhost:8983/solr/" + } + } + } +} +``` + +**Note for macOS/Windows users:** Docker Desktop automatically provides `host.docker.internal` for accessing services on +the host machine. The container is pre-configured to use this. + +**Note for Linux users:** You need to add the `--add-host` flag to enable communication with services running on the +host: + +```json +{ + "mcpServers": { + "solr-search-mcp": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "--add-host=host.docker.internal:host-gateway", + "solr-mcp-server:0.0.1-SNAPSHOT" + ], + "env": { + "SOLR_URL": "http://host.docker.internal:8983/solr/" + } + } + } +} +``` + +### Using a Public Docker Image + +If you've pushed the image to Docker Hub or GitHub Container Registry, you can use it directly: + +#### Docker Hub + +```json +{ + "mcpServers": { + "solr-search-mcp": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "YOUR_DOCKERHUB_USERNAME/solr-mcp-server:0.0.1-SNAPSHOT" + ], + "env": { + "SOLR_URL": "http://localhost:8983/solr/" + } + } + } +} +``` + +#### GitHub Container Registry + +```json +{ + "mcpServers": { + "solr-search-mcp": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "ghcr.io/YOUR_GITHUB_USERNAME/solr-mcp-server:0.0.1-SNAPSHOT" + ], + "env": { + "SOLR_URL": "http://localhost:8983/solr/" + } + } + } +} +``` + +### Restart Claude Desktop & Invoke + +After configuring, restart Claude Desktop to load the MCP server. ![claude-stdio.png](images/claude-stdio.png) @@ -440,12 +675,36 @@ controls: If you encounter issues: -1. Ensure Solr is running and accessible. By default, the server connects to http://localhost:8983/solr/, but you can set the `SOLR_URL` environment variable to point to a different Solr instance. +1. Ensure Solr is running and accessible. By default, the server connects to http://localhost:8983/solr/, but you can + set the `SOLR_URL` environment variable to point to a different Solr instance. 2. Check the logs for any error messages 3. Verify that the collections exist using the Solr Admin UI 4. If using HTTP mode, ensure the server is running on the expected port (default: 8080) 5. For STDIO mode with Claude Desktop, verify the JAR path is absolute and correct in the configuration +## FAQ + +### Why use Jib instead of Spring Boot Buildpacks? + +This project uses [Jib](https://github.com/GoogleContainerTools/jib) for building Docker images instead of Spring Boot +Buildpacks for a critical compatibility reason: + +**STDIO Mode Compatibility**: Docker images built with Spring Boot Buildpacks were outputting logs and diagnostic +information to stdout, which interfered with the MCP protocol's STDIO transport. The MCP protocol requires a clean +stdout channel for protocol messages - any extraneous output causes connection errors and prevents the server from +working properly with MCP clients like Claude Desktop. + +Jib provides additional benefits: + +- **Clean stdout**: Jib-built images don't pollute stdout with build information or runtime logs +- **No Docker daemon required**: Jib can build images without Docker installed +- **Faster builds**: Layered image building with better caching +- **Smaller images**: More efficient layer organization +- **Multi-platform support**: Easy cross-platform image building for amd64 and arm64 + +If you're building an MCP server with Docker support, ensure your containerization approach maintains a clean stdout +channel when running in STDIO mode. + ## License This project is licensed under the Apache License 2.0. diff --git a/build.gradle.kts b/build.gradle.kts index 96fd723..d0a768b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,6 +6,7 @@ plugins { alias(libs.plugins.spring.dependency.management) jacoco alias(libs.plugins.errorprone) + alias(libs.plugins.jib) } group = "org.apache.solr" @@ -77,4 +78,129 @@ tasks.withType().configureEach { option("NullAway:OnlyNullMarked", "true") // Enable nullness checks only in null-marked code error("NullAway") // bump checks from warnings (default) to errors } +} + +// Jib Plugin Configuration +// ========================= +// Jib is a Gradle plugin that builds optimized Docker images without requiring Docker installed. +// It creates layered images for faster rebuilds and smaller image sizes. +// +// Key features: +// - Multi-platform support (amd64 and arm64) +// - No Docker daemon required +// - Reproducible builds +// - Optimized layering for faster deployments +// +// Building Images: +// ---------------- +// 1. Build to Docker daemon (requires Docker installed): +// ./gradlew jibDockerBuild +// Creates image: solr-mcp-server:0.0.1-SNAPSHOT +// +// 2. Build to local tar file (no Docker required): +// ./gradlew jibBuildTar +// Creates: build/jib-image.tar +// Load with: docker load < build/jib-image.tar +// +// 3. Push to Docker Hub (requires authentication): +// docker login +// ./gradlew jib -Djib.to.image=dockerhub-username/solr-mcp-server:0.0.1-SNAPSHOT +// +// 4. Push to GitHub Container Registry (requires authentication): +// echo $GITHUB_TOKEN | docker login ghcr.io -u GITHUB_USERNAME --password-stdin +// ./gradlew jib -Djib.to.image=ghcr.io/github-username/solr-mcp-server:0.0.1-SNAPSHOT +// +// Authentication: +// --------------- +// For Docker Hub: +// docker login +// +// For GitHub Container Registry: +// Create a Personal Access Token (classic) with write:packages scope at: +// https://github.com/settings/tokens +// Then authenticate: +// echo YOUR_TOKEN | docker login ghcr.io -u YOUR_USERNAME --password-stdin +// +// Alternative: Set credentials in ~/.gradle/gradle.properties: +// jib.to.auth.username=YOUR_USERNAME +// jib.to.auth.password=YOUR_TOKEN_OR_PASSWORD +// +// Environment Variables: +// ---------------------- +// The container is pre-configured with: +// - SPRING_DOCKER_COMPOSE_ENABLED=false (Docker Compose disabled in container) +// - SOLR_URL=http://host.docker.internal:8983/solr/ (default Solr connection) +// +// These can be overridden at runtime: +// docker run -e SOLR_URL=http://custom-solr:8983/solr/ solr-mcp-server:0.0.1-SNAPSHOT +jib { + from { + // Use Eclipse Temurin JRE 25 as the base image + // Temurin is the open-source build of OpenJDK from Adoptium + image = "eclipse-temurin:25-jre" + + // Multi-platform support for both AMD64 and ARM64 architectures + // This allows the image to run on x86_64 machines and Apple Silicon (M1/M2/M3) + platforms { + platform { + architecture = "amd64" + os = "linux" + } + platform { + architecture = "arm64" + os = "linux" + } + } + } + + to { + // Default image name (can be overridden with -Djib.to.image=...) + // Format: repository/image-name:tag + image = "solr-mcp-server:${version}" + + // Tags to apply to the image + // The version tag is applied by default, plus "latest" tag + tags = setOf("latest") + } + + container { + // Container environment variables + // These are baked into the image but can be overridden at runtime + environment = mapOf( + // Disable Spring Boot Docker Compose support when running in container + "SPRING_DOCKER_COMPOSE_ENABLED" to "false", + + // Default Solr URL using host.docker.internal to reach host machine + // On Linux, use --add-host=host.docker.internal:host-gateway + "SOLR_URL" to "http://host.docker.internal:8983/solr/" + ) + + // JVM flags for containerized environments + // These optimize the JVM for running in containers + jvmFlags = listOf( + // Use container-aware memory settings + "-XX:+UseContainerSupport", + // Set max RAM percentage (default 75%) + "-XX:MaxRAMPercentage=75.0" + ) + + // Main class to run (auto-detected from Spring Boot plugin) + // mainClass is automatically set by Spring Boot Gradle plugin + + // Port exposures (for documentation purposes) + // The application doesn't expose ports by default (STDIO mode) + // If running in HTTP mode, the port would be 8080 + ports = listOf("8080") + + // Labels for image metadata + labels.set( + mapOf( + "org.opencontainers.image.title" to "Solr MCP Server", + "org.opencontainers.image.description" to "Spring AI MCP Server for Apache Solr", + "org.opencontainers.image.version" to version.toString(), + "org.opencontainers.image.vendor" to "Apache Software Foundation", + "org.opencontainers.image.licenses" to "Apache-2.0" + ) + ) + } } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index da81ac9..b1a353d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,6 +3,7 @@ spring-boot = "3.5.6" spring-dependency-management = "1.1.7" errorprone-plugin = "4.2.0" +jib = "3.4.5" # Main dependencies spring-ai = "1.1.0-M3" @@ -79,4 +80,5 @@ errorprone = [ [plugins] spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" } spring-dependency-management = { id = "io.spring.dependency-management", version.ref = "spring-dependency-management" } -errorprone = { id = "net.ltgt.errorprone", version.ref = "errorprone-plugin" } \ No newline at end of file +errorprone = { id = "net.ltgt.errorprone", version.ref = "errorprone-plugin" } +jib = { id = "com.google.cloud.tools.jib", version.ref = "jib" } \ No newline at end of file From b7fa39311c2a5daad4584eb590365e49862f8226 Mon Sep 17 00:00:00 2001 From: adityamparikh Date: Sun, 26 Oct 2025 20:18:37 -0400 Subject: [PATCH 2/6] Add Docker support with Jib and GitHub Actions CI/CD # Conflicts: # build.gradle.kts # gradle/libs.versions.toml --- .github/workflows/build-and-publish.yml | 309 +++++++++++++++++++++++ .github/workflows/claude-code-review.yml | 38 --- .github/workflows/claude.yml | 36 --- README.md | 273 +++++++++++++++++++- build.gradle.kts | 127 ++++++++++ gradle/libs.versions.toml | 2 + 6 files changed, 704 insertions(+), 81 deletions(-) create mode 100644 .github/workflows/build-and-publish.yml delete mode 100644 .github/workflows/claude-code-review.yml delete mode 100644 .github/workflows/claude.yml diff --git a/.github/workflows/build-and-publish.yml b/.github/workflows/build-and-publish.yml new file mode 100644 index 0000000..c9e805b --- /dev/null +++ b/.github/workflows/build-and-publish.yml @@ -0,0 +1,309 @@ +# GitHub Actions Workflow: Build and Publish +# =========================================== +# +# This workflow builds the Solr MCP Server project and publishes Docker images +# to both GitHub Container Registry (GHCR) and Docker Hub. +# +# Workflow Triggers: +# ------------------ +# 1. Push to 'main' branch - Builds, tests, and publishes Docker images +# 2. Version tags (v*) - Builds and publishes release images with version tags +# 3. Pull requests to 'main' - Only builds and tests (no publishing) +# 4. Manual trigger via workflow_dispatch +# +# Jobs: +# ----- +# 1. build: Compiles the JAR, runs tests, and uploads artifacts +# 2. publish-docker: Publishes multi-platform Docker images using Jib +# +# Published Images: +# ---------------- +# - GitHub Container Registry: ghcr.io/OWNER/solr-mcp-server:TAG +# - Docker Hub: DOCKERHUB_USERNAME/solr-mcp-server:TAG +# +# Image Tagging Strategy: +# ---------------------- +# - Main branch: VERSION-SHORT_SHA (e.g., 0.0.1-SNAPSHOT-a1b2c3d) + latest +# - Version tags: VERSION (e.g., 1.0.0) + latest +# +# Required Secrets (for Docker Hub): +# ---------------------------------- +# - DOCKERHUB_USERNAME: Your Docker Hub username +# - DOCKERHUB_TOKEN: Docker Hub access token (https://hub.docker.com/settings/security) +# +# Note: GitHub Container Registry uses GITHUB_TOKEN automatically (no setup needed) + +name: Build and Publish + +on: + push: + branches: + - main + tags: + - 'v*' # Trigger on version tags like v1.0.0, v2.1.3, etc. + pull_request: + branches: + - main + workflow_dispatch: # Allow manual workflow runs from GitHub UI + +env: + JAVA_VERSION: '25' + JAVA_DISTRIBUTION: 'temurin' + +jobs: + # ============================================================================ + # Job 1: Build JAR + # ============================================================================ + # This job compiles the project, runs tests, and generates build artifacts. + # It runs on all triggers (push, PR, tags, manual). + # + # Outputs: + # - Spring Boot JAR with all dependencies (fat JAR) + # - Plain JAR without dependencies + # - JUnit test results + # - JaCoCo code coverage reports + # ============================================================================ + build: + name: Build JAR + runs-on: ubuntu-latest + + steps: + # Checkout the repository code + - name: Checkout code + uses: actions/checkout@v4 + + # Set up Java Development Kit + # Uses Temurin (Eclipse Adoptium) distribution of OpenJDK 25 + # Gradle cache is enabled to speed up subsequent builds + - name: Set up JDK ${{ env.JAVA_VERSION }} + uses: actions/setup-java@v4 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: ${{ env.JAVA_DISTRIBUTION }} + cache: 'gradle' + + # Make the Gradle wrapper executable + # Required on Unix-based systems (Linux, macOS) + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + # Build the project with Gradle + # This runs: compilation, tests, spotless formatting, error-prone checks, + # JaCoCo coverage, and creates the JAR files + - name: Build with Gradle + run: ./gradlew build + + # Upload the compiled JAR files as workflow artifacts + # These can be downloaded from the GitHub Actions UI + # Artifacts are retained for 7 days + - name: Upload JAR artifact + uses: actions/upload-artifact@v4 + with: + name: solr-mcp-server-jar + path: build/libs/solr-mcp-server-*.jar + retention-days: 7 + + # Upload JUnit test results + # if: always() ensures this runs even if the build fails + # This allows viewing test results for failed builds + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results + path: build/test-results/ + retention-days: 7 + + # Upload JaCoCo code coverage report + # if: always() ensures this runs even if tests fail + # Coverage reports help identify untested code paths + - name: Upload coverage report + if: always() + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: build/reports/jacoco/ + retention-days: 7 + + # ============================================================================ + # Job 2: Publish Docker Images + # ============================================================================ + # This job builds multi-platform Docker images using Jib and publishes them + # to GitHub Container Registry (GHCR) and Docker Hub. + # + # This job: + # - Only runs after 'build' job succeeds (needs: build) + # - Skips for pull requests (only runs on push to main and tags) + # - Uses Jib to build without requiring Docker daemon + # - Supports multi-platform: linux/amd64 and linux/arm64 + # - Publishes to both GHCR (always) and Docker Hub (if secrets configured) + # + # Security Note: + # - Secrets are passed to Jib CLI arguments for authentication + # - This is required for registry authentication and is handled securely + # - GitHub Actions masks secret values in logs automatically + # ============================================================================ + publish-docker: + name: Publish Docker Images + runs-on: ubuntu-latest + needs: build # Wait for build job to complete successfully + if: github.event_name != 'pull_request' # Skip for PRs + + # Grant permissions for GHCR publishing + # contents:read - Read repository contents + # packages:write - Publish to GitHub Container Registry + permissions: + contents: read + packages: write + + steps: + # Checkout the repository code + - name: Checkout code + uses: actions/checkout@v4 + + # Set up Java for running Jib + # Jib doesn't require Docker but needs Java to run + - name: Set up JDK ${{ env.JAVA_VERSION }} + uses: actions/setup-java@v4 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: ${{ env.JAVA_DISTRIBUTION }} + cache: 'gradle' + + # Make Gradle wrapper executable + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + # Extract version and determine image tags + # Outputs: + # - version: Project version from build.gradle.kts + # - tags: Comma-separated list of Docker tags to apply + # - is_release: Whether this is a release build (from version tag) + - name: Extract metadata + id: meta + run: | + # Get version from build.gradle.kts + VERSION=$(grep '^version = ' build.gradle.kts | sed 's/version = "\(.*\)"/\1/') + echo "version=$VERSION" >> $GITHUB_OUTPUT + + # Determine image tags based on trigger type + if [[ "${{ github.ref }}" == refs/tags/v* ]]; then + # For version tags (e.g., v1.0.0), use semantic version + TAG_VERSION=${GITHUB_REF#refs/tags/v} + echo "tags=$TAG_VERSION,latest" >> $GITHUB_OUTPUT + echo "is_release=true" >> $GITHUB_OUTPUT + else + # For main branch, append short commit SHA for traceability + SHORT_SHA=$(echo ${{ github.sha }} | cut -c1-7) + echo "tags=$VERSION-$SHORT_SHA,latest" >> $GITHUB_OUTPUT + echo "is_release=false" >> $GITHUB_OUTPUT + fi + + # Authenticate to GitHub Container Registry + # Uses built-in GITHUB_TOKEN (no configuration needed) + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Authenticate to Docker Hub + # Requires DOCKERHUB_USERNAME and DOCKERHUB_TOKEN secrets + # This step will fail silently if secrets are not configured + # Create a Docker Hub access token, then add two GitHub Actions secrets named `DOCKERHUB_USERNAME` and `DOCKERHUB_TOKEN`. + # + # Steps (web UI) + # - Create Docker Hub token: + # - Visit `https://hub.docker.com` + # - Account → Settings → Security → New Access Token + # - Copy the generated token (you can’t view it again). + # - Add secrets to the repository: + # - In GitHub, open the repo → `Settings` → `Secrets and variables` → `Actions` → `New repository secret` + # - Add secret `DOCKERHUB_USERNAME` with your Docker Hub username. + # - Add secret `DOCKERHUB_TOKEN` with the token from Docker Hub. + # + # Optional + # - To make secrets available to multiple repos, add them at the organization level: Org → `Settings` → `Secrets and variables` → `Actions`. + # - You can also add environment-level secrets if you use GitHub Environments. + # + # CLI example (GitHub CLI) + # ```bash + # gh secret set DOCKERHUB_USERNAME --body "your-docker-username" + # gh secret set DOCKERHUB_TOKEN --body "your-docker-access-token" + # ``` + # + # Note: `GITHUB_TOKEN` is provided automatically for GHCR; do not store it manually. + # - name: Log in to Docker Hub + # uses: docker/login-action@v3 + # with: + # username: ${{ secrets.DOCKERHUB_USERNAME }} + # password: ${{ secrets.DOCKERHUB_TOKEN }} + + # Convert repository owner to lowercase + # Required because container registry names must be lowercase + # Example: "Apache" -> "apache" + - name: Determine repository owner (lowercase) + id: repo + run: | + echo "owner_lc=$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT + + # Build and publish images to GitHub Container Registry + # Uses Jib Gradle plugin to build multi-platform images + # Jib creates optimized, layered images without Docker daemon + # Each tag is built and pushed separately + - name: Build and publish to GitHub Container Registry + run: | + TAGS="${{ steps.meta.outputs.tags }}" + IFS=',' read -ra TAG_ARRAY <<< "$TAGS" + + # Build and push each tag to GHCR + # Jib automatically handles multi-platform builds (amd64, arm64) + for TAG in "${TAG_ARRAY[@]}"; do + echo "Building and pushing ghcr.io/${{ steps.repo.outputs.owner_lc }}/solr-mcp-server:$TAG" + ./gradlew jib \ + -Djib.to.image=ghcr.io/${{ steps.repo.outputs.owner_lc }}/solr-mcp-server:$TAG \ + -Djib.to.auth.username=${{ github.actor }} \ + -Djib.to.auth.password=${{ secrets.GITHUB_TOKEN }} + done + + # Build and publish images to Docker Hub + # Only runs if Docker Hub secrets are configured + # Gracefully skips if secrets are not available + - name: Build and publish to Docker Hub + if: secrets.DOCKERHUB_USERNAME != '' && secrets.DOCKERHUB_TOKEN != '' + run: | + TAGS="${{ steps.meta.outputs.tags }}" + IFS=',' read -ra TAG_ARRAY <<< "$TAGS" + + # Build and push each tag to Docker Hub + for TAG in "${TAG_ARRAY[@]}"; do + echo "Building and pushing ${{ secrets.DOCKERHUB_USERNAME }}/solr-mcp-server:$TAG" + ./gradlew jib \ + -Djib.to.image=${{ secrets.DOCKERHUB_USERNAME }}/solr-mcp-server:$TAG \ + -Djib.to.auth.username=${{ secrets.DOCKERHUB_USERNAME }} \ + -Djib.to.auth.password=${{ secrets.DOCKERHUB_TOKEN }} + done + + # Create a summary of published images + # Displayed in the GitHub Actions workflow summary page + # Makes it easy to see which images were published and their tags + - name: Summary + run: | + echo "### Docker Images Published :rocket:" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "#### GitHub Container Registry" >> $GITHUB_STEP_SUMMARY + TAGS="${{ steps.meta.outputs.tags }}" + IFS=',' read -ra TAG_ARRAY <<< "$TAGS" + for TAG in "${TAG_ARRAY[@]}"; do + echo "- \`ghcr.io/${{ steps.repo.outputs.owner_lc }}/solr-mcp-server:$TAG\`" >> $GITHUB_STEP_SUMMARY + done + + # Only show Docker Hub section if secrets are configured + if [[ "${{ secrets.DOCKERHUB_USERNAME }}" != "" ]]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "#### Docker Hub" >> $GITHUB_STEP_SUMMARY + for TAG in "${TAG_ARRAY[@]}"; do + echo "- \`${{ secrets.DOCKERHUB_USERNAME }}/solr-mcp-server:$TAG\`" >> $GITHUB_STEP_SUMMARY + done + fi \ No newline at end of file diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml deleted file mode 100644 index 4f75338..0000000 --- a/.github/workflows/claude-code-review.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Claude Auto Review - -on: - pull_request: - types: [opened, synchronize] - -jobs: - auto-review: - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: read - id-token: write - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Automatic PR Review - uses: anthropics/claude-code-action@beta - with: - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - timeout_minutes: "60" - direct_prompt: | - Please review this pull request and provide comprehensive feedback. - - Focus on: - - Code quality and best practices - - Potential bugs or issues - - Performance considerations - - Security implications - - Test coverage - - Documentation updates if needed - - Provide constructive feedback with specific suggestions for improvement. - Use inline comments to highlight specific areas of concern. - # allowed_tools: "mcp__github__create_pending_pull_request_review,mcp__github__add_pull_request_review_comment_to_pending_review,mcp__github__submit_pending_pull_request_review,mcp__github__get_pull_request_diff" \ No newline at end of file diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml deleted file mode 100644 index 71b68ac..0000000 --- a/.github/workflows/claude.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Claude PR Assistant - -on: - issue_comment: - types: [created] - pull_request_review_comment: - types: [created] - issues: - types: [opened, assigned] - pull_request_review: - types: [submitted] - -jobs: - claude-code-action: - if: | - (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || - (github.event_name == 'issues' && contains(github.event.issue.body, '@claude')) - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: read - issues: read - id-token: write - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Run Claude PR Action - uses: anthropics/claude-code-action@beta - with: - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - timeout_minutes: "60" \ No newline at end of file diff --git a/README.md b/README.md index 78188a3..457b243 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,18 @@ +[![Project Status: Incubating](https://img.shields.io/badge/status-incubating-yellow.svg)](https://github.com/apache/solr-mcp) + # Solr MCP Server -A Spring AI Model Context Protocol (MCP) server that provides tools for interacting with Apache Solr. This server enables AI assistants like Claude to search, index, and manage Solr collections through the MCP protocol. +A Spring AI Model Context Protocol (MCP) server that provides tools for interacting with Apache Solr. This server +enables AI assistants like Claude to search, index, and manage Solr collections through the MCP protocol. ## Overview -This project provides a set of tools that allow AI assistants to interact with Apache Solr, a powerful open-source search platform. By implementing the Spring AI MCP protocol, these tools can be used by any MCP-compatible client, including Claude Desktop. The project uses SolrJ, the official Java client for Solr, to communicate with Solr instances. +This project provides a set of tools that allow AI assistants to interact with Apache Solr, a powerful open-source +search platform. By implementing the Spring AI MCP protocol, these tools can be used by any MCP-compatible client, +including Claude Desktop. The project uses SolrJ, the official Java client for Solr, to communicate with Solr instances. The server provides the following capabilities: + - Search Solr collections with advanced query options - Index documents into Solr collections - Manage and monitor Solr collections @@ -43,6 +49,7 @@ docker-compose up -d ``` This will start a Solr instance in SolrCloud mode with ZooKeeper and create two sample collections: + - `books` - A collection with sample book data - `films` - A collection with sample film data @@ -67,6 +74,127 @@ The build produces two JAR files in `build/libs/`: - `solr-mcp-server-0.0.1-SNAPSHOT.jar` - Executable JAR with all dependencies (fat JAR) - `solr-mcp-server-0.0.1-SNAPSHOT-plain.jar` - Plain JAR without dependencies +### 4. Building Docker Images (Optional) + +This project uses [Jib](https://github.com/GoogleContainerTools/jib) to build optimized Docker images without requiring +Docker installed. Jib creates layered images for faster rebuilds and smaller image sizes. + +#### Option 1: Build to Docker Daemon (Recommended) + +Build directly to your local Docker daemon (requires Docker installed): + +```bash +./gradlew jibDockerBuild +``` + +This creates a local Docker image: `solr-mcp-server:0.0.1-SNAPSHOT` + +Verify the image: + +```bash +docker images | grep solr-mcp-server +``` + +#### Option 2: Build to Tar File (No Docker Required) + +Build to a tar file without Docker installed: + +```bash +./gradlew jibBuildTar +``` + +This creates `build/jib-image.tar`. Load it into Docker: + +```bash +docker load < build/jib-image.tar +``` + +#### Option 3: Push to Docker Hub + +Authenticate with Docker Hub and push: + +```bash +# Login to Docker Hub +docker login + +# Build and push +./gradlew jib -Djib.to.image=YOUR_DOCKERHUB_USERNAME/solr-mcp-server:0.0.1-SNAPSHOT +``` + +#### Option 4: Push to GitHub Container Registry + +Authenticate with GitHub Container Registry and push: + +```bash +# Create a Personal Access Token (classic) with write:packages scope at: +# https://github.com/settings/tokens + +# Login to GitHub Container Registry +export GITHUB_TOKEN=YOUR_GITHUB_TOKEN +echo $GITHUB_TOKEN | docker login ghcr.io -u YOUR_GITHUB_USERNAME --password-stdin + +# Build and push +./gradlew jib -Djib.to.image=ghcr.io/YOUR_GITHUB_USERNAME/solr-mcp-server:0.0.1-SNAPSHOT +``` + +#### Multi-Platform Support + +The Docker images are built with multi-platform support for: + +- `linux/amd64` (Intel/AMD 64-bit) +- `linux/arm64` (Apple Silicon M1/M2/M3) + +#### Automated Builds with GitHub Actions + +This project includes a GitHub Actions workflow that automatically builds and publishes Docker images to both GitHub +Container Registry and Docker Hub. + +**Triggers:** + +- Push to `main` branch - Builds and publishes images tagged with `version-SHA` and `latest` +- Version tags (e.g., `v1.0.0`) - Builds and publishes images tagged with the version number and `latest` +- Pull requests - Builds and tests only (no publishing) + +**Published Images:** + +- GitHub Container Registry: `ghcr.io/OWNER/solr-mcp-server:TAG` +- Docker Hub: `DOCKERHUB_USERNAME/solr-mcp-server:TAG` + +**Setup for Docker Hub Publishing:** + +To enable Docker Hub publishing, configure these repository secrets: + +1. Go to your GitHub repository Settings > Secrets and variables > Actions +2. Add the following secrets: + - `DOCKERHUB_USERNAME`: Your Docker Hub username + - `DOCKERHUB_TOKEN`: Docker Hub access token (create at https://hub.docker.com/settings/security) + +**Note:** GitHub Container Registry publishing works automatically using the `GITHUB_TOKEN` provided by GitHub Actions. + +#### Running the Docker Container + +Run the container with STDIO mode: + +```bash +docker run -i --rm solr-mcp-server:0.0.1-SNAPSHOT +``` + +Or with custom Solr URL: + +```bash +docker run -i --rm \ + -e SOLR_URL=http://your-solr-host:8983/solr/ \ + solr-mcp-server:0.0.1-SNAPSHOT +``` + +**Note for Linux users:** If you need to connect to Solr running on the host machine, add the `--add-host` flag: + +```bash +docker run -i --rm \ + --add-host=host.docker.internal:host-gateway \ + solr-mcp-server:0.0.1-SNAPSHOT +``` + ## Project Structure The codebase follows a clean, modular architecture organized by functionality: @@ -108,7 +236,7 @@ src/main/java/org/apache/solr/mcp/server/ - **Configuration**: Spring Boot configuration using properties files - `application.properties` - Default configuration - `application-stdio.properties` - STDIO transport profile - - `application-http.properties` - HTTP transport profile + - `application-http.properties` - HTTP transport profile - **Document Creators**: Strategy pattern implementation for parsing different document formats - Automatically sanitizes field names to comply with Solr schema requirements @@ -194,7 +322,9 @@ Parameters: ## Adding to Claude Desktop -To add this MCP server to Claude Desktop: +You can add this MCP server to Claude Desktop using either the JAR file or Docker container. + +### Option 1: Using JAR File 1. Build the project as a standalone JAR: @@ -220,13 +350,118 @@ To add this MCP server to Claude Desktop: "PROFILES": "stdio" } } - } + } } ``` **Note:** Replace `/absolute/path/to/solr-mcp-server` with the actual path to your project directory. -### 4. Restart Claude Desktop & Invoke +### Option 2: Using Docker Container + +1. Build the Docker image: + +```bash +./gradlew jibDockerBuild +``` + +2. In Claude Desktop, go to Settings > Developer > Edit Config + +3. Add the following configuration to your MCP settings: + +```json +{ + "mcpServers": { + "solr-search-mcp": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "solr-mcp-server:0.0.1-SNAPSHOT" + ], + "env": { + "SOLR_URL": "http://localhost:8983/solr/" + } + } + } +} +``` + +**Note for macOS/Windows users:** Docker Desktop automatically provides `host.docker.internal` for accessing services on +the host machine. The container is pre-configured to use this. + +**Note for Linux users:** You need to add the `--add-host` flag to enable communication with services running on the +host: + +```json +{ + "mcpServers": { + "solr-search-mcp": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "--add-host=host.docker.internal:host-gateway", + "solr-mcp-server:0.0.1-SNAPSHOT" + ], + "env": { + "SOLR_URL": "http://host.docker.internal:8983/solr/" + } + } + } +} +``` + +### Using a Public Docker Image + +If you've pushed the image to Docker Hub or GitHub Container Registry, you can use it directly: + +#### Docker Hub + +```json +{ + "mcpServers": { + "solr-search-mcp": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "YOUR_DOCKERHUB_USERNAME/solr-mcp-server:0.0.1-SNAPSHOT" + ], + "env": { + "SOLR_URL": "http://localhost:8983/solr/" + } + } + } +} +``` + +#### GitHub Container Registry + +```json +{ + "mcpServers": { + "solr-search-mcp": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "ghcr.io/YOUR_GITHUB_USERNAME/solr-mcp-server:0.0.1-SNAPSHOT" + ], + "env": { + "SOLR_URL": "http://localhost:8983/solr/" + } + } + } +} +``` + +### Restart Claude Desktop & Invoke + +After configuring, restart Claude Desktop to load the MCP server. ![claude-stdio.png](images/claude-stdio.png) @@ -440,12 +675,36 @@ controls: If you encounter issues: -1. Ensure Solr is running and accessible. By default, the server connects to http://localhost:8983/solr/, but you can set the `SOLR_URL` environment variable to point to a different Solr instance. +1. Ensure Solr is running and accessible. By default, the server connects to http://localhost:8983/solr/, but you can + set the `SOLR_URL` environment variable to point to a different Solr instance. 2. Check the logs for any error messages 3. Verify that the collections exist using the Solr Admin UI 4. If using HTTP mode, ensure the server is running on the expected port (default: 8080) 5. For STDIO mode with Claude Desktop, verify the JAR path is absolute and correct in the configuration +## FAQ + +### Why use Jib instead of Spring Boot Buildpacks? + +This project uses [Jib](https://github.com/GoogleContainerTools/jib) for building Docker images instead of Spring Boot +Buildpacks for a critical compatibility reason: + +**STDIO Mode Compatibility**: Docker images built with Spring Boot Buildpacks were outputting logs and diagnostic +information to stdout, which interfered with the MCP protocol's STDIO transport. The MCP protocol requires a clean +stdout channel for protocol messages - any extraneous output causes connection errors and prevents the server from +working properly with MCP clients like Claude Desktop. + +Jib provides additional benefits: + +- **Clean stdout**: Jib-built images don't pollute stdout with build information or runtime logs +- **No Docker daemon required**: Jib can build images without Docker installed +- **Faster builds**: Layered image building with better caching +- **Smaller images**: More efficient layer organization +- **Multi-platform support**: Easy cross-platform image building for amd64 and arm64 + +If you're building an MCP server with Docker support, ensure your containerization approach maintains a clean stdout +channel when running in STDIO mode. + ## License This project is licensed under the Apache License 2.0. diff --git a/build.gradle.kts b/build.gradle.kts index 363308b..3c37af4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,6 +7,7 @@ plugins { jacoco alias(libs.plugins.errorprone) alias(libs.plugins.spotless) + alias(libs.plugins.jib) } group = "org.apache.solr" @@ -99,3 +100,129 @@ spotless { ktlint() } } + +// Jib Plugin Configuration +// ========================= +// Jib is a Gradle plugin that builds optimized Docker images without requiring Docker installed. +// It creates layered images for faster rebuilds and smaller image sizes. +// +// Key features: +// - Multi-platform support (amd64 and arm64) +// - No Docker daemon required +// - Reproducible builds +// - Optimized layering for faster deployments +// +// Building Images: +// ---------------- +// 1. Build to Docker daemon (requires Docker installed): +// ./gradlew jibDockerBuild +// Creates image: solr-mcp-server:0.0.1-SNAPSHOT +// +// 2. Build to local tar file (no Docker required): +// ./gradlew jibBuildTar +// Creates: build/jib-image.tar +// Load with: docker load < build/jib-image.tar +// +// 3. Push to Docker Hub (requires authentication): +// docker login +// ./gradlew jib -Djib.to.image=dockerhub-username/solr-mcp-server:0.0.1-SNAPSHOT +// +// 4. Push to GitHub Container Registry (requires authentication): +// echo $GITHUB_TOKEN | docker login ghcr.io -u GITHUB_USERNAME --password-stdin +// ./gradlew jib -Djib.to.image=ghcr.io/github-username/solr-mcp-server:0.0.1-SNAPSHOT +// +// Authentication: +// --------------- +// For Docker Hub: +// docker login +// +// For GitHub Container Registry: +// Create a Personal Access Token (classic) with write:packages scope at: +// https://github.com/settings/tokens +// Then authenticate: +// echo YOUR_TOKEN | docker login ghcr.io -u YOUR_USERNAME --password-stdin +// +// Alternative: Set credentials in ~/.gradle/gradle.properties: +// jib.to.auth.username=YOUR_USERNAME +// jib.to.auth.password=YOUR_TOKEN_OR_PASSWORD +// +// Environment Variables: +// ---------------------- +// The container is pre-configured with: +// - SPRING_DOCKER_COMPOSE_ENABLED=false (Docker Compose disabled in container) +// - SOLR_URL=http://host.docker.internal:8983/solr/ (default Solr connection) +// +// These can be overridden at runtime: +// docker run -e SOLR_URL=http://custom-solr:8983/solr/ solr-mcp-server:0.0.1-SNAPSHOT +jib { + from { + // Use Eclipse Temurin JRE 25 as the base image + // Temurin is the open-source build of OpenJDK from Adoptium + image = "eclipse-temurin:25-jre" + + // Multi-platform support for both AMD64 and ARM64 architectures + // This allows the image to run on x86_64 machines and Apple Silicon (M1/M2/M3) + platforms { + platform { + architecture = "amd64" + os = "linux" + } + platform { + architecture = "arm64" + os = "linux" + } + } + } + + to { + // Default image name (can be overridden with -Djib.to.image=...) + // Format: repository/image-name:tag + image = "solr-mcp-server:$version" + + // Tags to apply to the image + // The version tag is applied by default, plus "latest" tag + tags = setOf("latest") + } + + container { + // Container environment variables + // These are baked into the image but can be overridden at runtime + environment = + mapOf( + // Disable Spring Boot Docker Compose support when running in container + "SPRING_DOCKER_COMPOSE_ENABLED" to "false", + // Default Solr URL using host.docker.internal to reach host machine + // On Linux, use --add-host=host.docker.internal:host-gateway + "SOLR_URL" to "http://host.docker.internal:8983/solr/", + ) + + // JVM flags for containerized environments + // These optimize the JVM for running in containers + jvmFlags = + listOf( + // Use container-aware memory settings + "-XX:+UseContainerSupport", + // Set max RAM percentage (default 75%) + "-XX:MaxRAMPercentage=75.0", + ) + + // Main class to run (auto-detected from Spring Boot plugin) + // mainClass is automatically set by Spring Boot Gradle plugin + + // Port exposures (for documentation purposes) + // The application doesn't expose ports by default (STDIO mode) + // If running in HTTP mode, the port would be 8080 + ports = listOf("8080") + + // Labels for image metadata + labels.set( + mapOf( + "org.opencontainers.image.title" to "Solr MCP Server", + "org.opencontainers.image.description" to "Spring AI MCP Server for Apache Solr", + "org.opencontainers.image.version" to version.toString(), + "org.opencontainers.image.vendor" to "Apache Software Foundation", + "org.opencontainers.image.licenses" to "Apache-2.0", + ), + ) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9de2429..f53361d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,6 +3,7 @@ spring-boot = "3.5.6" spring-dependency-management = "1.1.7" errorprone-plugin = "4.2.0" +jib = "3.4.5" spotless = "7.0.2" # Main dependencies @@ -81,4 +82,5 @@ errorprone = [ spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" } spring-dependency-management = { id = "io.spring.dependency-management", version.ref = "spring-dependency-management" } errorprone = { id = "net.ltgt.errorprone", version.ref = "errorprone-plugin" } +jib = { id = "com.google.cloud.tools.jib", version.ref = "jib" } spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } \ No newline at end of file From 1cb62fba6c0716d4677606b0dae0b49fc4d1a26e Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Wed, 29 Oct 2025 10:56:52 -0400 Subject: [PATCH 3/6] Update the repo name pattern. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 457b243..813ebe6 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,8 @@ The server supports two transport modes: ### 1. Clone the repository ```bash -git clone https://github.com/yourusername/solr-mcp-server.git -cd solr-mcp-server +git clone https://github.com/yourusername/solr-mcp.git +cd solr-mcp ``` ### 2. Start Solr using Docker Compose From e841c4c857c5f7fa3294a9e44860280e2014bae9 Mon Sep 17 00:00:00 2001 From: adityamparikh Date: Sun, 26 Oct 2025 20:18:37 -0400 Subject: [PATCH 4/6] Add Docker support with Jib and GitHub Actions CI/CD # Conflicts: # build.gradle.kts # gradle/libs.versions.toml --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f53361d..7e48e57 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ spring-boot = "3.5.6" spring-dependency-management = "1.1.7" errorprone-plugin = "4.2.0" -jib = "3.4.5" +jib = "3.4.7" spotless = "7.0.2" # Main dependencies From 14343b9724ad9ad4abc9d00ffbd6d740ceb323aa Mon Sep 17 00:00:00 2001 From: adityamparikh Date: Thu, 30 Oct 2025 19:58:17 -0400 Subject: [PATCH 5/6] test: add Docker integration tests for MCP server under both STDIO and HTTP modes --- .github/workflows/build-and-publish.yml | 15 ++ .github/workflows/build.yml | 15 ++ build.gradle.kts | 151 ++++++++++- compose.yaml | 15 ++ gradle/libs.versions.toml | 7 +- init-solr.sh | 15 ++ settings.gradle.kts | 17 ++ .../apache/solr/mcp/server/ClientStdio.java | 33 ++- .../DockerImageHttpIntegrationTest.java | 248 ++++++++++++++++++ .../DockerImageStdioIntegrationTest.java | 191 ++++++++++++++ 10 files changed, 690 insertions(+), 17 deletions(-) create mode 100644 src/test/java/org/apache/solr/mcp/server/DockerImageHttpIntegrationTest.java create mode 100644 src/test/java/org/apache/solr/mcp/server/DockerImageStdioIntegrationTest.java diff --git a/.github/workflows/build-and-publish.yml b/.github/workflows/build-and-publish.yml index c9e805b..80a27ee 100644 --- a/.github/workflows/build-and-publish.yml +++ b/.github/workflows/build-and-publish.yml @@ -1,3 +1,18 @@ +# 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. + # GitHub Actions Workflow: Build and Publish # =========================================== # diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f02281a..281bd62 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,3 +1,18 @@ +# 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. + name: SonarQube on: push: diff --git a/build.gradle.kts b/build.gradle.kts index 3c37af4..56b39db 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,20 @@ +/* + * 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. + */ + import net.ltgt.gradle.errorprone.errorprone plugins { @@ -59,9 +76,38 @@ dependencyManagement { } } +// Configures Spring Boot plugin to generate build metadata at build time +// This creates META-INF/build-info.properties containing: +// - build.artifact: The artifact name (e.g., "solr-mcp-server") +// - build.group: The group ID (e.g., "org.apache.solr") +// - build.name: The project name +// - build.version: The version (e.g., "0.0.1-SNAPSHOT") +// - build.time: The timestamp when the build was executed +// +// When it executes: +// - bootBuildInfo task runs before processResources during any build +// - Triggered by: ./gradlew build, bootJar, test, classes, etc. +// - The generated file is included in the JAR's classpath +// - Tests can access it via: getResourceAsStream("/META-INF/build-info.properties") +// +// Use cases: +// - Runtime version introspection via Spring Boot Actuator +// - Dynamic JAR path resolution in tests (e.g., ClientStdio.java) +// - Application metadata exposure through /actuator/info endpoint +springBoot { + buildInfo() +} + tasks.withType { - useJUnitPlatform() - finalizedBy(tasks.jacocoTestReport) + useJUnitPlatform { + // Only exclude docker integration tests from regular test runs, not from dockerIntegrationTest + if (name != "dockerIntegrationTest") { + excludeTags("docker-integration") + } + } + if (name != "dockerIntegrationTest") { + finalizedBy(tasks.jacocoTestReport) + } } tasks.jacocoTestReport { @@ -71,6 +117,19 @@ tasks.jacocoTestReport { html.required.set(true) csv.required.set(false) } + // Exclude docker integration tests from coverage + classDirectories.setFrom( + files( + classDirectories.files.map { + fileTree(it) { + exclude( + "**/DockerImageStdioIntegrationTest*.class", + "**/DockerImageHttpIntegrationTest*.class", + ) + } + }, + ), + ) } tasks.withType().configureEach { @@ -101,6 +160,87 @@ spotless { } } +// Docker Integration Test Task +// ============================= +// This task runs integration tests for the Docker image produced by Jib. +// It is separate from the regular test task and must be explicitly invoked. +// +// Usage: +// ./gradlew dockerIntegrationTest +// +// Prerequisites: +// - Docker must be installed and running +// - The task will automatically build the Docker image using jibDockerBuild +// +// The task: +// - Checks if Docker is available +// - Builds the Docker image using Jib (if Docker is available) +// - Runs tests tagged with "docker-integration" +// - Uses the same test configuration as regular tests +// +// Notes: +// - If Docker is not available, the task will fail with a helpful error message +// - The test will verify the Docker image starts correctly and remains stable +// - Tests run in isolation from regular unit tests +tasks.register("dockerIntegrationTest") { + description = "Runs integration tests for the Docker image" + group = "verification" + + // Always run this task, don't use Gradle's up-to-date checking + // Docker images can change without Gradle knowing + outputs.upToDateWhen { false } + + // Check if Docker is available + val dockerAvailable = + try { + val process = ProcessBuilder("docker", "info").start() + process.waitFor() == 0 + } catch (e: Exception) { + false + } + + if (!dockerAvailable) { + doFirst { + throw GradleException( + "Docker is not available. Please ensure Docker is installed and running.", + ) + } + } + + // Depend on building the Docker image first (only if Docker is available) + if (dockerAvailable) { + dependsOn(tasks.jibDockerBuild) + } + + // Configure test task to only run docker integration tests + useJUnitPlatform { + includeTags("docker-integration") + } + + // Use the same test classpath and configuration as regular tests + testClassesDirs = sourceSets["test"].output.classesDirs + classpath = sourceSets["test"].runtimeClasspath + + // Ensure this doesn't trigger the regular test task or jacocoTestReport + mustRunAfter(tasks.test) + + // Set longer timeout for Docker tests + systemProperty("junit.jupiter.execution.timeout.default", "5m") + + // Output test results + testLogging { + events("passed", "skipped", "failed", "standardOut", "standardError") + exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL + showStandardStreams = true + } + + // Generate separate test report in a different directory + reports { + html.outputLocation.set(layout.buildDirectory.dir("reports/dockerIntegrationTest")) + junitXml.outputLocation.set(layout.buildDirectory.dir("test-results/dockerIntegrationTest")) + } +} + // Jib Plugin Configuration // ========================= // Jib is a Gradle plugin that builds optimized Docker images without requiring Docker installed. @@ -191,9 +331,6 @@ jib { mapOf( // Disable Spring Boot Docker Compose support when running in container "SPRING_DOCKER_COMPOSE_ENABLED" to "false", - // Default Solr URL using host.docker.internal to reach host machine - // On Linux, use --add-host=host.docker.internal:host-gateway - "SOLR_URL" to "http://host.docker.internal:8983/solr/", ) // JVM flags for containerized environments @@ -206,8 +343,8 @@ jib { "-XX:MaxRAMPercentage=75.0", ) - // Main class to run (auto-detected from Spring Boot plugin) - // mainClass is automatically set by Spring Boot Gradle plugin + // Explicitly set main class to avoid ASM scanning issues with newer Java versions + mainClass = "org.apache.solr.mcp.server.Main" // Port exposures (for documentation purposes) // The application doesn't expose ports by default (STDIO mode) diff --git a/compose.yaml b/compose.yaml index 4238b85..96f9eb6 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,3 +1,18 @@ +# 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. + services: solr: image: solr:9-slim diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7e48e57..81ca101 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ spring-boot = "3.5.6" spring-dependency-management = "1.1.7" errorprone-plugin = "4.2.0" -jib = "3.4.7" +jib = "3.4.4" spotless = "7.0.2" # Main dependencies @@ -21,6 +21,7 @@ jetty = "10.0.22" # Test dependencies testcontainers = "1.21.3" +awaitility = "4.2.2" [libraries] # Spring @@ -52,6 +53,7 @@ nullaway = { module = "com.uber.nullaway:nullaway", version.ref = "nullaway" } testcontainers-junit-jupiter = { module = "org.testcontainers:junit-jupiter" } testcontainers-solr = { module = "org.testcontainers:solr", version.ref = "testcontainers" } junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" } +awaitility = { module = "org.awaitility:awaitility", version.ref = "awaitility" } [bundles] spring-ai-mcp = [ @@ -70,7 +72,8 @@ test = [ "spring-ai-spring-boot-testcontainers", "testcontainers-junit-jupiter", "testcontainers-solr", - "spring-ai-starter-mcp-client" + "spring-ai-starter-mcp-client", + "awaitility" ] errorprone = [ diff --git a/init-solr.sh b/init-solr.sh index 4ae13f6..2c015a8 100755 --- a/init-solr.sh +++ b/init-solr.sh @@ -1,4 +1,19 @@ #!/bin/bash +# 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. + set -e # Ensure mydata directory exists diff --git a/settings.gradle.kts b/settings.gradle.kts index aa6a022..881dbc5 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1 +1,18 @@ +/* + * 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. + */ + rootProject.name = "solr-mcp-server" diff --git a/src/test/java/org/apache/solr/mcp/server/ClientStdio.java b/src/test/java/org/apache/solr/mcp/server/ClientStdio.java index 60ab695..066a5e5 100644 --- a/src/test/java/org/apache/solr/mcp/server/ClientStdio.java +++ b/src/test/java/org/apache/solr/mcp/server/ClientStdio.java @@ -20,24 +20,41 @@ import io.modelcontextprotocol.client.transport.ServerParameters; import io.modelcontextprotocol.client.transport.StdioClientTransport; import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper; -import java.io.File; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; // run after project has been built with "./gradlew build -x test and the mcp server jar is // connected to a running solr" public class ClientStdio { - public static void main(String[] args) { + public static void main(String[] args) throws IOException { + // Read build info generated by Spring Boot + Properties buildInfo = new Properties(); + try (InputStream input = + ClientStdio.class.getResourceAsStream("/META-INF/build-info.properties")) { + if (input == null) { + throw new IllegalStateException( + "build-info.properties not found. Run './gradlew build' first."); + } + buildInfo.load(input); + } - System.out.println(new File(".").getAbsolutePath()); + String jarName = + String.format( + "build/libs/%s-%s.jar", + buildInfo.getProperty("build.artifact"), + buildInfo.getProperty("build.version")); - var stdioParams = - ServerParameters.builder("java") - .args("-jar", "build/libs/solr-mcp-server-0.0.1-SNAPSHOT.jar") - .build(); + var stdioParams = ServerParameters + .builder("java") + .args("-jar", jarName) + .build(); var transport = new StdioClientTransport(stdioParams, new JacksonMcpJsonMapper(new ObjectMapper())); new SampleClient(transport).run(); } -} +} \ No newline at end of file diff --git a/src/test/java/org/apache/solr/mcp/server/DockerImageHttpIntegrationTest.java b/src/test/java/org/apache/solr/mcp/server/DockerImageHttpIntegrationTest.java new file mode 100644 index 0000000..05bd0ed --- /dev/null +++ b/src/test/java/org/apache/solr/mcp/server/DockerImageHttpIntegrationTest.java @@ -0,0 +1,248 @@ +/* + * 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. + */ +package org.apache.solr.mcp.server; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.SolrContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Integration test for the Docker image produced by Jib running in HTTP mode (streamable HTTP). + * + *

This test verifies that the Docker image built by Jib: + * + *

    + *
  • Starts successfully without errors in HTTP mode + *
  • Runs the Spring Boot MCP server application correctly + *
  • Exposes HTTP endpoint on port 8080 + *
  • Responds to HTTP requests + *
  • Can connect to an external Solr instance + *
+ * + *

Prerequisites: Before running this test, you must build the Docker image: + * + *

{@code
+ * ./gradlew jibDockerBuild
+ * }
+ * + *

This will create the image: {@code solr-mcp-server:0.0.1-SNAPSHOT} + * + *

Test Architecture: + * + *

    + *
  1. Creates a shared Docker network for inter-container communication + *
  2. Starts a Solr container on the network + *
  3. Starts the MCP server Docker image in HTTP mode with connection to Solr + *
  4. Verifies the container starts and HTTP endpoint is accessible + *
  5. Validates HTTP responses and container health + *
+ * + *

Note: This test is tagged with "docker-integration" and is designed to run + * separately from regular unit tests using the {@code dockerIntegrationTest} Gradle task. + */ +@Testcontainers +@Tag("docker-integration") +class DockerImageHttpIntegrationTest { + + private static final Logger log = + LoggerFactory.getLogger(DockerImageHttpIntegrationTest.class); + + // Docker image name and tag from build.gradle.kts + private static final String DOCKER_IMAGE = "solr-mcp-server:0.0.1-SNAPSHOT"; + private static final String SOLR_IMAGE = "solr:9.9-slim"; + private static final int HTTP_PORT = 8080; + + // Network for container communication + private static final Network network = Network.newNetwork(); + + // Solr container for backend + // Note: This field is used implicitly through the @Container annotation. + // Testcontainers JUnit extension automatically: + // 1. Starts this container before tests run + // 2. Makes it accessible via network alias "solr" at http://solr:8983/solr/ + // 3. Stops and cleans up the container after tests complete + @Container + private static final SolrContainer solrContainer = + new SolrContainer(DockerImageName.parse(SOLR_IMAGE)) + .withNetwork(network) + .withNetworkAliases("solr") + .withLogConsumer(new Slf4jLogConsumer(log).withPrefix("SOLR")); + + // MCP Server container (the image we're testing) + // Note: In HTTP mode, the application exposes a web server on port 8080 + @Container + private static final GenericContainer mcpServerContainer = + new GenericContainer<>(DockerImageName.parse(DOCKER_IMAGE)) + .withNetwork(network) + .withEnv("SOLR_URL", "http://solr:8983/solr/") + .withEnv("SPRING_DOCKER_COMPOSE_ENABLED", "false") + .withEnv("PROFILES", "http") + .withExposedPorts(HTTP_PORT) + .withLogConsumer(new Slf4jLogConsumer(log).withPrefix("MCP-SERVER-HTTP")) + // Wait for HTTP endpoint to be ready + .waitingFor( + Wait.forHttp("/actuator/health") + .forPort(HTTP_PORT) + .withStartupTimeout(Duration.ofSeconds(60))); + + private static HttpClient httpClient; + private static String baseUrl; + + @BeforeAll + static void setup() { + log.info("Solr container started. Internal URL: http://solr:8983/solr/"); + log.info("MCP Server container started in HTTP mode"); + + // Initialize HTTP client + httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build(); + + // Get the mapped port for accessing the container from the host + Integer mappedPort = mcpServerContainer.getMappedPort(HTTP_PORT); + baseUrl = "http://localhost:" + mappedPort; + + log.info("MCP Server HTTP endpoint available at: {}", baseUrl); + } + + @Test + void testSolrContainerIsRunning() { + // Verify Solr container started successfully + // This is essential because MCP server depends on Solr being available + assertTrue(solrContainer.isRunning(), "Solr container should be running"); + + log.info("Solr container is running and available at http://solr:8983/solr/"); + } + + @Test + void testContainerStartsAndRemainsStable() { + // Verify initial startup + assertTrue(mcpServerContainer.isRunning(), "Container should start successfully"); + + // Monitor container stability over 10 seconds to ensure it doesn't crash + await().atMost(10, TimeUnit.SECONDS) + .pollInterval(1, TimeUnit.SECONDS) + .pollDelay(Duration.ZERO) + .untilAsserted(() -> assertTrue(mcpServerContainer.isRunning())); + + log.info("Container started successfully and remained stable for 10 seconds"); + } + + @Test + void testNoErrorsInLogs() { + String logs = mcpServerContainer.getLogs(); + + // Check for critical error patterns + assertFalse( + logs.contains("Exception in thread \"main\""), + "Logs should not contain main thread exceptions"); + + assertFalse( + logs.contains("Application run failed"), + "Logs should not contain application failure messages"); + + assertFalse( + logs.contains("ERROR") && logs.contains("Failed to start"), + "Logs should not contain startup failure errors"); + + assertFalse( + logs.contains("fatal error") || logs.contains("JVM crash"), + "Logs should not contain JVM crash messages"); + + log.info("No critical errors found in container logs"); + } + + @Test + void testHttpEndpointResponds() throws IOException, InterruptedException { + // Test that the HTTP endpoint is accessible + HttpRequest request = + HttpRequest.newBuilder() + .uri(URI.create(baseUrl + "/actuator/health")) + .timeout(Duration.ofSeconds(10)) + .GET() + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + assertEquals(200, response.statusCode(), "Health endpoint should return 200 OK"); + assertTrue( + response.body().contains("UP") || response.body().contains("\"status\""), + "Health endpoint should return UP status or status field"); + + log.info("HTTP endpoint responded successfully with status: {}", response.statusCode()); + } + + @Test + void testSolrConnectivity() { + // Verify environment variables are working and Solr is accessible + String logs = mcpServerContainer.getLogs(); + + assertFalse( + logs.contains("Connection refused"), + "Logs should not contain connection refused errors"); + + assertFalse( + logs.contains("UnknownHostException"), + "Logs should not contain unknown host exceptions"); + + log.info("Container can connect to Solr without errors"); + } + + @Test + void testHttpModeConfiguration() { + String logs = mcpServerContainer.getLogs(); + + // Verify HTTP mode is active by checking for typical Spring Boot web server logs + assertTrue( + logs.contains("Tomcat started on port") || logs.contains("Netty started on port"), + "Logs should indicate web server started on a port"); + + log.info("HTTP mode configuration verified"); + } + + @Test + void testPortExposure() { + // Verify the port is exposed and mapped + Integer mappedPort = mcpServerContainer.getMappedPort(HTTP_PORT); + assertNotNull(mappedPort, "HTTP port should be exposed and mapped"); + assertTrue(mappedPort > 0, "Mapped port should be a valid port number"); + + log.info("Port {} is properly exposed and mapped to {}", HTTP_PORT, mappedPort); + } +} diff --git a/src/test/java/org/apache/solr/mcp/server/DockerImageStdioIntegrationTest.java b/src/test/java/org/apache/solr/mcp/server/DockerImageStdioIntegrationTest.java new file mode 100644 index 0000000..a8cb4ca --- /dev/null +++ b/src/test/java/org/apache/solr/mcp/server/DockerImageStdioIntegrationTest.java @@ -0,0 +1,191 @@ +/* + * 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. + */ +package org.apache.solr.mcp.server; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.SolrContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Integration test for the Docker image produced by Jib running in STDIO mode. + * + *

This test verifies that the Docker image built by Jib: + * + *

    + *
  • Starts successfully without errors in STDIO mode + *
  • Runs the Spring Boot MCP server application correctly + *
  • Doesn't crash during initial startup period + *
  • Can connect to an external Solr instance + *
+ * + *

Prerequisites: Before running this test, you must build the Docker image: + * + *

{@code
+ * ./gradlew jibDockerBuild
+ * }
+ * + *

This will create the image: {@code solr-mcp-server:0.0.1-SNAPSHOT} + * + *

Test Architecture: + * + *

    + *
  1. Creates a shared Docker network for inter-container communication + *
  2. Starts a Solr container on the network + *
  3. Starts the MCP server Docker image in STDIO mode with connection to Solr + *
  4. Verifies the container starts and remains stable + *
  5. Validates container health over time + *
+ * + *

Note: This test is tagged with "docker-integration" and is designed to run + * separately from regular unit tests using the {@code dockerIntegrationTest} Gradle task. + */ +@Testcontainers +@Tag("docker-integration") +class DockerImageStdioIntegrationTest { + + private static final Logger log = + LoggerFactory.getLogger(DockerImageStdioIntegrationTest.class); + + // Docker image name and tag from build.gradle.kts + private static final String DOCKER_IMAGE = "solr-mcp-server:0.0.1-SNAPSHOT"; + private static final String SOLR_IMAGE = "solr:9.9-slim"; + + // Network for container communication + private static final Network network = Network.newNetwork(); + + // Solr container for backend + // Note: This field is used implicitly through the @Container annotation. + // Testcontainers JUnit extension automatically: + // 1. Starts this container before tests run + // 2. Makes it accessible via network alias "solr" at http://solr:8983/solr/ + // 3. Stops and cleans up the container after tests complete + @Container + private static final SolrContainer solrContainer = + new SolrContainer(DockerImageName.parse(SOLR_IMAGE)) + .withNetwork(network) + .withNetworkAliases("solr") + .withLogConsumer(new Slf4jLogConsumer(log).withPrefix("SOLR")); + + // MCP Server container (the image we're testing) + // Note: In STDIO mode, the application doesn't produce logs to stdout that we can wait for, + // so we use a simple startup delay and then verify the container is running + @Container + private static final GenericContainer mcpServerContainer = + new GenericContainer<>(DockerImageName.parse(DOCKER_IMAGE)) + .withNetwork(network) + .withEnv("SOLR_URL", "http://solr:8983/solr/") + .withEnv("SPRING_DOCKER_COMPOSE_ENABLED", "false") + .withLogConsumer(new Slf4jLogConsumer(log).withPrefix("MCP-SERVER")) + // Give the application time to start (STDIO mode doesn't produce logs to wait + // for) + .withStartupTimeout(Duration.ofSeconds(60)); + + @BeforeAll + static void setup() throws InterruptedException { + log.info("Solr container started. Internal URL: http://solr:8983/solr/"); + log.info("MCP Server container starting. Waiting for initialization..."); + + // Give the MCP server a few seconds to initialize + // In STDIO mode, the app runs but doesn't produce logs we can monitor + Thread.sleep(5000); + + log.info("Initialization wait complete. Beginning tests."); + } + + @Test + void testSolrContainerIsRunning() { + // Verify Solr container started successfully + // This is essential because MCP server depends on Solr being available + assertTrue(solrContainer.isRunning(), "Solr container should be running"); + + log.info("Solr container is running and available at http://solr:8983/solr/"); + } + + @Test + void testContainerStartsAndRemainsStable() { + // Verify initial startup + assertTrue(mcpServerContainer.isRunning(), "Container should start successfully"); + + // Monitor container stability over 10 seconds to ensure it doesn't crash + await().atMost(10, TimeUnit.SECONDS) + .pollInterval(1, TimeUnit.SECONDS) + .pollDelay(Duration.ZERO) + .untilAsserted(() -> assertTrue(mcpServerContainer.isRunning())); + + log.info("Container started successfully and remained stable for 10 seconds"); + } + + @Test + void testNoErrorsInLogs() { + String logs = mcpServerContainer.getLogs(); + + // Check for critical error patterns + assertFalse( + logs.contains("Exception in thread \"main\""), + "Logs should not contain main thread exceptions"); + + assertFalse( + logs.contains("Application run failed"), + "Logs should not contain application failure messages"); + + assertFalse( + logs.contains("ERROR") && logs.contains("Failed to start"), + "Logs should not contain startup failure errors"); + + assertFalse( + logs.contains("fatal error") || logs.contains("JVM crash"), + "Logs should not contain JVM crash messages"); + + assertFalse( + logs.contains("exec format error"), + "Logs should not contain platform compatibility errors"); + + log.info("No critical errors found in container logs"); + } + + @Test + void testSolrConnectivity() { + // Verify environment variables are working and Solr is accessible + String logs = mcpServerContainer.getLogs(); + + assertFalse( + logs.contains("Connection refused"), + "Logs should not contain connection refused errors"); + + assertFalse( + logs.contains("UnknownHostException"), + "Logs should not contain unknown host exceptions"); + + log.info("Container can connect to Solr without errors"); + } +} From b325cc2cc6dc0b5655ad85dfd3cb921eb96e0dd5 Mon Sep 17 00:00:00 2001 From: adityamparikh Date: Thu, 30 Oct 2025 22:02:26 -0400 Subject: [PATCH 6/6] refactor: rename project from solr-mcp-server to solr-mcp --- .github/workflows/build-and-publish.yml | 20 +++++------ .run/SolrMcpServerHttp.run.xml | 2 +- .run/SolrMcpServerStdio.run.xml | 2 +- README.md | 34 +++++++++---------- build.gradle.kts | 12 +++---- settings.gradle.kts | 2 +- sonar-project.properties | 12 ------- src/main/resources/application.properties | 2 +- .../DockerImageHttpIntegrationTest.java | 4 +-- .../DockerImageStdioIntegrationTest.java | 4 +-- 10 files changed, 41 insertions(+), 53 deletions(-) delete mode 100644 sonar-project.properties diff --git a/.github/workflows/build-and-publish.yml b/.github/workflows/build-and-publish.yml index 80a27ee..1790463 100644 --- a/.github/workflows/build-and-publish.yml +++ b/.github/workflows/build-and-publish.yml @@ -33,8 +33,8 @@ # # Published Images: # ---------------- -# - GitHub Container Registry: ghcr.io/OWNER/solr-mcp-server:TAG -# - Docker Hub: DOCKERHUB_USERNAME/solr-mcp-server:TAG +# - GitHub Container Registry: ghcr.io/OWNER/solr-mcp:TAG +# - Docker Hub: DOCKERHUB_USERNAME/solr-mcp:TAG # # Image Tagging Strategy: # ---------------------- @@ -114,8 +114,8 @@ jobs: - name: Upload JAR artifact uses: actions/upload-artifact@v4 with: - name: solr-mcp-server-jar - path: build/libs/solr-mcp-server-*.jar + name: solr-mcp-jar + path: build/libs/solr-mcp-*.jar retention-days: 7 # Upload JUnit test results @@ -275,9 +275,9 @@ jobs: # Build and push each tag to GHCR # Jib automatically handles multi-platform builds (amd64, arm64) for TAG in "${TAG_ARRAY[@]}"; do - echo "Building and pushing ghcr.io/${{ steps.repo.outputs.owner_lc }}/solr-mcp-server:$TAG" + echo "Building and pushing ghcr.io/${{ steps.repo.outputs.owner_lc }}/solr-mcp:$TAG" ./gradlew jib \ - -Djib.to.image=ghcr.io/${{ steps.repo.outputs.owner_lc }}/solr-mcp-server:$TAG \ + -Djib.to.image=ghcr.io/${{ steps.repo.outputs.owner_lc }}/solr-mcp:$TAG \ -Djib.to.auth.username=${{ github.actor }} \ -Djib.to.auth.password=${{ secrets.GITHUB_TOKEN }} done @@ -293,9 +293,9 @@ jobs: # Build and push each tag to Docker Hub for TAG in "${TAG_ARRAY[@]}"; do - echo "Building and pushing ${{ secrets.DOCKERHUB_USERNAME }}/solr-mcp-server:$TAG" + echo "Building and pushing ${{ secrets.DOCKERHUB_USERNAME }}/solr-mcp:$TAG" ./gradlew jib \ - -Djib.to.image=${{ secrets.DOCKERHUB_USERNAME }}/solr-mcp-server:$TAG \ + -Djib.to.image=${{ secrets.DOCKERHUB_USERNAME }}/solr-mcp:$TAG \ -Djib.to.auth.username=${{ secrets.DOCKERHUB_USERNAME }} \ -Djib.to.auth.password=${{ secrets.DOCKERHUB_TOKEN }} done @@ -311,7 +311,7 @@ jobs: TAGS="${{ steps.meta.outputs.tags }}" IFS=',' read -ra TAG_ARRAY <<< "$TAGS" for TAG in "${TAG_ARRAY[@]}"; do - echo "- \`ghcr.io/${{ steps.repo.outputs.owner_lc }}/solr-mcp-server:$TAG\`" >> $GITHUB_STEP_SUMMARY + echo "- \`ghcr.io/${{ steps.repo.outputs.owner_lc }}/solr-mcp:$TAG\`" >> $GITHUB_STEP_SUMMARY done # Only show Docker Hub section if secrets are configured @@ -319,6 +319,6 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY echo "#### Docker Hub" >> $GITHUB_STEP_SUMMARY for TAG in "${TAG_ARRAY[@]}"; do - echo "- \`${{ secrets.DOCKERHUB_USERNAME }}/solr-mcp-server:$TAG\`" >> $GITHUB_STEP_SUMMARY + echo "- \`${{ secrets.DOCKERHUB_USERNAME }}/solr-mcp:$TAG\`" >> $GITHUB_STEP_SUMMARY done fi \ No newline at end of file diff --git a/.run/SolrMcpServerHttp.run.xml b/.run/SolrMcpServerHttp.run.xml index 8038646..ee4a430 100644 --- a/.run/SolrMcpServerHttp.run.xml +++ b/.run/SolrMcpServerHttp.run.xml @@ -2,7 +2,7 @@

This will create the image: {@code solr-mcp-server:0.0.1-SNAPSHOT} + *

This will create the image: {@code solr-mcp:0.0.1-SNAPSHOT} * *

Test Architecture: * @@ -86,7 +86,7 @@ class DockerImageHttpIntegrationTest { LoggerFactory.getLogger(DockerImageHttpIntegrationTest.class); // Docker image name and tag from build.gradle.kts - private static final String DOCKER_IMAGE = "solr-mcp-server:0.0.1-SNAPSHOT"; + private static final String DOCKER_IMAGE = "solr-mcp:0.0.1-SNAPSHOT"; private static final String SOLR_IMAGE = "solr:9.9-slim"; private static final int HTTP_PORT = 8080; diff --git a/src/test/java/org/apache/solr/mcp/server/DockerImageStdioIntegrationTest.java b/src/test/java/org/apache/solr/mcp/server/DockerImageStdioIntegrationTest.java index a8cb4ca..1c801c3 100644 --- a/src/test/java/org/apache/solr/mcp/server/DockerImageStdioIntegrationTest.java +++ b/src/test/java/org/apache/solr/mcp/server/DockerImageStdioIntegrationTest.java @@ -54,7 +54,7 @@ * ./gradlew jibDockerBuild * } * - *

This will create the image: {@code solr-mcp-server:0.0.1-SNAPSHOT} + *

This will create the image: {@code solr-mcp:0.0.1-SNAPSHOT} * *

Test Architecture: * @@ -77,7 +77,7 @@ class DockerImageStdioIntegrationTest { LoggerFactory.getLogger(DockerImageStdioIntegrationTest.class); // Docker image name and tag from build.gradle.kts - private static final String DOCKER_IMAGE = "solr-mcp-server:0.0.1-SNAPSHOT"; + private static final String DOCKER_IMAGE = "solr-mcp:0.0.1-SNAPSHOT"; private static final String SOLR_IMAGE = "solr:9.9-slim"; // Network for container communication