Skip to content

Add Maven and Gradle protocol support for Java MCP servers #1456

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions cmd/thv/app/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,24 @@ ToolHive supports building containers from protocol schemes:
$ thv build npx://package-name
$ thv build go://package-name
$ thv build go://./local-path
$ thv build maven://com.example.MCPServer
$ thv build gradle://com.example.MCPServer

Automatically generates a container that can run the specified package
using either uvx (Python with uv package manager), npx (Node.js),
or go (Golang). For Go, you can also specify local paths starting
with './' or '../' to build local Go projects.
go (Golang), maven (Java with Maven), or gradle (Java with Gradle).
For Go, you can also specify local paths starting with './' or '../'
to build local Go projects.

The container will be built and tagged locally, ready to be used with 'thv run'
or other container tools. The built image name will be displayed upon successful completion.

Examples:
$ thv build uvx://mcp-server-git
$ thv build --tag my-custom-name:latest npx://@modelcontextprotocol/server-filesystem
$ thv build go://./my-local-server`,
$ thv build go://./my-local-server
$ thv build maven://com.example.MCPServer
$ thv build gradle://com.example.MCPServer`,
Args: cobra.ExactArgs(1),
RunE: buildCmdFunc,
}
Expand Down Expand Up @@ -66,7 +71,7 @@ func buildCmdFunc(cmd *cobra.Command, args []string) error {

// Validate that this is a protocol scheme
if !runner.IsImageProtocolScheme(protocolScheme) {
return fmt.Errorf("invalid protocol scheme: %s. Supported schemes are: uvx://, npx://, go://", protocolScheme)
return fmt.Errorf("invalid protocol scheme: %s. Supported schemes are: %s", protocolScheme, runner.GetSupportedSchemes())
}

// Create image manager (even for dry-run, we pass it but it won't be used)
Expand Down
13 changes: 8 additions & 5 deletions cmd/thv/app/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,14 @@ ToolHive supports four ways to run an MCP server:
$ thv run npx://package-name [-- args...]
$ thv run go://package-name [-- args...]
$ thv run go://./local-path [-- args...]

Automatically generates a container that runs the specified package
using either uvx (Python with uv package manager), npx (Node.js),
or go (Golang). For Go, you can also specify local paths starting
with './' or '../' to build and run local Go projects.
$ thv run maven://com.example.MCPServer [-- args...]
$ thv run gradle://com.example.MCPServer [-- args...]

Automatically generates a container that runs the specified package
using either uvx (Python with uv package manager), npx (Node.js),
go (Golang), maven (Java with Maven), or gradle (Java with Gradle).
For Go, you can also specify local paths starting with './' or '../'
to build and run local Go projects.

4. From an exported configuration:

Expand Down
9 changes: 7 additions & 2 deletions docs/cli/thv_build.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 8 additions & 5 deletions docs/cli/thv_run.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions docs/server/docs.go

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions docs/server/swagger.json

Large diffs are not rendered by default.

20 changes: 14 additions & 6 deletions docs/server/swagger.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

79 changes: 79 additions & 0 deletions pkg/container/templates/gradle.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
FROM gradle:8.11-jdk21-alpine

{{if .CACertContent}}
# Add custom CA certificate BEFORE any network operations
# This ensures that package managers can verify TLS certificates in corporate networks
COPY ca-cert.crt /tmp/custom-ca.crt
RUN cat /tmp/custom-ca.crt >> /etc/ssl/certs/ca-certificates.crt && \
rm /tmp/custom-ca.crt
{{end}}

# Install CA certificates
RUN apk add --no-cache ca-certificates

# Set working directory
WORKDIR /app

# Create a non-root user to run the application and set proper permissions
RUN addgroup -S appgroup && \
adduser -S appuser -G appgroup && \
mkdir -p /app && \
chown -R appuser:appgroup /app && \
mkdir -p /home/appuser/.gradle && \
chown -R appuser:appgroup /home/appuser

{{if .CACertContent}}
# Properly install the custom CA certificate using standard tools
RUN mkdir -p /usr/local/share/ca-certificates && \
cp /tmp/custom-ca.crt /usr/local/share/ca-certificates/custom-ca.crt 2>/dev/null || \
echo "CA cert already added to bundle" && \
chmod 644 /usr/local/share/ca-certificates/custom-ca.crt 2>/dev/null || true && \
update-ca-certificates

# Configure Java to use the custom CA certificate
ENV JAVA_TOOL_OPTIONS="-Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts -Djavax.net.ssl.trustStorePassword=changeit"
{{end}}

# Set environment variables for better performance in containers
ENV GRADLE_OPTS="-Xmx512m -XX:MaxMetaspaceSize=128m -Dorg.gradle.daemon=false" \
GRADLE_USER_HOME="/home/appuser/.gradle"

{{if .IsLocalPath}}
# Copy the local source code
COPY . /app/

# Change ownership of copied files to appuser
USER root
RUN chown -R appuser:appgroup /app
{{end}}

# Switch to non-root user
USER appuser

# Create a minimal build.gradle file to download and run the dependency
RUN cat > build.gradle << 'EOF'
plugins {
id 'java'
id 'application'
}

repositories {
mavenCentral()
}

dependencies {
implementation '{{.MCPPackage}}'
}

task copyToLib(type: Copy) {
from configurations.runtimeClasspath
into 'lib'
}
EOF

# Download all dependencies to lib directory during build
RUN gradle copyToLib --no-daemon

# Run the MCP server with all JARs in classpath
# The JAR filename follows the pattern artifactId-version.jar
ENTRYPOINT ["sh", "-c", "java -cp 'lib/*' -jar lib/$(echo '{{.MCPPackage}}' | cut -d: -f2)-$(echo '{{.MCPPackage}}' | cut -d: -f3).jar {{range .MCPArgs}}{{.}} {{end}}"]
72 changes: 72 additions & 0 deletions pkg/container/templates/maven.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
FROM maven:3.9-eclipse-temurin-21-alpine

{{if .CACertContent}}
# Add custom CA certificate BEFORE any network operations
# This ensures that package managers can verify TLS certificates in corporate networks
COPY ca-cert.crt /tmp/custom-ca.crt
RUN cat /tmp/custom-ca.crt >> /etc/ssl/certs/ca-certificates.crt && \
rm /tmp/custom-ca.crt
{{end}}

# Install CA certificates
RUN apk add --no-cache ca-certificates

# Set working directory
WORKDIR /app

# Create a non-root user to run the application and set proper permissions
RUN addgroup -S appgroup && \
adduser -S appuser -G appgroup && \
mkdir -p /app && \
chown -R appuser:appgroup /app && \
mkdir -p /home/appuser/.m2 && \
chown -R appuser:appgroup /home/appuser

{{if .CACertContent}}
# Properly install the custom CA certificate using standard tools
RUN mkdir -p /usr/local/share/ca-certificates && \
cp /tmp/custom-ca.crt /usr/local/share/ca-certificates/custom-ca.crt 2>/dev/null || \
echo "CA cert already added to bundle" && \
chmod 644 /usr/local/share/ca-certificates/custom-ca.crt 2>/dev/null || true && \
update-ca-certificates

# Configure Java to use the custom CA certificate
ENV JAVA_TOOL_OPTIONS="-Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts -Djavax.net.ssl.trustStorePassword=changeit"
{{end}}

# Set environment variables for better performance in containers
ENV MAVEN_OPTS="-Xmx512m -XX:MaxMetaspaceSize=128m" \
MAVEN_CONFIG="/home/appuser/.m2"

{{if .IsLocalPath}}
# Copy the local source code
COPY . /app/

# Change ownership of copied files to appuser
USER root
RUN chown -R appuser:appgroup /app
{{end}}

# Switch to non-root user
USER appuser

# Download the Maven artifact and its dependencies
# Also try to download the runner JAR (for Quarkus applications)
RUN mvn dependency:get -Dartifact={{.MCPPackage}}:jar -Dtransitive=true && \
mvn dependency:get -Dartifact={{.MCPPackage}}:jar:runner 2>/dev/null || true

# Prepare JAR paths
RUN GROUP_ID=$(echo '{{.MCPPackage}}' | cut -d: -f1 | tr '.' '/') && \
ARTIFACT_ID=$(echo '{{.MCPPackage}}' | cut -d: -f2) && \
VERSION=$(echo '{{.MCPPackage}}' | cut -d: -f3) && \
BASE_PATH="/home/appuser/.m2/repository/$GROUP_ID/$ARTIFACT_ID/$VERSION" && \
RUNNER_JAR="$BASE_PATH/$ARTIFACT_ID-$VERSION-runner.jar" && \
REGULAR_JAR="$BASE_PATH/$ARTIFACT_ID-$VERSION.jar" && \
if [ -f "$RUNNER_JAR" ]; then \
echo "$RUNNER_JAR" > /app/jar-path.txt; \
else \
echo "$REGULAR_JAR" > /app/jar-path.txt; \
fi

# Run the MCP server
ENTRYPOINT ["sh", "-c", "java -jar $(cat /app/jar-path.txt) {{range .MCPArgs}}{{.}} {{end}}"]
12 changes: 12 additions & 0 deletions pkg/container/templates/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ const (
TransportTypeNPX TransportType = "npx"
// TransportTypeGO represents the go transport.
TransportTypeGO TransportType = "go"
// TransportTypeMaven represents the maven transport.
TransportTypeMaven TransportType = "maven"
// TransportTypeGradle represents the gradle transport.
TransportTypeGradle TransportType = "gradle"
)

// GetDockerfileTemplate returns the Dockerfile template for the specified transport type.
Expand All @@ -48,6 +52,10 @@ func GetDockerfileTemplate(transportType TransportType, data TemplateData) (stri
templateName = "npx.tmpl"
case TransportTypeGO:
templateName = "go.tmpl"
case TransportTypeMaven:
templateName = "maven.tmpl"
case TransportTypeGradle:
templateName = "gradle.tmpl"
default:
return "", fmt.Errorf("unsupported transport type: %s", transportType)
}
Expand Down Expand Up @@ -82,6 +90,10 @@ func ParseTransportType(s string) (TransportType, error) {
return TransportTypeNPX, nil
case "go":
return TransportTypeGO, nil
case "maven":
return TransportTypeMaven, nil
case "gradle":
return TransportTypeGradle, nil
default:
return "", fmt.Errorf("unsupported transport type: %s", s)
}
Expand Down
Loading
Loading