This is an automated email from the ASF dual-hosted git repository.
epugh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr-mcp.git
The following commit(s) were added to refs/heads/main by this push:
new e7642e7 feat: Add Docker support with Jib and GitHub Actions CI/CD
(#6)
e7642e7 is described below
commit e7642e713b506316f1e7a54f797cf0cb9d2fec93
Author: Aditya Parikh <[email protected]>
AuthorDate: Fri Oct 31 07:15:22 2025 -0400
feat: Add Docker support with Jib and GitHub Actions CI/CD (#6)
* Add Docker support with Jib and GitHub Actions CI/CD
* Minimal .asf.yaml to get correct github notifications in place
* Apply spotless plugin (#5)
* Add Docker support with Jib and GitHub Actions CI/CD
# Conflicts:
# build.gradle.kts
# gradle/libs.versions.toml
* Update the repo name pattern.
* Add Docker support with Jib and GitHub Actions CI/CD
# Conflicts:
# build.gradle.kts
# gradle/libs.versions.toml
* test: add Docker integration tests for MCP server under both STDIO and
HTTP modes
---------
Co-authored-by: Chris Hostetter <[email protected]>
Co-authored-by: Eric Pugh <[email protected]>
---
.github/workflows/build-and-publish.yml | 324 +++++++++++++++++++++
.github/workflows/build.yml | 15 +
.github/workflows/claude-code-review.yml | 38 ---
.github/workflows/claude.yml | 36 ---
README.md | 277 +++++++++++++++++-
build.gradle.kts | 268 ++++++++++++++++-
compose.yaml | 15 +
gradle/libs.versions.toml | 7 +-
init-solr.sh | 15 +
settings.gradle.kts | 17 ++
.../org/apache/solr/mcp/server/ClientStdio.java | 33 ++-
.../mcp/server/DockerImageHttpIntegrationTest.java | 248 ++++++++++++++++
.../server/DockerImageStdioIntegrationTest.java | 191 ++++++++++++
13 files changed, 1390 insertions(+), 94 deletions(-)
diff --git a/.github/workflows/build-and-publish.yml
b/.github/workflows/build-and-publish.yml
new file mode 100644
index 0000000..80a27ee
--- /dev/null
+++ b/.github/workflows/build-and-publish.yml
@@ -0,0 +1,324 @@
+# 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
+# ===========================================
+#
+# 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/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/.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..813ebe6 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,18 @@
+[](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
@@ -32,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
@@ -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.

@@ -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..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 {
@@ -7,6 +24,7 @@ plugins {
jacoco
alias(libs.plugins.errorprone)
alias(libs.plugins.spotless)
+ alias(libs.plugins.jib)
}
group = "org.apache.solr"
@@ -58,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<Test> {
- 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 {
@@ -70,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<JavaCompile>().configureEach {
@@ -99,3 +159,207 @@ spotless {
ktlint()
}
}
+
+// 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<Test>("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.
+// 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",
+ )
+
+ // 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",
+ )
+
+ // 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)
+ // 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/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 9de2429..81ca101 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.4"
spotless = "7.0.2"
# Main dependencies
@@ -20,6 +21,7 @@ jetty = "10.0.22"
# Test dependencies
testcontainers = "1.21.3"
+awaitility = "4.2.2"
[libraries]
# Spring
@@ -51,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 = [
@@ -69,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 = [
@@ -81,4 +85,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
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 com.fasterxml.jackson.databind.ObjectMapper;
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).
+ *
+ * <p>This test verifies that the Docker image built by Jib:
+ *
+ * <ul>
+ * <li>Starts successfully without errors in HTTP mode
+ * <li>Runs the Spring Boot MCP server application correctly
+ * <li>Exposes HTTP endpoint on port 8080
+ * <li>Responds to HTTP requests
+ * <li>Can connect to an external Solr instance
+ * </ul>
+ *
+ * <p><strong>Prerequisites:</strong> Before running this test, you must build
the Docker image:
+ *
+ * <pre>{@code
+ * ./gradlew jibDockerBuild
+ * }</pre>
+ *
+ * <p>This will create the image: {@code solr-mcp-server:0.0.1-SNAPSHOT}
+ *
+ * <p><strong>Test Architecture:</strong>
+ *
+ * <ol>
+ * <li>Creates a shared Docker network for inter-container communication
+ * <li>Starts a Solr container on the network
+ * <li>Starts the MCP server Docker image in HTTP mode with connection to
Solr
+ * <li>Verifies the container starts and HTTP endpoint is accessible
+ * <li>Validates HTTP responses and container health
+ * </ol>
+ *
+ * <p><strong>Note:</strong> 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<String> 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.
+ *
+ * <p>This test verifies that the Docker image built by Jib:
+ *
+ * <ul>
+ * <li>Starts successfully without errors in STDIO mode
+ * <li>Runs the Spring Boot MCP server application correctly
+ * <li>Doesn't crash during initial startup period
+ * <li>Can connect to an external Solr instance
+ * </ul>
+ *
+ * <p><strong>Prerequisites:</strong> Before running this test, you must build
the Docker image:
+ *
+ * <pre>{@code
+ * ./gradlew jibDockerBuild
+ * }</pre>
+ *
+ * <p>This will create the image: {@code solr-mcp-server:0.0.1-SNAPSHOT}
+ *
+ * <p><strong>Test Architecture:</strong>
+ *
+ * <ol>
+ * <li>Creates a shared Docker network for inter-container communication
+ * <li>Starts a Solr container on the network
+ * <li>Starts the MCP server Docker image in STDIO mode with connection to
Solr
+ * <li>Verifies the container starts and remains stable
+ * <li>Validates container health over time
+ * </ol>
+ *
+ * <p><strong>Note:</strong> 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");
+ }
+}