Skip to content

Commit dcad29c

Browse files
committed
Add Maven and Gradle protocol support for Java MCP servers
This change introduces support for packaging and running Java-based MCP servers using Maven and Gradle build systems, extending ToolHive's capabilities beyond Python (uvx), Node.js (npx), and Go packages. Changes include: - New maven.tmpl and gradle.tmpl container templates for Java containers - Maven and Gradle transport types in templates.go - Protocol handling for maven:// and gradle:// schemes - GetSupportedSchemes() function for dynamic scheme listing - Comprehensive test coverage for new functionality - Updated CLI documentation for thv run and thv build commands The implementation follows the existing pattern used for other protocol schemes, ensuring consistency across the codebase. Signed-off-by: Juan Antonio Osorio <[email protected]>
1 parent 86edb93 commit dcad29c

File tree

13 files changed

+392
-33
lines changed

13 files changed

+392
-33
lines changed

cmd/thv/app/build.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,24 @@ ToolHive supports building containers from protocol schemes:
2222
$ thv build npx://package-name
2323
$ thv build go://package-name
2424
$ thv build go://./local-path
25+
$ thv build maven://com.example.MCPServer
26+
$ thv build gradle://com.example.MCPServer
2527
2628
Automatically generates a container that can run the specified package
2729
using either uvx (Python with uv package manager), npx (Node.js),
28-
or go (Golang). For Go, you can also specify local paths starting
29-
with './' or '../' to build local Go projects.
30+
go (Golang), maven (Java with Maven), or gradle (Java with Gradle).
31+
For Go, you can also specify local paths starting with './' or '../'
32+
to build local Go projects.
3033
3134
The container will be built and tagged locally, ready to be used with 'thv run'
3235
or other container tools. The built image name will be displayed upon successful completion.
3336
3437
Examples:
3538
$ thv build uvx://mcp-server-git
3639
$ thv build --tag my-custom-name:latest npx://@modelcontextprotocol/server-filesystem
37-
$ thv build go://./my-local-server`,
40+
$ thv build go://./my-local-server
41+
$ thv build maven://com.example.MCPServer
42+
$ thv build gradle://com.example.MCPServer`,
3843
Args: cobra.ExactArgs(1),
3944
RunE: buildCmdFunc,
4045
}
@@ -66,7 +71,7 @@ func buildCmdFunc(cmd *cobra.Command, args []string) error {
6671

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

7277
// Create image manager (even for dry-run, we pass it but it won't be used)

cmd/thv/app/run.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,14 @@ ToolHive supports four ways to run an MCP server:
4747
$ thv run npx://package-name [-- args...]
4848
$ thv run go://package-name [-- args...]
4949
$ thv run go://./local-path [-- args...]
50-
51-
Automatically generates a container that runs the specified package
52-
using either uvx (Python with uv package manager), npx (Node.js),
53-
or go (Golang). For Go, you can also specify local paths starting
54-
with './' or '../' to build and run local Go projects.
50+
$ thv run maven://com.example.MCPServer [-- args...]
51+
$ thv run gradle://com.example.MCPServer [-- args...]
52+
53+
Automatically generates a container that runs the specified package
54+
using either uvx (Python with uv package manager), npx (Node.js),
55+
go (Golang), maven (Java with Maven), or gradle (Java with Gradle).
56+
For Go, you can also specify local paths starting with './' or '../'
57+
to build and run local Go projects.
5558
5659
4. From an exported configuration:
5760

docs/cli/thv_build.md

Lines changed: 7 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/cli/thv_run.md

Lines changed: 8 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/server/docs.go

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/server/swagger.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/server/swagger.yaml

Lines changed: 14 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/container/templates/gradle.tmpl

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
FROM gradle:8.11-jdk21-alpine
2+
3+
{{if .CACertContent}}
4+
# Add custom CA certificate BEFORE any network operations
5+
# This ensures that package managers can verify TLS certificates in corporate networks
6+
COPY ca-cert.crt /tmp/custom-ca.crt
7+
RUN cat /tmp/custom-ca.crt >> /etc/ssl/certs/ca-certificates.crt && \
8+
rm /tmp/custom-ca.crt
9+
{{end}}
10+
11+
# Install CA certificates
12+
RUN apk add --no-cache ca-certificates
13+
14+
# Set working directory
15+
WORKDIR /app
16+
17+
# Create a non-root user to run the application and set proper permissions
18+
RUN addgroup -S appgroup && \
19+
adduser -S appuser -G appgroup && \
20+
mkdir -p /app && \
21+
chown -R appuser:appgroup /app && \
22+
mkdir -p /home/appuser/.gradle && \
23+
chown -R appuser:appgroup /home/appuser
24+
25+
{{if .CACertContent}}
26+
# Properly install the custom CA certificate using standard tools
27+
RUN mkdir -p /usr/local/share/ca-certificates && \
28+
cp /tmp/custom-ca.crt /usr/local/share/ca-certificates/custom-ca.crt 2>/dev/null || \
29+
echo "CA cert already added to bundle" && \
30+
chmod 644 /usr/local/share/ca-certificates/custom-ca.crt 2>/dev/null || true && \
31+
update-ca-certificates
32+
33+
# Configure Java to use the custom CA certificate
34+
ENV JAVA_TOOL_OPTIONS="-Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts -Djavax.net.ssl.trustStorePassword=changeit"
35+
{{end}}
36+
37+
# Set environment variables for better performance in containers
38+
ENV GRADLE_OPTS="-Xmx512m -XX:MaxMetaspaceSize=128m -Dorg.gradle.daemon=false" \
39+
GRADLE_USER_HOME="/home/appuser/.gradle"
40+
41+
{{if .IsLocalPath}}
42+
# Copy the local source code
43+
COPY . /app/
44+
45+
# Change ownership of copied files to appuser
46+
USER root
47+
RUN chown -R appuser:appgroup /app
48+
{{end}}
49+
50+
# Switch to non-root user
51+
USER appuser
52+
53+
# Create a minimal build.gradle file to download and run the dependency
54+
RUN cat > build.gradle << 'EOF'
55+
plugins {
56+
id 'java'
57+
id 'application'
58+
}
59+
60+
repositories {
61+
mavenCentral()
62+
}
63+
64+
dependencies {
65+
implementation '{{.MCPPackage}}'
66+
}
67+
68+
task copyToLib(type: Copy) {
69+
from configurations.runtimeClasspath
70+
into 'lib'
71+
}
72+
EOF
73+
74+
# Download all dependencies to lib directory during build
75+
RUN gradle copyToLib --no-daemon
76+
77+
# Run the MCP server with all JARs in classpath
78+
# The JAR filename follows the pattern artifactId-version.jar
79+
ENTRYPOINT ["sh", "-c", "java -cp 'lib/*' -jar lib/$(echo '{{.MCPPackage}}' | cut -d: -f2)-$(echo '{{.MCPPackage}}' | cut -d: -f3).jar {{range .MCPArgs}}{{.}} {{end}}"]

pkg/container/templates/maven.tmpl

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
FROM maven:3.9-eclipse-temurin-21-alpine
2+
3+
{{if .CACertContent}}
4+
# Add custom CA certificate BEFORE any network operations
5+
# This ensures that package managers can verify TLS certificates in corporate networks
6+
COPY ca-cert.crt /tmp/custom-ca.crt
7+
RUN cat /tmp/custom-ca.crt >> /etc/ssl/certs/ca-certificates.crt && \
8+
rm /tmp/custom-ca.crt
9+
{{end}}
10+
11+
# Install CA certificates
12+
RUN apk add --no-cache ca-certificates
13+
14+
# Set working directory
15+
WORKDIR /app
16+
17+
# Create a non-root user to run the application and set proper permissions
18+
RUN addgroup -S appgroup && \
19+
adduser -S appuser -G appgroup && \
20+
mkdir -p /app && \
21+
chown -R appuser:appgroup /app && \
22+
mkdir -p /home/appuser/.m2 && \
23+
chown -R appuser:appgroup /home/appuser
24+
25+
{{if .CACertContent}}
26+
# Properly install the custom CA certificate using standard tools
27+
RUN mkdir -p /usr/local/share/ca-certificates && \
28+
cp /tmp/custom-ca.crt /usr/local/share/ca-certificates/custom-ca.crt 2>/dev/null || \
29+
echo "CA cert already added to bundle" && \
30+
chmod 644 /usr/local/share/ca-certificates/custom-ca.crt 2>/dev/null || true && \
31+
update-ca-certificates
32+
33+
# Configure Java to use the custom CA certificate
34+
ENV JAVA_TOOL_OPTIONS="-Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts -Djavax.net.ssl.trustStorePassword=changeit"
35+
{{end}}
36+
37+
# Set environment variables for better performance in containers
38+
ENV MAVEN_OPTS="-Xmx512m -XX:MaxMetaspaceSize=128m" \
39+
MAVEN_CONFIG="/home/appuser/.m2"
40+
41+
{{if .IsLocalPath}}
42+
# Copy the local source code
43+
COPY . /app/
44+
45+
# Change ownership of copied files to appuser
46+
USER root
47+
RUN chown -R appuser:appgroup /app
48+
{{end}}
49+
50+
# Switch to non-root user
51+
USER appuser
52+
53+
# Download the Maven artifact and its dependencies
54+
# Also try to download the runner JAR (for Quarkus applications)
55+
RUN mvn dependency:get -Dartifact={{.MCPPackage}}:jar -Dtransitive=true && \
56+
mvn dependency:get -Dartifact={{.MCPPackage}}:jar:runner 2>/dev/null || true
57+
58+
# Prepare JAR paths
59+
RUN GROUP_ID=$(echo '{{.MCPPackage}}' | cut -d: -f1 | tr '.' '/') && \
60+
ARTIFACT_ID=$(echo '{{.MCPPackage}}' | cut -d: -f2) && \
61+
VERSION=$(echo '{{.MCPPackage}}' | cut -d: -f3) && \
62+
BASE_PATH="/home/appuser/.m2/repository/$GROUP_ID/$ARTIFACT_ID/$VERSION" && \
63+
RUNNER_JAR="$BASE_PATH/$ARTIFACT_ID-$VERSION-runner.jar" && \
64+
REGULAR_JAR="$BASE_PATH/$ARTIFACT_ID-$VERSION.jar" && \
65+
if [ -f "$RUNNER_JAR" ]; then \
66+
echo "$RUNNER_JAR" > /app/jar-path.txt; \
67+
else \
68+
echo "$REGULAR_JAR" > /app/jar-path.txt; \
69+
fi
70+
71+
# Run the MCP server
72+
ENTRYPOINT ["sh", "-c", "java -jar $(cat /app/jar-path.txt) {{range .MCPArgs}}{{.}} {{end}}"]

pkg/container/templates/templates.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ const (
3434
TransportTypeNPX TransportType = "npx"
3535
// TransportTypeGO represents the go transport.
3636
TransportTypeGO TransportType = "go"
37+
// TransportTypeMaven represents the maven transport.
38+
TransportTypeMaven TransportType = "maven"
39+
// TransportTypeGradle represents the gradle transport.
40+
TransportTypeGradle TransportType = "gradle"
3741
)
3842

3943
// GetDockerfileTemplate returns the Dockerfile template for the specified transport type.
@@ -48,6 +52,10 @@ func GetDockerfileTemplate(transportType TransportType, data TemplateData) (stri
4852
templateName = "npx.tmpl"
4953
case TransportTypeGO:
5054
templateName = "go.tmpl"
55+
case TransportTypeMaven:
56+
templateName = "maven.tmpl"
57+
case TransportTypeGradle:
58+
templateName = "gradle.tmpl"
5159
default:
5260
return "", fmt.Errorf("unsupported transport type: %s", transportType)
5361
}
@@ -82,6 +90,10 @@ func ParseTransportType(s string) (TransportType, error) {
8290
return TransportTypeNPX, nil
8391
case "go":
8492
return TransportTypeGO, nil
93+
case "maven":
94+
return TransportTypeMaven, nil
95+
case "gradle":
96+
return TransportTypeGradle, nil
8597
default:
8698
return "", fmt.Errorf("unsupported transport type: %s", s)
8799
}

0 commit comments

Comments
 (0)