This is an automated email from the ASF dual-hosted git repository.
terrymanu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shardingsphere.git
The following commit(s) were added to refs/heads/master by this push:
new 731ef1e2e22 Centralize runtime image metadata (#38795)
731ef1e2e22 is described below
commit 731ef1e2e223df7146071b46dcdb8df38b412fe9
Author: Liang Zhang <[email protected]>
AuthorDate: Thu Jun 4 13:22:24 2026 +0800
Centralize runtime image metadata (#38795)
* Centralize MCP E2E image metadata
Move MCP E2E MySQL image, LLM base image, optional digest, and model
checksum into e2e-env.properties.
Wire the workflow, local build script, Dockerfile, runtime evidence, and
tests to the shared properties.
* Centralize runtime image metadata
Centralize MCP E2E runtime image, MySQL image, and LLM model metadata
in e2e-env.properties so the workflow, local Docker build script,
Dockerfile, and Java E2E runtime support read the same source of truth.
Parameterize the LLM runtime Dockerfile with the base image, server
runtime, formal model id, model repository, quantization, revision,
file name, and checksum instead of embedding Docker image digests or
model coordinates in the Dockerfile or workflow.
Keep mcp.llm.model as the explicit served model id
ggml-org/Qwen3-1.7B-GGUF:Q4_K_M, and use model metadata only for
building the prepackaged runtime image and writing score evidence.
Move the MCP MySQL Testcontainers image into the same env properties
and verify property loading with focused tests.
---
.github/workflows/e2e-mcp.yml | 46 +++++++-
.../e2e/mcp/llm/config/LLME2EConfiguration.java | 99 +++++++++++++----
.../mcp/llm/config/LLME2EConfigurationTest.java | 121 +++++++++++++++++----
.../artifact/LLME2EArtifactWriter.java | 2 +-
.../artifact/LLME2EArtifactWriterTest.java | 8 +-
.../client/LLMChatModelClientTest.java | 12 +-
.../e2e/mcp/llm/fixture/LLMRuntimeSupport.java | 62 ++++-------
.../e2e/mcp/llm/fixture/LLMRuntimeSupportTest.java | 15 +--
.../support/runtime/MySQLRuntimeTestSupport.java | 15 ++-
.../runtime/MySQLRuntimeTestSupportTest.java | 16 +++
.../test/resources/docker/llm-runtime/Dockerfile | 27 +++--
.../resources/docker/llm-runtime/build-local.sh | 66 ++++++++---
.../mcp/src/test/resources/env/e2e-env.properties | 9 ++
13 files changed, 375 insertions(+), 123 deletions(-)
diff --git a/.github/workflows/e2e-mcp.yml b/.github/workflows/e2e-mcp.yml
index 0580713a9e4..ecde48c830e 100644
--- a/.github/workflows/e2e-mcp.yml
+++ b/.github/workflows/e2e-mcp.yml
@@ -44,7 +44,6 @@ permissions:
env:
MAVEN_OPTS: -Dhttp.keepAlive=false -Dmaven.wagon.http.pool=false
-Dmaven.wagon.http.retryHandler.class=standard
-Dmaven.wagon.http.retryHandler.count=3 -Dspotless.apply.skip=true
- MCP_LLM_SERVER_IMAGE: apache/shardingsphere-mcp-llm-runtime:local
MCP_DISTRIBUTION_IMAGE: apache/shardingsphere-mcp-e2e:local
jobs:
@@ -69,13 +68,56 @@ jobs:
run: |
docker version
docker system df
+ - name: Load MCP E2E Properties
+ id: mcp-e2e-properties
+ shell: bash
+ run: |
+ set -euo pipefail
+
property_file="test/e2e/mcp/src/test/resources/env/e2e-env.properties"
+ load_property() {
+ local value
+ value="$(awk -F= -v key="$1" '$1 == key {sub(/^[^=]*=/, "");
print; found = 1; exit} END {if (!found) exit 1}' "${property_file}")"
+ if [[ -z "${value}" ]]; then
+ echo "MCP E2E property is required: $1" >&2
+ exit 1
+ fi
+ printf '%s' "${value}"
+ }
+ load_optional_property() {
+ awk -F= -v key="$1" '$1 == key {sub(/^[^=]*=/, ""); print; found =
1; exit} END {if (!found) exit 1}' "${property_file}" || true
+ }
+ base_server_image="$(load_property 'mcp.llm.base-server-image')"
+ base_server_image_digest="$(load_optional_property
'mcp.llm.base-server-image-digest')"
+ if [[ "${base_server_image}" != *@sha256:* && -n
"${base_server_image_digest}" ]]; then
+
base_server_image="${base_server_image}@${base_server_image_digest}"
+ fi
+ {
+ echo "server_image=$(load_property 'mcp.llm.server-image')"
+ echo "base_server_image=${base_server_image}"
+ echo "server_runtime=$(load_property 'mcp.llm.server-runtime')"
+ echo "model_repository=$(load_property 'mcp.llm.model-repository')"
+ echo "model_quantization=$(load_property
'mcp.llm.model-quantization')"
+ echo "model_reference=$(load_property 'mcp.llm.model')"
+ echo "model_revision=$(load_property 'mcp.llm.model-revision')"
+ echo "model_file_name=$(load_property 'mcp.llm.model-file-name')"
+ echo "model_sha256=$(load_property 'mcp.llm.model-sha256')"
+ } >> "${GITHUB_OUTPUT}"
- name: Build MCP LLM Runtime Image
uses: docker/build-push-action@v6
with:
context: test/e2e/mcp/src/test/resources/docker/llm-runtime
file: test/e2e/mcp/src/test/resources/docker/llm-runtime/Dockerfile
- tags: ${{ env.MCP_LLM_SERVER_IMAGE }}
+ tags: ${{ steps.mcp-e2e-properties.outputs.server_image }}
load: true
+ build-args: |
+ BASE_IMAGE=${{ steps.mcp-e2e-properties.outputs.base_server_image
}}
+ SERVER_RUNTIME=${{ steps.mcp-e2e-properties.outputs.server_runtime
}}
+ MODEL_REPOSITORY=${{
steps.mcp-e2e-properties.outputs.model_repository }}
+ MODEL_QUANTIZATION=${{
steps.mcp-e2e-properties.outputs.model_quantization }}
+ MODEL_REFERENCE=${{
steps.mcp-e2e-properties.outputs.model_reference }}
+ MODEL_REVISION=${{ steps.mcp-e2e-properties.outputs.model_revision
}}
+ MODEL_FILE_NAME=${{
steps.mcp-e2e-properties.outputs.model_file_name }}
+ MODEL_SHA256=${{ steps.mcp-e2e-properties.outputs.model_sha256 }}
cache-from: type=gha,scope=mcp-llm-runtime
cache-to: type=gha,mode=max,scope=mcp-llm-runtime,ignore-error=true
- name: Build MCP E2E Test Dependencies
diff --git
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/config/LLME2EConfiguration.java
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/config/LLME2EConfiguration.java
index 314d7d2559b..90828d1d462 100644
---
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/config/LLME2EConfiguration.java
+++
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/config/LLME2EConfiguration.java
@@ -19,6 +19,7 @@ package org.apache.shardingsphere.test.e2e.mcp.llm.config;
import
org.apache.shardingsphere.test.e2e.env.runtime.EnvironmentPropertiesLoader;
+import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@@ -47,11 +48,9 @@ public final class LLME2EConfiguration {
private static final String DEFAULT_API_KEY = "mcp-llm-score";
- private static final String DEFAULT_SERVER_IMAGE =
"apache/shardingsphere-mcp-llm-runtime:local";
-
- private static final String BASE_SERVER_IMAGE_DIGEST_AMD64 =
"sha256:988d2695631987e28a29d98970aaf0e979e23b843a26824abb790ac4245d1d57";
+ private static final String DEFAULT_SERVER_RUNTIME = "llama.cpp";
- private static final String BASE_SERVER_IMAGE_DIGEST_ARM64 =
"sha256:a478a81b2606aa5bb4c5864c01894fe1d8851adad8b6710f14b9519944d013ca";
+ private static final String DEFAULT_SERVER_IMAGE =
"apache/shardingsphere-mcp-llm-runtime:local";
private final String baseUrl;
@@ -73,10 +72,16 @@ public final class LLME2EConfiguration {
private final RuntimeMode runtimeMode;
+ private final String serverRuntime;
+
private final String serverImage;
+ private final String baseServerImage;
+
private final String baseServerImageDigest;
+ private final ModelMetadata modelMetadata;
+
/**
* Load LLM E2E configuration.
*
@@ -85,6 +90,7 @@ public final class LLME2EConfiguration {
public static LLME2EConfiguration load() {
Properties props = EnvironmentPropertiesLoader.loadProperties();
RuntimeMode runtimeMode = RuntimeMode.from(readString(props,
"mcp.llm.runtime-mode", RuntimeMode.DOCKER.getValue()));
+ ModelMetadata modelMetadata = readModelMetadata(props);
return new LLME2EConfiguration(
normalizeBaseUrl(readString(props, "mcp.llm.base-url",
DEFAULT_BASE_URL)),
readString(props, "mcp.llm.provider", "openai-compatible"),
@@ -96,8 +102,11 @@ public final class LLME2EConfiguration {
Paths.get(readString(props, "mcp.llm.artifact-root",
"target/llm-e2e")),
readString(props, "mcp.llm.run-id", createDefaultRunId()),
runtimeMode,
+ readString(props, "mcp.llm.server-runtime",
DEFAULT_SERVER_RUNTIME),
readString(props, "mcp.llm.server-image",
DEFAULT_SERVER_IMAGE),
- readString(props, "mcp.llm.base-server-image-digest",
getDefaultBaseServerImageDigest(runtimeMode)));
+ readString(props, "mcp.llm.base-server-image", ""),
+ readString(props, "mcp.llm.base-server-image-digest", ""),
+ modelMetadata);
}
/**
@@ -121,7 +130,7 @@ public final class LLME2EConfiguration {
*/
public LLME2EConfiguration withBaseUrl(final String baseUrl) {
return new LLME2EConfiguration(normalizeBaseUrl(baseUrl),
modelProvider, modelName, apiKey, readyTimeoutSeconds, requestTimeoutSeconds,
maxTurns, artifactRoot, runId,
- runtimeMode, serverImage, baseServerImageDigest);
+ runtimeMode, serverRuntime, serverImage, baseServerImage,
baseServerImageDigest, modelMetadata);
}
/**
@@ -133,7 +142,7 @@ public final class LLME2EConfiguration {
*/
public LLME2EConfiguration withModelEndpoint(final String baseUrl, final
String apiKey) {
return new LLME2EConfiguration(normalizeBaseUrl(baseUrl),
modelProvider, modelName, apiKey, readyTimeoutSeconds, requestTimeoutSeconds,
maxTurns, artifactRoot, runId,
- runtimeMode, serverImage, baseServerImageDigest);
+ runtimeMode, serverRuntime, serverImage, baseServerImage,
baseServerImageDigest, modelMetadata);
}
/**
@@ -145,7 +154,7 @@ public final class LLME2EConfiguration {
*/
public LLME2EConfiguration withReadinessTimeouts(final int
readyTimeoutSeconds, final int requestTimeoutSeconds) {
return new LLME2EConfiguration(baseUrl, modelProvider, modelName,
apiKey, readyTimeoutSeconds, requestTimeoutSeconds, maxTurns, artifactRoot,
runId, runtimeMode,
- serverImage, baseServerImageDigest);
+ serverRuntime, serverImage, baseServerImage,
baseServerImageDigest, modelMetadata);
}
/**
@@ -166,11 +175,28 @@ public final class LLME2EConfiguration {
return baseUrl + "/models";
}
+ /**
+ * Get model SHA-256 checksum.
+ *
+ * @return model SHA-256 checksum
+ */
+ public String getModelSha256() {
+ return modelMetadata.getSha256();
+ }
+
private static String readString(final Properties props, final String
propertyName, final String defaultValue) {
String result = props.getProperty(propertyName);
return null == result || result.trim().isEmpty() ? defaultValue :
result.trim();
}
+ private static String readRequiredString(final Properties props, final
String propertyName) {
+ String result = readString(props, propertyName, "");
+ if (result.isEmpty()) {
+ throw new IllegalStateException(String.format("MCP LLM E2E
property `%s` is required.", propertyName));
+ }
+ return result;
+ }
+
private static int readInteger(final Properties props, final String
propertyName, final int defaultValue) {
String result = readString(props, propertyName,
String.valueOf(defaultValue));
try {
@@ -180,6 +206,25 @@ public final class LLME2EConfiguration {
}
}
+ private static long readRequiredLong(final Properties props, final String
propertyName) {
+ String result = readRequiredString(props, propertyName);
+ try {
+ return Long.parseLong(result);
+ } catch (final NumberFormatException ex) {
+ throw new IllegalStateException(String.format("MCP LLM E2E
property `%s` must be a long value.", propertyName), ex);
+ }
+ }
+
+ private static ModelMetadata readModelMetadata(final Properties props) {
+ return new ModelMetadata(
+ readRequiredString(props, "mcp.llm.model-repository"),
+ readRequiredString(props, "mcp.llm.model-file-name"),
+ readRequiredString(props, "mcp.llm.model-quantization"),
+ readRequiredString(props, "mcp.llm.model-revision"),
+ readRequiredLong(props, "mcp.llm.model-size-bytes"),
+ readRequiredString(props, "mcp.llm.model-sha256"));
+ }
+
private static String normalizeBaseUrl(final String baseUrl) {
return baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() -
1) : baseUrl;
}
@@ -188,18 +233,34 @@ public final class LLME2EConfiguration {
return RUN_ID_FORMATTER.format(LocalDateTime.now()) + "-" +
UUID.randomUUID().toString().substring(0, 8);
}
- private static String getDefaultBaseServerImageDigest(final RuntimeMode
runtimeMode) {
- return RuntimeMode.DOCKER == runtimeMode ?
getDefaultBaseServerImageDigest(System.getProperty("os.arch", "")) : "";
- }
-
- static String getDefaultBaseServerImageDigest(final String architecture) {
- if ("amd64".equals(architecture) || "x86_64".equals(architecture)) {
- return BASE_SERVER_IMAGE_DIGEST_AMD64;
- }
- if ("aarch64".equals(architecture) || "arm64".equals(architecture)) {
- return BASE_SERVER_IMAGE_DIGEST_ARM64;
+ /**
+ * LLM model metadata.
+ */
+ @RequiredArgsConstructor
+ @EqualsAndHashCode
+ @Getter
+ public static final class ModelMetadata {
+
+ private final String repository;
+
+ private final String fileName;
+
+ private final String quantization;
+
+ private final String revision;
+
+ private final long sizeBytes;
+
+ private final String sha256;
+
+ /**
+ * Get model path inside the runtime container.
+ *
+ * @return model path
+ */
+ public String getContainerPath() {
+ return "/models/" + fileName;
}
- throw new IllegalStateException(String.format("Unsupported local
architecture for MCP LLM Docker score mode: %s", architecture));
}
/**
diff --git
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/config/LLME2EConfigurationTest.java
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/config/LLME2EConfigurationTest.java
index 2f7fdb69988..fa643877511 100644
---
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/config/LLME2EConfigurationTest.java
+++
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/config/LLME2EConfigurationTest.java
@@ -17,15 +17,18 @@
package org.apache.shardingsphere.test.e2e.mcp.llm.config;
+import
org.apache.shardingsphere.test.e2e.env.runtime.EnvironmentPropertiesLoader;
import
org.apache.shardingsphere.test.e2e.mcp.llm.config.LLME2EConfiguration.RuntimeMode;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.nio.file.Path;
+import java.util.Properties;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
class LLME2EConfigurationTest {
@@ -36,24 +39,57 @@ class LLME2EConfigurationTest {
private String originalApiKey;
+ private String originalReadyTimeoutSeconds;
+
private String originalServerImage;
+ private String originalBaseServerImage;
+
private String originalBaseServerImageDigest;
- private String originalArchitecture;
+ private String originalServerRuntime;
+
+ private String originalModelRepository;
+
+ private String originalModelFileName;
+
+ private String originalModelQuantization;
+
+ private String originalModelRevision;
+
+ private String originalModelSizeBytes;
+
+ private String originalModelSha256;
@BeforeEach
void setUp() {
originalRuntimeMode = System.getProperty("mcp.llm.runtime-mode");
originalModel = System.getProperty("mcp.llm.model");
originalApiKey = System.getProperty("mcp.llm.api-key");
+ originalReadyTimeoutSeconds =
System.getProperty("mcp.llm.ready-timeout-seconds");
originalServerImage = System.getProperty("mcp.llm.server-image");
+ originalBaseServerImage =
System.getProperty("mcp.llm.base-server-image");
originalBaseServerImageDigest =
System.getProperty("mcp.llm.base-server-image-digest");
- originalArchitecture = System.getProperty("os.arch");
+ originalServerRuntime = System.getProperty("mcp.llm.server-runtime");
+ originalModelRepository =
System.getProperty("mcp.llm.model-repository");
+ originalModelFileName = System.getProperty("mcp.llm.model-file-name");
+ originalModelQuantization =
System.getProperty("mcp.llm.model-quantization");
+ originalModelRevision = System.getProperty("mcp.llm.model-revision");
+ originalModelSizeBytes =
System.getProperty("mcp.llm.model-size-bytes");
+ originalModelSha256 = System.getProperty("mcp.llm.model-sha256");
System.clearProperty("mcp.llm.model");
System.clearProperty("mcp.llm.api-key");
+ System.clearProperty("mcp.llm.ready-timeout-seconds");
System.clearProperty("mcp.llm.server-image");
+ System.clearProperty("mcp.llm.base-server-image");
System.clearProperty("mcp.llm.base-server-image-digest");
+ System.clearProperty("mcp.llm.server-runtime");
+ System.clearProperty("mcp.llm.model-repository");
+ System.clearProperty("mcp.llm.model-file-name");
+ System.clearProperty("mcp.llm.model-quantization");
+ System.clearProperty("mcp.llm.model-revision");
+ System.clearProperty("mcp.llm.model-size-bytes");
+ System.clearProperty("mcp.llm.model-sha256");
}
@AfterEach
@@ -61,28 +97,44 @@ class LLME2EConfigurationTest {
restoreProperty("mcp.llm.runtime-mode", originalRuntimeMode);
restoreProperty("mcp.llm.model", originalModel);
restoreProperty("mcp.llm.api-key", originalApiKey);
+ restoreProperty("mcp.llm.ready-timeout-seconds",
originalReadyTimeoutSeconds);
restoreProperty("mcp.llm.server-image", originalServerImage);
+ restoreProperty("mcp.llm.base-server-image", originalBaseServerImage);
restoreProperty("mcp.llm.base-server-image-digest",
originalBaseServerImageDigest);
- restoreProperty("os.arch", originalArchitecture);
+ restoreProperty("mcp.llm.server-runtime", originalServerRuntime);
+ restoreProperty("mcp.llm.model-repository", originalModelRepository);
+ restoreProperty("mcp.llm.model-file-name", originalModelFileName);
+ restoreProperty("mcp.llm.model-quantization",
originalModelQuantization);
+ restoreProperty("mcp.llm.model-revision", originalModelRevision);
+ restoreProperty("mcp.llm.model-size-bytes", originalModelSizeBytes);
+ restoreProperty("mcp.llm.model-sha256", originalModelSha256);
}
@Test
void assertLoadWithDockerRuntimeMode() {
System.setProperty("mcp.llm.runtime-mode", "docker");
- System.setProperty("os.arch", "arm64");
LLME2EConfiguration actual = LLME2EConfiguration.load();
+ Properties expectedProps =
EnvironmentPropertiesLoader.loadProperties();
assertThat(actual.getRuntimeMode(), is(RuntimeMode.DOCKER));
assertThat(actual.getBaseUrl(), is("http://127.0.0.1:8080/v1"));
- assertThat(actual.getModelName(),
is("ggml-org/Qwen3-1.7B-GGUF:Q4_K_M"));
+ String expectedModelReference =
expectedProps.getProperty("mcp.llm.model");
+ assertThat(actual.getModelName(), is(expectedModelReference));
assertThat(actual.getApiKey(), is("mcp-llm-score"));
+ assertThat(actual.getServerRuntime(),
is(expectedProps.getProperty("mcp.llm.server-runtime")));
assertThat(actual.getServerImage(),
is("apache/shardingsphere-mcp-llm-runtime:local"));
- assertThat(actual.getBaseServerImageDigest(),
is("sha256:a478a81b2606aa5bb4c5864c01894fe1d8851adad8b6710f14b9519944d013ca"));
+ assertThat(actual.getBaseServerImage(),
is("ghcr.io/ggml-org/llama.cpp:server"));
+ assertThat(actual.getBaseServerImageDigest(), is(""));
+ assertThat(actual.getModelMetadata().getRepository(),
is(expectedProps.getProperty("mcp.llm.model-repository")));
+ assertThat(actual.getModelMetadata().getFileName(),
is(expectedProps.getProperty("mcp.llm.model-file-name")));
+ assertThat(actual.getModelMetadata().getQuantization(),
is(expectedProps.getProperty("mcp.llm.model-quantization")));
+ assertThat(actual.getModelMetadata().getRevision(),
is(expectedProps.getProperty("mcp.llm.model-revision")));
+ assertThat(actual.getModelMetadata().getSizeBytes(),
is(Long.parseLong(expectedProps.getProperty("mcp.llm.model-size-bytes"))));
+ assertFalse(actual.getModelSha256().isBlank());
}
@Test
void assertLoadWithExternalDebugRuntimeMode() {
System.setProperty("mcp.llm.runtime-mode", "external-debug");
- System.setProperty("os.arch", "riscv64");
LLME2EConfiguration actual = LLME2EConfiguration.load();
assertThat(actual.getRuntimeMode(), is(RuntimeMode.EXTERNAL_DEBUG));
assertThat(actual.getBaseServerImageDigest(), is(""));
@@ -96,21 +148,44 @@ class LLME2EConfigurationTest {
}
@Test
- void assertLoadWithConfiguredServerImage() {
- System.setProperty("mcp.llm.runtime-mode", "docker");
- System.setProperty("mcp.llm.server-image", "foo/mcp-llm-runtime:bar");
- System.setProperty("mcp.llm.base-server-image-digest", "sha256:foo");
+ void assertLoadWithMissingRequiredProperty() {
+ System.setProperty("mcp.llm.model-repository", " ");
+ IllegalStateException actualException =
assertThrows(IllegalStateException.class, LLME2EConfiguration::load);
+ assertThat(actualException.getMessage(), is("MCP LLM E2E property
`mcp.llm.model-repository` is required."));
+ }
+
+ @Test
+ void assertLoadWithInvalidIntegerProperty() {
+ System.setProperty("mcp.llm.ready-timeout-seconds", "invalid-number");
LLME2EConfiguration actual = LLME2EConfiguration.load();
- assertThat(actual.getServerImage(), is("foo/mcp-llm-runtime:bar"));
- assertThat(actual.getBaseServerImageDigest(), is("sha256:foo"));
+ assertThat(actual.getReadyTimeoutSeconds(), is(600));
}
@Test
- void assertLoadWithUnsupportedArchitecture() {
+ void assertLoadWithConfiguredServerImage() {
System.setProperty("mcp.llm.runtime-mode", "docker");
- System.setProperty("os.arch", "riscv64");
- IllegalStateException actualException =
assertThrows(IllegalStateException.class, LLME2EConfiguration::load);
- assertThat(actualException.getMessage(), is("Unsupported local
architecture for MCP LLM Docker score mode: riscv64"));
+ System.setProperty("mcp.llm.server-image",
"test/mcp-llm-runtime:test");
+ System.setProperty("mcp.llm.base-server-image", "test/llama.cpp:test");
+ System.setProperty("mcp.llm.base-server-image-digest",
"test-base-server-image-digest");
+ System.setProperty("mcp.llm.server-runtime", "test-runtime");
+ System.setProperty("mcp.llm.model-repository",
"ggml-org/Qwen3-1.7B-GGUF");
+ System.setProperty("mcp.llm.model-file-name",
"Qwen3-1.7B-Q4_K_M.gguf");
+ System.setProperty("mcp.llm.model-quantization", "Q4_K_M");
+ System.setProperty("mcp.llm.model-revision",
"daeb8e2d528a760970442092f6bf1e55c3b659eb");
+ System.setProperty("mcp.llm.model-size-bytes", "1282439264");
+ System.setProperty("mcp.llm.model-sha256", "configured-model-sha256");
+ LLME2EConfiguration actual = LLME2EConfiguration.load();
+ assertThat(actual.getServerRuntime(), is("test-runtime"));
+ assertThat(actual.getServerImage(), is("test/mcp-llm-runtime:test"));
+ assertThat(actual.getBaseServerImage(), is("test/llama.cpp:test"));
+ assertThat(actual.getBaseServerImageDigest(),
is("test-base-server-image-digest"));
+ assertThat(actual.getModelMetadata().getRepository(),
is("ggml-org/Qwen3-1.7B-GGUF"));
+ assertThat(actual.getModelMetadata().getFileName(),
is("Qwen3-1.7B-Q4_K_M.gguf"));
+ assertThat(actual.getModelMetadata().getQuantization(), is("Q4_K_M"));
+ assertThat(actual.getModelMetadata().getRevision(),
is("daeb8e2d528a760970442092f6bf1e55c3b659eb"));
+ assertThat(actual.getModelMetadata().getSizeBytes(), is(1282439264L));
+ assertThat(actual.getModelName(),
is("ggml-org/Qwen3-1.7B-GGUF:Q4_K_M"));
+ assertThat(actual.getModelSha256(), is("configured-model-sha256"));
}
@Test
@@ -123,10 +198,12 @@ class LLME2EConfigurationTest {
@Test
void assertWithModelEndpoint() {
- LLME2EConfiguration actual =
createConfiguration(RuntimeMode.DOCKER).withModelEndpoint("http://127.0.0.1:8081/v1/",
"foo-key");
+ LLME2EConfiguration actual =
createConfiguration(RuntimeMode.DOCKER).withModelEndpoint("http://127.0.0.1:8081/v1/",
"test-api-key");
assertThat(actual.getBaseUrl(), is("http://127.0.0.1:8081/v1"));
- assertThat(actual.getApiKey(), is("foo-key"));
+ assertThat(actual.getApiKey(), is("test-api-key"));
assertThat(actual.getServerImage(),
is("apache/shardingsphere-mcp-llm-runtime:local"));
+ assertThat(actual.getModelMetadata().getRevision(),
is("daeb8e2d528a760970442092f6bf1e55c3b659eb"));
+ assertThat(actual.getModelSha256(), is("configured-model-sha256"));
}
@Test
@@ -135,12 +212,14 @@ class LLME2EConfigurationTest {
assertThat(actual.getReadyTimeoutSeconds(), is(1));
assertThat(actual.getRequestTimeoutSeconds(), is(2));
assertThat(actual.getRuntimeMode(), is(RuntimeMode.EXTERNAL_DEBUG));
+ assertThat(actual.getBaseServerImage(),
is("ghcr.io/ggml-org/llama.cpp:server"));
}
private LLME2EConfiguration createConfiguration(final RuntimeMode
runtimeMode) {
return new LLME2EConfiguration("http://127.0.0.1:8080/v1",
"openai-compatible", "ggml-org/Qwen3-1.7B-GGUF:Q4_K_M", "mcp-llm-score", 600,
240, 10,
- Path.of("target/llm-e2e"), "run-id", runtimeMode,
"apache/shardingsphere-mcp-llm-runtime:local",
-
"sha256:a478a81b2606aa5bb4c5864c01894fe1d8851adad8b6710f14b9519944d013ca");
+ Path.of("target/llm-e2e"), "run-id", runtimeMode, "llama.cpp",
"apache/shardingsphere-mcp-llm-runtime:local",
"ghcr.io/ggml-org/llama.cpp:server", "",
+ new
LLME2EConfiguration.ModelMetadata("ggml-org/Qwen3-1.7B-GGUF",
"Qwen3-1.7B-Q4_K_M.gguf", "Q4_K_M", "daeb8e2d528a760970442092f6bf1e55c3b659eb",
1282439264L,
+ "configured-model-sha256"));
}
private void restoreProperty(final String name, final String value) {
diff --git
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/artifact/LLME2EArtifactWriter.java
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/artifact/LLME2EArtifactWriter.java
index b3738e8e7a6..e2292160636 100644
---
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/artifact/LLME2EArtifactWriter.java
+++
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/artifact/LLME2EArtifactWriter.java
@@ -38,7 +38,7 @@ public final class LLME2EArtifactWriter {
private static final Pattern ENV_SECRET_ASSIGNMENT_PATTERN =
Pattern.compile("(?i)((?:MCP_LLM_API_KEY|HF_TOKEN|HUGGING_FACE_HUB_TOKEN|LLAMA_API_KEY)\\s*=\\s*)\\S+");
private static final List<String> REQUIRED_SCORE_EVIDENCE_KEYS = List.of(
- "runtimeMode", "dockerOwned", "provider", "serverRuntime",
"serverImage", "serverImageId", "baseServerImageDigest", "modelReference",
"servedModelId",
+ "runtimeMode", "dockerOwned", "provider", "serverRuntime",
"serverImage", "serverImageId", "baseServerImage", "modelReference",
"servedModelId",
"modelQuantization", "modelSizeBytes", "modelRevision",
"modelFileName", "modelSha256", "modelPackaging", "baseUrlOwnedByTest",
"scoreClosing");
/**
diff --git
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/artifact/LLME2EArtifactWriterTest.java
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/artifact/LLME2EArtifactWriterTest.java
index d9f0df8b195..8a8cf3c50ef 100644
---
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/artifact/LLME2EArtifactWriterTest.java
+++
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/artifact/LLME2EArtifactWriterTest.java
@@ -55,6 +55,7 @@ class LLME2EArtifactWriterTest {
assertTrue((boolean)
castToMap(runContext.get("runtime")).get("dockerOwned"));
assertThat(castToMap(runContext.get("runtime")).get("serverRuntime"),
is("llama.cpp"));
assertThat(castToMap(runContext.get("runtime")).get("serverImage"),
is("apache/shardingsphere-mcp-llm-runtime:local"));
+
assertThat(castToMap(runContext.get("runtime")).get("baseServerImage"),
is("ghcr.io/ggml-org/llama.cpp:server"));
assertThat(castToMap(runContext.get("runtime")).get("modelPackaging"),
is("prepackaged"));
assertThat(Files.readString(tempDir.resolve("raw-model-output.txt")),
is("{\"token\":\"<redacted>\"}"));
assertThat(Files.readString(tempDir.resolve("mcp-runtime.log")),
is("Authorization: Bearer <redacted>" + System.lineSeparator() +
"MCP_LLM_API_KEY=<redacted>"));
@@ -77,15 +78,16 @@ class LLME2EArtifactWriterTest {
Map.entry("provider", "openai-compatible"),
Map.entry("serverRuntime", "llama.cpp"),
Map.entry("serverImage",
"apache/shardingsphere-mcp-llm-runtime:local"),
- Map.entry("serverImageId", "sha256:image"),
- Map.entry("baseServerImageDigest", "sha256:base"),
+ Map.entry("serverImageId", "test-server-image-id"),
+ Map.entry("baseServerImage",
"ghcr.io/ggml-org/llama.cpp:server"),
+ Map.entry("baseServerImageDigest",
"test-base-server-image-digest"),
Map.entry("modelReference", MODEL_NAME),
Map.entry("servedModelId", MODEL_NAME),
Map.entry("modelQuantization", "Q4_K_M"),
Map.entry("modelSizeBytes", 1282439264L),
Map.entry("modelRevision",
"daeb8e2d528a760970442092f6bf1e55c3b659eb"),
Map.entry("modelFileName", "Qwen3-1.7B-Q4_K_M.gguf"),
- Map.entry("modelSha256",
"d2387ca2dbfee2ffabce7120d3770dadca0b293052bc2f0e138fdc940d9bc7b5"),
+ Map.entry("modelSha256", "configured-model-sha256"),
Map.entry("modelPackaging", "prepackaged"),
Map.entry("baseUrlOwnedByTest", true),
Map.entry("scoreClosing", true));
diff --git
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/client/LLMChatModelClientTest.java
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/client/LLMChatModelClientTest.java
index 2b93844ae56..4ad955568c0 100644
---
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/client/LLMChatModelClientTest.java
+++
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/client/LLMChatModelClientTest.java
@@ -51,6 +51,9 @@ class LLMChatModelClientTest {
private static final String REQUIRED_MODEL =
"ggml-org/Qwen3-1.7B-GGUF:Q4_K_M";
+ private static final LLME2EConfiguration.ModelMetadata MODEL_METADATA =
new LLME2EConfiguration.ModelMetadata(
+ "ggml-org/Qwen3-1.7B-GGUF", "Qwen3-1.7B-Q4_K_M.gguf", "Q4_K_M",
"daeb8e2d528a760970442092f6bf1e55c3b659eb", 1282439264L,
"configured-model-sha256");
+
@Test
void assertWaitUntilReady() throws IOException, InterruptedException {
List<String> actualBodies = new LinkedList<>();
@@ -70,14 +73,14 @@ class LLMChatModelClientTest {
@Test
void assertWaitUntilReadyReportsProbeFailure() throws IOException,
InterruptedException {
HttpClient httpClient = mock(HttpClient.class);
- HttpResponse<String> modelListResponse = createResponse(200,
"{\"data\":[{\"id\":\"ggml-org/Qwen3-1.7B-GGUF:Q4_K_M\"}]}");
+ HttpResponse<String> modelListResponse = createResponse(200,
String.format("{\"data\":[{\"id\":\"%s\"}]}", REQUIRED_MODEL));
HttpResponse<String> completionResponse = createResponse(401,
"{\"error\":{\"code\":\"unauthorized\"}}");
when(httpClient.send(any(HttpRequest.class),
ArgumentMatchers.<HttpResponse.BodyHandler<String>>any()))
.thenReturn(modelListResponse, completionResponse);
IllegalStateException actualException =
assertThrows(IllegalStateException.class,
() -> new
LLMChatModelClient(createConfiguration("http://127.0.0.1:8080/v1", 1),
httpClient).waitUntilReady());
assertTrue(actualException.getMessage().startsWith(
- "Model service is not ready for
`ggml-org/Qwen3-1.7B-GGUF:Q4_K_M` after 1 readiness attempt(s),
elapsedMillis="));
+ String.format("Model service is not ready for `%s` after 1
readiness attempt(s), elapsedMillis=", REQUIRED_MODEL)));
assertTrue(actualException.getMessage().endsWith(
"timeoutSeconds=1. Last readiness failure: completion
readiness request returned HTTP 401 with error code `unauthorized`."));
}
@@ -91,7 +94,7 @@ class LLMChatModelClientTest {
List.of(
LLMChatMessage.system("system"),
LLMChatMessage.user("user"),
- LLMChatMessage.assistant("", List.of(new
LLMToolCall("call_0", "mcp_read_resource", "{\"uri\":\"mcp://foo\"}"))),
+ LLMChatMessage.assistant("", List.of(new
LLMToolCall("call_0", "mcp_read_resource",
"{\"uri\":\"mcp://test-resource\"}"))),
LLMChatMessage.tool("call_0", "tool result")),
createToolDefinitions(), "required", true);
assertThat(actual.getContent(), is("done"));
@@ -159,7 +162,8 @@ class LLMChatModelClientTest {
private LLME2EConfiguration createConfiguration(final String baseUrl,
final int readyTimeoutSeconds) {
return new LLME2EConfiguration(baseUrl, "openai-compatible",
REQUIRED_MODEL, "mcp-llm-score", readyTimeoutSeconds, 30, 10,
- Path.of("target/llm-e2e"), "run-id", RuntimeMode.DOCKER,
"apache/shardingsphere-mcp-llm-runtime:local", "sha256:foo");
+ Path.of("target/llm-e2e"), "run-id", RuntimeMode.DOCKER,
"llama.cpp", "apache/shardingsphere-mcp-llm-runtime:local",
"ghcr.io/ggml-org/llama.cpp:server",
+ "test-base-server-image-digest", MODEL_METADATA);
}
private HttpServer startModelServer(final String modelName, final
List<String> requestBodies) throws IOException {
diff --git
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/fixture/LLMRuntimeSupport.java
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/fixture/LLMRuntimeSupport.java
index 1345eaea489..ba581aad655 100644
---
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/fixture/LLMRuntimeSupport.java
+++
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/fixture/LLMRuntimeSupport.java
@@ -44,24 +44,6 @@ public final class LLMRuntimeSupport {
private static final String REQUIRED_PROVIDER = "openai-compatible";
- private static final String REQUIRED_MODEL =
"ggml-org/Qwen3-1.7B-GGUF:Q4_K_M";
-
- private static final String SERVER_RUNTIME = "llama.cpp";
-
- private static final String MODEL_FILE_NAME = "Qwen3-1.7B-Q4_K_M.gguf";
-
- private static final String MODEL_PATH = "/models/" + MODEL_FILE_NAME;
-
- private static final String MODEL_QUANTIZATION = "Q4_K_M";
-
- private static final String MODEL_REVISION =
"daeb8e2d528a760970442092f6bf1e55c3b659eb";
-
- private static final String MODEL_SHA256 =
"d2387ca2dbfee2ffabce7120d3770dadca0b293052bc2f0e138fdc940d9bc7b5";
-
- private static final long MODEL_SIZE_BYTES = 1282439264L;
-
- private static final String SCORE_API_KEY = "mcp-llm-score";
-
private static final int SERVER_PORT = 8080;
private static ModelRuntime sharedContainerRuntime;
@@ -78,14 +60,13 @@ public final class LLMRuntimeSupport {
if (RuntimeMode.EXTERNAL_DEBUG == config.getRuntimeMode()) {
return prepareExternalDebugRuntime(config);
}
- validateRequiredModel(config);
if (null != sharedContainerRuntime &&
sharedContainerRuntime.isReusable(config)) {
return sharedContainerRuntime;
}
stopSharedRuntime();
requireDockerAvailable();
String serverImageId =
requireScoreImageAvailable(config.getServerImage());
- GenericContainer<?> container =
createContainer(config.getServerImage());
+ GenericContainer<?> container = createContainer(config);
container.start();
LLME2EConfiguration actualConfig =
createDockerRuntimeConfiguration(config, container);
new LLMChatModelClient(actualConfig,
HttpClient.newHttpClient()).waitUntilReady();
@@ -107,12 +88,6 @@ public final class LLMRuntimeSupport {
}
}
- private static void validateRequiredModel(final LLME2EConfiguration
config) {
- if (!REQUIRED_MODEL.equals(config.getModelName())) {
- throw new IllegalStateException("MCP LLM Docker score mode
requires model ggml-org/Qwen3-1.7B-GGUF:Q4_K_M.");
- }
- }
-
private static boolean isModelReady(final LLME2EConfiguration config)
throws InterruptedException {
try {
new LLMChatModelClient(config.withReadinessTimeouts(2, 2),
HttpClient.newHttpClient()).waitUntilReady();
@@ -145,19 +120,19 @@ public final class LLMRuntimeSupport {
}
}
- private static GenericContainer<?> createContainer(final String
serverImage) {
- return new GenericContainer<>(DockerImageName.parse(serverImage))
+ private static GenericContainer<?> createContainer(final
LLME2EConfiguration config) {
+ return new
GenericContainer<>(DockerImageName.parse(config.getServerImage()))
.withImagePullPolicy(imageName -> false)
.withExposedPorts(SERVER_PORT)
- .withCommand("--host", "0.0.0.0", "--port",
String.valueOf(SERVER_PORT), "-m", MODEL_PATH, "--alias", REQUIRED_MODEL,
+ .withCommand("--host", "0.0.0.0", "--port",
String.valueOf(SERVER_PORT), "-m",
config.getModelMetadata().getContainerPath(), "--alias", config.getModelName(),
"--jinja", "--reasoning", "off", "--reasoning-budget",
"0", "--chat-template-kwargs", "{\"enable_thinking\":false}",
- "--api-key", SCORE_API_KEY, "--no-ui", "-n", "512",
"--parallel", "1", "-c", "2048", "-b", "256", "-ub", "128", "--cache-ram", "0",
"--no-cache-prompt")
+ "--api-key", config.getApiKey(), "--no-ui", "-n",
"512", "--parallel", "1", "-c", "2048", "-b", "256", "-ub", "128",
"--cache-ram", "0", "--no-cache-prompt")
.waitingFor(Wait.forListeningPort())
.withStartupTimeout(Duration.ofMinutes(5));
}
private static LLME2EConfiguration createDockerRuntimeConfiguration(final
LLME2EConfiguration config, final GenericContainer<?> container) {
- return config.withModelEndpoint(String.format("http://%s:%d/v1",
container.getHost(), container.getMappedPort(SERVER_PORT)), SCORE_API_KEY);
+ return config.withModelEndpoint(String.format("http://%s:%d/v1",
container.getHost(), container.getMappedPort(SERVER_PORT)), config.getApiKey());
}
private static void registerShutdownHook(final ModelRuntime runtime) {
@@ -200,21 +175,22 @@ public final class LLMRuntimeSupport {
}
private static Map<String, Object> createScoreClosingEvidence(final
LLME2EConfiguration config, final String serverImageId) {
- Map<String, Object> result = new LinkedHashMap<>(16, 1F);
+ Map<String, Object> result = new LinkedHashMap<>(18, 1F);
result.put("runtimeMode", config.getRuntimeMode().getValue());
result.put("dockerOwned", true);
result.put("provider", config.getModelProvider());
- result.put("serverRuntime", SERVER_RUNTIME);
+ result.put("serverRuntime", config.getServerRuntime());
result.put("serverImage", config.getServerImage());
result.put("serverImageId", serverImageId);
+ result.put("baseServerImage", config.getBaseServerImage());
result.put("baseServerImageDigest",
config.getBaseServerImageDigest());
- result.put("modelReference", REQUIRED_MODEL);
- result.put("servedModelId", REQUIRED_MODEL);
- result.put("modelQuantization", MODEL_QUANTIZATION);
- result.put("modelSizeBytes", MODEL_SIZE_BYTES);
- result.put("modelRevision", MODEL_REVISION);
- result.put("modelFileName", MODEL_FILE_NAME);
- result.put("modelSha256", MODEL_SHA256);
+ result.put("modelReference", config.getModelName());
+ result.put("servedModelId", config.getModelName());
+ result.put("modelQuantization",
config.getModelMetadata().getQuantization());
+ result.put("modelSizeBytes",
config.getModelMetadata().getSizeBytes());
+ result.put("modelRevision",
config.getModelMetadata().getRevision());
+ result.put("modelFileName",
config.getModelMetadata().getFileName());
+ result.put("modelSha256", config.getModelSha256());
result.put("modelPackaging", "prepackaged");
result.put("baseUrlOwnedByTest", true);
result.put("scoreClosing", true);
@@ -233,8 +209,12 @@ public final class LLMRuntimeSupport {
return null != container && container.isRunning()
&& configuration.getRuntimeMode() ==
config.getRuntimeMode()
&&
configuration.getModelName().equals(config.getModelName())
+ && configuration.getApiKey().equals(config.getApiKey())
+ &&
configuration.getServerRuntime().equals(config.getServerRuntime())
&&
configuration.getServerImage().equals(config.getServerImage())
- &&
configuration.getBaseServerImageDigest().equals(config.getBaseServerImageDigest());
+ &&
configuration.getBaseServerImage().equals(config.getBaseServerImage())
+ &&
configuration.getBaseServerImageDigest().equals(config.getBaseServerImageDigest())
+ &&
configuration.getModelMetadata().equals(config.getModelMetadata());
}
private void stop() {
diff --git
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/fixture/LLMRuntimeSupportTest.java
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/fixture/LLMRuntimeSupportTest.java
index 73c5d2a0976..58b91eaa240 100644
---
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/fixture/LLMRuntimeSupportTest.java
+++
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/fixture/LLMRuntimeSupportTest.java
@@ -41,6 +41,9 @@ class LLMRuntimeSupportTest {
private static final String REQUIRED_MODEL =
"ggml-org/Qwen3-1.7B-GGUF:Q4_K_M";
+ private static final LLME2EConfiguration.ModelMetadata MODEL_METADATA =
new LLME2EConfiguration.ModelMetadata(
+ "ggml-org/Qwen3-1.7B-GGUF", "Qwen3-1.7B-Q4_K_M.gguf", "Q4_K_M",
"daeb8e2d528a760970442092f6bf1e55c3b659eb", 1282439264L,
"configured-model-sha256");
+
private static final String DOCKER_REQUIRED_MESSAGE = "Docker is required
to start the prepackaged llama.cpp server for MCP LLM E2E.";
@Test
@@ -82,21 +85,15 @@ class LLMRuntimeSupportTest {
@Test
void assertPrepareWithUnsupportedProvider() {
LLME2EConfiguration config = new
LLME2EConfiguration("http://127.0.0.1:8080/v1", "openai", REQUIRED_MODEL,
"mcp-llm-score", 600, 240, 10,
- Path.of("target/llm-e2e"), "run-id", RuntimeMode.DOCKER,
"apache/shardingsphere-mcp-llm-runtime:local", "sha256:foo");
+ Path.of("target/llm-e2e"), "run-id", RuntimeMode.DOCKER,
"llama.cpp", "apache/shardingsphere-mcp-llm-runtime:local",
"ghcr.io/ggml-org/llama.cpp:server",
+ "test-base-server-image-digest", MODEL_METADATA);
IllegalStateException actualException =
assertThrows(IllegalStateException.class, () ->
LLMRuntimeSupport.prepare(config));
assertThat(actualException.getMessage(), is("MCP LLM E2E requires
provider openai-compatible."));
}
- @Test
- void assertPrepareWithUnsupportedDockerModel() {
- IllegalStateException actualException =
assertThrows(IllegalStateException.class,
- () ->
LLMRuntimeSupport.prepare(createConfiguration(RuntimeMode.DOCKER,
"debug-model", "http://127.0.0.1:8080/v1")));
- assertThat(actualException.getMessage(), is("MCP LLM Docker score mode
requires model ggml-org/Qwen3-1.7B-GGUF:Q4_K_M."));
- }
-
private LLME2EConfiguration createConfiguration(final RuntimeMode
runtimeMode, final String modelName, final String baseUrl) {
return new LLME2EConfiguration(baseUrl, "openai-compatible",
modelName, "mcp-llm-score", 2, 2, 10, Path.of("target/llm-e2e"), "run-id",
- runtimeMode, "apache/shardingsphere-mcp-llm-runtime:local",
"sha256:foo");
+ runtimeMode, "llama.cpp",
"apache/shardingsphere-mcp-llm-runtime:local",
"ghcr.io/ggml-org/llama.cpp:server", "test-base-server-image-digest",
MODEL_METADATA);
}
private HttpServer startModelServer(final String modelName) throws
IOException {
diff --git
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/runtime/MySQLRuntimeTestSupport.java
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/runtime/MySQLRuntimeTestSupport.java
index 089d27cadae..8b35d27c5cc 100644
---
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/runtime/MySQLRuntimeTestSupport.java
+++
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/runtime/MySQLRuntimeTestSupport.java
@@ -39,6 +39,7 @@ import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
+import java.util.Properties;
/**
* E2E-local MySQL-backed runtime test support.
@@ -76,7 +77,7 @@ public final class MySQLRuntimeTestSupport {
* @return MySQL runtime container
*/
public static GenericContainer<?> createContainer() {
- return new GenericContainer<>(DockerImageName.parse("mysql:8.0.36"))
+ return new GenericContainer<>(DockerImageName.parse(getMySQLImage()))
.withEnv("MYSQL_ROOT_PASSWORD", ROOT_PASSWORD)
.withEnv("MYSQL_DATABASE", DATABASE_NAME)
.withEnv("MYSQL_USER", USERNAME)
@@ -86,6 +87,18 @@ public final class MySQLRuntimeTestSupport {
.withStartupTimeout(Duration.ofMinutes(2));
}
+ private static String getMySQLImage() {
+ return getMySQLImage(EnvironmentPropertiesLoader.loadProperties());
+ }
+
+ static String getMySQLImage(final Properties props) {
+ String result = props.getProperty("mcp.e2e.mysql.image", "").trim();
+ if (result.isEmpty()) {
+ throw new IllegalStateException("MCP E2E MySQL image property
`mcp.e2e.mysql.image` is required.");
+ }
+ return result;
+ }
+
/**
* Check whether Docker is available for Testcontainers-backed tests.
*
diff --git
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/runtime/MySQLRuntimeTestSupportTest.java
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/runtime/MySQLRuntimeTestSupportTest.java
index eb7ea9c1354..648e96e474a 100644
---
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/runtime/MySQLRuntimeTestSupportTest.java
+++
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/runtime/MySQLRuntimeTestSupportTest.java
@@ -19,8 +19,11 @@ package
org.apache.shardingsphere.test.e2e.mcp.support.runtime;
import org.junit.jupiter.api.Test;
+import java.util.Properties;
+
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertThrows;
class MySQLRuntimeTestSupportTest {
@@ -34,4 +37,17 @@ class MySQLRuntimeTestSupportTest {
void assertCreateDockerRequiredMessageWithoutReadinessDiagnostic() {
assertThat(MySQLRuntimeTestSupport.createDockerRequiredMessage("Docker
is required.", ""), is("Docker is required."));
}
+
+ @Test
+ void assertGetMySQLImage() {
+ Properties props = new Properties();
+ props.setProperty("mcp.e2e.mysql.image", "mysql:8.4.0");
+ assertThat(MySQLRuntimeTestSupport.getMySQLImage(props),
is("mysql:8.4.0"));
+ }
+
+ @Test
+ void assertGetMySQLImageWithMissingProperty() {
+ IllegalStateException actualException =
assertThrows(IllegalStateException.class, () ->
MySQLRuntimeTestSupport.getMySQLImage(new Properties()));
+ assertThat(actualException.getMessage(), is("MCP E2E MySQL image
property `mcp.e2e.mysql.image` is required."));
+ }
}
diff --git a/test/e2e/mcp/src/test/resources/docker/llm-runtime/Dockerfile
b/test/e2e/mcp/src/test/resources/docker/llm-runtime/Dockerfile
index 1b81a49c66c..418ddea40a6 100644
--- a/test/e2e/mcp/src/test/resources/docker/llm-runtime/Dockerfile
+++ b/test/e2e/mcp/src/test/resources/docker/llm-runtime/Dockerfile
@@ -16,21 +16,30 @@
# limitations under the License.
#
-ARG
BASE_IMAGE=ghcr.io/ggml-org/llama.cpp@sha256:988d2695631987e28a29d98970aaf0e979e23b843a26824abb790ac4245d1d57
+ARG BASE_IMAGE
FROM ${BASE_IMAGE}
+ARG SERVER_RUNTIME
+ARG MODEL_REPOSITORY
+ARG MODEL_QUANTIZATION
+ARG MODEL_REFERENCE
+ARG MODEL_REVISION
+ARG MODEL_FILE_NAME
+ARG MODEL_SHA256
LABEL org.opencontainers.image.title="Apache ShardingSphere MCP LLM E2E
Runtime"
-LABEL org.opencontainers.image.description="Prepackaged llama.cpp server plus
Qwen3 1.7B Q4_K_M GGUF for MCP LLM E2E score evidence"
+LABEL org.opencontainers.image.description="Prepackaged model server for MCP
LLM E2E score evidence"
LABEL org.opencontainers.image.licenses="Apache-2.0"
-LABEL org.apache.shardingsphere.mcp.llm.server-runtime="llama.cpp"
-LABEL
org.apache.shardingsphere.mcp.llm.model-reference="ggml-org/Qwen3-1.7B-GGUF:Q4_K_M"
-LABEL
org.apache.shardingsphere.mcp.llm.model-revision="daeb8e2d528a760970442092f6bf1e55c3b659eb"
-LABEL
org.apache.shardingsphere.mcp.llm.model-sha256="d2387ca2dbfee2ffabce7120d3770dadca0b293052bc2f0e138fdc940d9bc7b5"
+LABEL org.apache.shardingsphere.mcp.llm.server-runtime="${SERVER_RUNTIME}"
+LABEL org.apache.shardingsphere.mcp.llm.model-repository="${MODEL_REPOSITORY}"
+LABEL
org.apache.shardingsphere.mcp.llm.model-quantization="${MODEL_QUANTIZATION}"
+LABEL org.apache.shardingsphere.mcp.llm.model-reference="${MODEL_REFERENCE}"
+LABEL org.apache.shardingsphere.mcp.llm.model-revision="${MODEL_REVISION}"
+LABEL org.apache.shardingsphere.mcp.llm.model-sha256="${MODEL_SHA256}"
ENV LD_LIBRARY_PATH=/app
-ADD
--checksum=sha256:d2387ca2dbfee2ffabce7120d3770dadca0b293052bc2f0e138fdc940d9bc7b5
\
-
https://huggingface.co/ggml-org/Qwen3-1.7B-GGUF/resolve/daeb8e2d528a760970442092f6bf1e55c3b659eb/Qwen3-1.7B-Q4_K_M.gguf
\
- /models/Qwen3-1.7B-Q4_K_M.gguf
+ADD --checksum=sha256:${MODEL_SHA256} \
+
https://huggingface.co/${MODEL_REPOSITORY}/resolve/${MODEL_REVISION}/${MODEL_FILE_NAME}
\
+ /models/${MODEL_FILE_NAME}
EXPOSE 8080
diff --git a/test/e2e/mcp/src/test/resources/docker/llm-runtime/build-local.sh
b/test/e2e/mcp/src/test/resources/docker/llm-runtime/build-local.sh
index b223e2cbe5c..5d4bba5e393 100644
--- a/test/e2e/mcp/src/test/resources/docker/llm-runtime/build-local.sh
+++ b/test/e2e/mcp/src/test/resources/docker/llm-runtime/build-local.sh
@@ -44,29 +44,62 @@ case "${MODE}" in
;;
esac
-IMAGE_TAG="${MCP_LLM_SERVER_IMAGE:-apache/shardingsphere-mcp-llm-runtime:local}"
-ARCHITECTURE="${MCP_LLM_ARCHITECTURE:-$(uname -m)}"
SCRIPT_DIR="$(CDPATH= cd "$(dirname "$0")" && pwd -P)"
DOCKERFILE_PATH="${SCRIPT_DIR}/Dockerfile"
+ENV_FILE="${SCRIPT_DIR}/../../env/e2e-env.properties"
-case "${ARCHITECTURE}" in
- amd64|x86_64)
-
BASE_DIGEST="sha256:988d2695631987e28a29d98970aaf0e979e23b843a26824abb790ac4245d1d57"
- ;;
- arm64|aarch64)
-
BASE_DIGEST="sha256:a478a81b2606aa5bb4c5864c01894fe1d8851adad8b6710f14b9519944d013ca"
+if [ ! -f "${ENV_FILE}" ]; then
+ echo "MCP E2E environment properties file is required: ${ENV_FILE}" >&2
+ exit 1
+fi
+
+read_property() {
+ awk -F= -v key="$1" '$1 == key {sub(/^[^=]*=/, ""); print; found = 1; exit}
END {if (!found) exit 1}' "${ENV_FILE}"
+}
+
+read_required_property() {
+ PROPERTY_VALUE="$(read_property "$1" || true)"
+ if [ -z "${PROPERTY_VALUE}" ]; then
+ echo "MCP E2E property is required: $1" >&2
+ exit 1
+ fi
+ echo "${PROPERTY_VALUE}"
+}
+
+read_optional_property() {
+ read_property "$1" || true
+}
+
+IMAGE_TAG="${MCP_LLM_SERVER_IMAGE:-$(read_required_property
"mcp.llm.server-image")}"
+BASE_IMAGE="${MCP_LLM_BASE_SERVER_IMAGE:-$(read_required_property
"mcp.llm.base-server-image")}"
+BASE_DIGEST="${MCP_LLM_BASE_SERVER_IMAGE_DIGEST:-$(read_optional_property
"mcp.llm.base-server-image-digest")}"
+SERVER_RUNTIME="${MCP_LLM_SERVER_RUNTIME:-$(read_required_property
"mcp.llm.server-runtime")}"
+MODEL_REPOSITORY="${MCP_LLM_MODEL_REPOSITORY:-$(read_required_property
"mcp.llm.model-repository")}"
+MODEL_QUANTIZATION="${MCP_LLM_MODEL_QUANTIZATION:-$(read_required_property
"mcp.llm.model-quantization")}"
+MODEL_REFERENCE="${MCP_LLM_MODEL:-$(read_required_property "mcp.llm.model")}"
+MODEL_REVISION="${MCP_LLM_MODEL_REVISION:-$(read_required_property
"mcp.llm.model-revision")}"
+MODEL_FILE_NAME="${MCP_LLM_MODEL_FILE_NAME:-$(read_required_property
"mcp.llm.model-file-name")}"
+MODEL_SHA256="${MCP_LLM_MODEL_SHA256:-$(read_required_property
"mcp.llm.model-sha256")}"
+
+case "${BASE_IMAGE}" in
+ *@sha256:*)
;;
*)
- echo "Unsupported local architecture for MCP LLM Docker score mode:
${ARCHITECTURE}" >&2
- exit 1
+ if [ -n "${BASE_DIGEST}" ]; then
+ BASE_IMAGE="${BASE_IMAGE}@${BASE_DIGEST}"
+ fi
;;
esac
-BASE_IMAGE="ghcr.io/ggml-org/llama.cpp@${BASE_DIGEST}"
-
if [ "--dry-run" = "${MODE}" ] || [ "--print" = "${MODE}" ]; then
- echo "architecture=${ARCHITECTURE}"
echo "base_image=${BASE_IMAGE}"
+ echo "server_runtime=${SERVER_RUNTIME}"
+ echo "model_repository=${MODEL_REPOSITORY}"
+ echo "model_quantization=${MODEL_QUANTIZATION}"
+ echo "model_reference=${MODEL_REFERENCE}"
+ echo "model_revision=${MODEL_REVISION}"
+ echo "model_file_name=${MODEL_FILE_NAME}"
+ echo "model_sha256=${MODEL_SHA256}"
echo "image_tag=${IMAGE_TAG}"
echo "dockerfile=${DOCKERFILE_PATH}"
echo "context=${SCRIPT_DIR}"
@@ -80,6 +113,13 @@ fi
docker build \
--build-arg "BASE_IMAGE=${BASE_IMAGE}" \
+ --build-arg "SERVER_RUNTIME=${SERVER_RUNTIME}" \
+ --build-arg "MODEL_REPOSITORY=${MODEL_REPOSITORY}" \
+ --build-arg "MODEL_QUANTIZATION=${MODEL_QUANTIZATION}" \
+ --build-arg "MODEL_REFERENCE=${MODEL_REFERENCE}" \
+ --build-arg "MODEL_REVISION=${MODEL_REVISION}" \
+ --build-arg "MODEL_FILE_NAME=${MODEL_FILE_NAME}" \
+ --build-arg "MODEL_SHA256=${MODEL_SHA256}" \
-t "${IMAGE_TAG}" \
-f "${DOCKERFILE_PATH}" \
"${SCRIPT_DIR}"
diff --git a/test/e2e/mcp/src/test/resources/env/e2e-env.properties
b/test/e2e/mcp/src/test/resources/env/e2e-env.properties
index 0e769a0ea69..4b220eb91d7 100644
--- a/test/e2e/mcp/src/test/resources/env/e2e-env.properties
+++ b/test/e2e/mcp/src/test/resources/env/e2e-env.properties
@@ -21,6 +21,7 @@ e2e.timezone=UTC
e2e.run.type=
mcp.e2e.container.image=
+mcp.e2e.mysql.image=mysql:8.0.36
mcp.e2e.mysql.ready-timeout-seconds=90
mcp.distribution.home=
@@ -34,5 +35,13 @@ mcp.llm.request-timeout-seconds=240
mcp.llm.max-turns=10
mcp.llm.artifact-root=target/llm-e2e
mcp.llm.run-id=
+mcp.llm.server-runtime=llama.cpp
mcp.llm.server-image=apache/shardingsphere-mcp-llm-runtime:local
+mcp.llm.base-server-image=ghcr.io/ggml-org/llama.cpp:server
mcp.llm.base-server-image-digest=
+mcp.llm.model-repository=ggml-org/Qwen3-1.7B-GGUF
+mcp.llm.model-quantization=Q4_K_M
+mcp.llm.model-revision=daeb8e2d528a760970442092f6bf1e55c3b659eb
+mcp.llm.model-file-name=Qwen3-1.7B-Q4_K_M.gguf
+mcp.llm.model-size-bytes=1282439264
+mcp.llm.model-sha256=d2387ca2dbfee2ffabce7120d3770dadca0b293052bc2f0e138fdc940d9bc7b5