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

Reply via email to