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 709596fea5e Add mcp support unit coverage (#38813)
709596fea5e is described below

commit 709596fea5e2e836c1f3eae49789042d5078dd07
Author: Liang Zhang <[email protected]>
AuthorDate: Fri Jun 5 21:30:52 2026 +0800

    Add mcp support unit coverage (#38813)
    
    Add focused unit tests for MCP E2E support logic that can be validated 
without
    running end-to-end scenarios, including LLM conversation helpers, transport
    protocol utilities, fixture handlers, custom algorithms, and distribution
    fixture support.
---
 .../mcp/llm/config/LLME2EConfigurationTest.java    |  34 ++-
 .../llm/conversation/LLMMCPActionExecutorTest.java | 186 +++++++++++++++
 .../LLMMCPConversationArtifactsTest.java           | 121 ++++++++++
 .../LLMMCPFinalAnswerValidatorTest.java            | 138 +++++++++++
 .../LLMMCPInteractionCoverageTest.java             |  56 +++++
 .../mcp/llm/conversation/LLMMCPJsonValuesTest.java |  65 ++++++
 ...LLMMCPModelFacingToolResponseFormatterTest.java |  76 +++++-
 ...rmatterTest.java => LLMMCPNextActionsTest.java} |  30 +--
 .../conversation/LLMMCPScenarioInferenceTest.java  |  84 +++++++
 .../LLMMCPSideEffectNextActionTest.java            |  44 ++++
 .../conversation/LLMMCPToolCallNormalizerTest.java | 100 ++++++++
 .../LLMMCPToolDefinitionFactoryTest.java           |   7 +
 .../mcp/llm/scenario/LLMStructuredAnswerTest.java  |  75 ++++++
 .../usability/LLMUsabilitySuiteRunnerTest.java     |  91 ++++++++
 ...ckagedDistributionPluginFixtureSupportTest.java |  14 ++
 ...PWorkflowCustomEncryptAlgorithmFixtureTest.java |  65 ++++++
 .../MCPWorkflowCustomMaskAlgorithmFixtureTest.java |  36 +++
 .../plugin/PluginFixtureHandlerProviderTest.java   |  36 +++
 .../plugin/PluginFixturePingToolHandlerTest.java   |  46 ++++
 .../PluginFixtureStatusResourceHandlerTest.java    |  47 ++++
 .../MCPInteractionProtocolSupportTest.java         |  65 ++++++
 .../transport/MCPInteractionTraceRecordTest.java   |  69 ++++++
 .../transport/MCPPayloadAssertionsTest.java        |  85 +++++++
 .../client/AbstractMCPInteractionClientTest.java   | 159 +++++++++++++
 .../client/MCPHttpInteractionClientTest.java       | 254 +++++++++++++++++++++
 .../client/MCPHttpTransportTestSupportTest.java    |  65 ++++++
 .../client/MCPStdioLogbackConfigurationTest.java   |  42 ++++
 27 files changed, 2068 insertions(+), 22 deletions(-)

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 e37289507b3..767a6dfb65f 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
@@ -22,7 +22,10 @@ import 
org.apache.shardingsphere.test.e2e.mcp.llm.config.LLME2EConfiguration.Run
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
 
+import java.io.IOException;
+import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.Properties;
 
@@ -33,6 +36,9 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
 
 class LLME2EConfigurationTest {
     
+    @TempDir
+    private Path tempDir;
+    
     private String originalRuntimeMode;
     
     private String originalModel;
@@ -207,9 +213,35 @@ class LLME2EConfigurationTest {
         assertThat(actual.getBaseServerImage(), 
is("ghcr.io/ggml-org/llama.cpp:server"));
     }
     
+    @Test
+    void assertCreateArtifactDirectory() throws IOException {
+        Path actual = createConfiguration(RuntimeMode.DOCKER, 
tempDir).createArtifactDirectory("scenario-id");
+        assertThat(actual, 
is(tempDir.resolve("run-id").resolve("scenario-id")));
+        assertFalse(Files.notExists(actual));
+    }
+    
+    @Test
+    void assertGetChatCompletionsUrl() {
+        
assertThat(createConfiguration(RuntimeMode.DOCKER).getChatCompletionsUrl(), 
is("http://127.0.0.1:8080/v1/chat/completions";));
+    }
+    
+    @Test
+    void assertGetModelsUrl() {
+        assertThat(createConfiguration(RuntimeMode.DOCKER).getModelsUrl(), 
is("http://127.0.0.1:8080/v1/models";));
+    }
+    
+    @Test
+    void assertGetContainerPath() {
+        
assertThat(createConfiguration(RuntimeMode.DOCKER).getModelMetadata().getContainerPath(),
 is("/models/Qwen3-1.7B-Q4_K_M.gguf"));
+    }
+    
     private LLME2EConfiguration createConfiguration(final RuntimeMode 
runtimeMode) {
+        return createConfiguration(runtimeMode, Path.of("target/llm-e2e"));
+    }
+    
+    private LLME2EConfiguration createConfiguration(final RuntimeMode 
runtimeMode, final Path artifactRoot) {
         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, "llama.cpp", 
"apache/shardingsphere-mcp-llm-runtime:local", 
"ghcr.io/ggml-org/llama.cpp:server", "",
+                artifactRoot, "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",
                         "configured-model-sha256"));
     }
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPActionExecutorTest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPActionExecutorTest.java
new file mode 100644
index 00000000000..b51bb5681b0
--- /dev/null
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPActionExecutorTest.java
@@ -0,0 +1,186 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.test.e2e.mcp.llm.conversation;
+
+import 
org.apache.shardingsphere.test.e2e.mcp.support.transport.MCPInteractionActionNames;
+import 
org.apache.shardingsphere.test.e2e.mcp.support.transport.client.MCPInteractionClient;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.isA;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class LLMMCPActionExecutorTest {
+    
+    @Test
+    void assertExecuteSafelyWithListResources() throws InterruptedException {
+        FakeMCPInteractionClient client = new FakeMCPInteractionClient();
+        assertThat(new 
LLMMCPActionExecutor(client).executeSafely(MCPInteractionActionNames.LIST_RESOURCES,
 Map.of()), is(Map.of("resources", List.of())));
+    }
+    
+    @Test
+    void assertExecuteSafelyWithReadResource() throws InterruptedException {
+        FakeMCPInteractionClient client = new FakeMCPInteractionClient();
+        assertThat(new 
LLMMCPActionExecutor(client).executeSafely(MCPInteractionActionNames.READ_RESOURCE,
 Map.of("uri", " shardingsphere://databases ")), is(Map.of("items", 
List.of())));
+        assertThat(client.resourceUri, is("shardingsphere://databases"));
+    }
+    
+    @Test
+    void assertExecuteSafelyWithEmptyResourceUri() {
+        assertThrows(IllegalArgumentException.class, () -> new 
LLMMCPActionExecutor(new 
FakeMCPInteractionClient()).executeSafely(MCPInteractionActionNames.READ_RESOURCE,
 Map.of("uri", " ")));
+    }
+    
+    @Test
+    void assertExecuteSafelyWithListPrompts() throws InterruptedException {
+        assertThat(new LLMMCPActionExecutor(new 
FakeMCPInteractionClient()).executeSafely(MCPInteractionActionNames.LIST_PROMPTS,
 Map.of()), is(Map.of("prompts", List.of())));
+    }
+    
+    @Test
+    void assertExecuteSafelyWithGetPrompt() throws InterruptedException {
+        FakeMCPInteractionClient client = new FakeMCPInteractionClient();
+        assertThat(new 
LLMMCPActionExecutor(client).executeSafely(MCPInteractionActionNames.GET_PROMPT,
 Map.of("name", " inspect_metadata ", "arguments", Map.of("database", 
"logic_db"))),
+                is(Map.of("messages", List.of())));
+        assertThat(client.promptName, is("inspect_metadata"));
+        assertThat(client.promptArguments, is(Map.of("database", "logic_db")));
+    }
+    
+    @Test
+    void assertExecuteSafelyWithCompletionReference() throws 
InterruptedException {
+        FakeMCPInteractionClient client = new FakeMCPInteractionClient();
+        Map<String, Object> actual = new 
LLMMCPActionExecutor(client).executeSafely(MCPInteractionActionNames.COMPLETE, 
Map.of(
+                "reference", Map.of("type", "prompt", "name", 
"inspect_metadata"),
+                "argument_name", "schema",
+                "argument_value", "pub",
+                "context_arguments", Map.of("database", "logic_db")));
+        assertThat(actual, is(Map.of("completion", "public")));
+        assertThat(client.completionReference, is(Map.of("type", "ref/prompt", 
"name", "inspect_metadata")));
+        assertThat(client.completionArgumentName, is("schema"));
+        assertThat(client.completionArgumentValue, is("pub"));
+        assertThat(client.contextArguments, is(Map.of("database", 
"logic_db")));
+    }
+    
+    @Test
+    void assertExecuteSafelyWithInlineCompletionReference() throws 
InterruptedException {
+        FakeMCPInteractionClient client = new FakeMCPInteractionClient();
+        new 
LLMMCPActionExecutor(client).executeSafely(MCPInteractionActionNames.COMPLETE, 
Map.of("reference_type", "resource", "resource_uri", 
"shardingsphere://databases", "argument_name", "uri"));
+        assertThat(client.completionReference, is(Map.of("type", 
"ref/resource", "uri", "shardingsphere://databases")));
+    }
+    
+    @Test
+    void assertExecuteSafelyWithCompletionRecovery() throws 
InterruptedException {
+        Map<String, Object> actual = new LLMMCPActionExecutor(new 
FakeMCPInteractionClient()).executeSafely(MCPInteractionActionNames.COMPLETE, 
Map.of("argument_value", "pub"));
+        assertThat(actual.get("response_mode"), is("recovery"));
+        assertThat(actual.get("error_code"), is("invalid_tool_arguments"));
+        assertThat(actual.get("message"), is("mcp_complete requires a 
reference object and argument_name."));
+    }
+    
+    @Test
+    void assertExecuteSafelyWithToolCall() throws InterruptedException {
+        FakeMCPInteractionClient client = new FakeMCPInteractionClient();
+        assertThat(new 
LLMMCPActionExecutor(client).executeSafely("database_gateway_execute_query", 
Map.of("sql", "SELECT 1")), is(Map.of("result_kind", "result_set")));
+        assertThat(client.actionName, is("database_gateway_execute_query"));
+        assertThat(client.arguments, is(Map.of("sql", "SELECT 1")));
+    }
+    
+    @Test
+    void assertExecuteSafelyWithIOException() {
+        FakeMCPInteractionClient client = new FakeMCPInteractionClient();
+        client.failWithIOException = true;
+        IllegalStateException actual = 
assertThrows(IllegalStateException.class, () -> new 
LLMMCPActionExecutor(client).executeSafely(MCPInteractionActionNames.LIST_RESOURCES,
 Map.of()));
+        assertThat(actual.getMessage(), is("MCP action `mcp_list_resources` 
failed: unavailable"));
+        assertThat(actual.getCause(), isA(IOException.class));
+    }
+    
+    private static final class FakeMCPInteractionClient implements 
MCPInteractionClient {
+        
+        private boolean failWithIOException;
+        
+        private String actionName;
+        
+        private Map<String, Object> arguments = Map.of();
+        
+        private String resourceUri;
+        
+        private String promptName;
+        
+        private Map<String, Object> promptArguments = Map.of();
+        
+        private Map<String, Object> completionReference = Map.of();
+        
+        private String completionArgumentName;
+        
+        private String completionArgumentValue;
+        
+        private Map<String, String> contextArguments = Map.of();
+        
+        @Override
+        public void open() {
+        }
+        
+        @Override
+        public Map<String, Object> call(final String actionName, final 
Map<String, Object> arguments) {
+            this.actionName = actionName;
+            this.arguments = arguments;
+            return Map.of("result_kind", "result_set");
+        }
+        
+        @Override
+        public Map<String, Object> listResources() throws IOException {
+            if (failWithIOException) {
+                throw new IOException("unavailable");
+            }
+            return Map.of("resources", List.of());
+        }
+        
+        @Override
+        public Map<String, Object> readResource(final String resourceUri) {
+            this.resourceUri = resourceUri;
+            return Map.of("items", List.of());
+        }
+        
+        @Override
+        public Map<String, Object> listPrompts() {
+            return Map.of("prompts", List.of());
+        }
+        
+        @Override
+        public Map<String, Object> getPrompt(final String promptName, final 
Map<String, Object> arguments) {
+            this.promptName = promptName;
+            promptArguments = arguments;
+            return Map.of("messages", List.of());
+        }
+        
+        @Override
+        public Map<String, Object> complete(final Map<String, Object> 
reference, final String argumentName, final String argumentValue, final 
Map<String, String> contextArguments) {
+            completionReference = reference;
+            completionArgumentName = argumentName;
+            completionArgumentValue = argumentValue;
+            this.contextArguments = contextArguments;
+            return Map.of("completion", "public");
+        }
+        
+        @Override
+        public void close() {
+        }
+    }
+}
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPConversationArtifactsTest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPConversationArtifactsTest.java
new file mode 100644
index 00000000000..56f14115e0c
--- /dev/null
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPConversationArtifactsTest.java
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.test.e2e.mcp.llm.conversation;
+
+import 
org.apache.shardingsphere.test.e2e.mcp.llm.conversation.artifact.LLME2EArtifactBundle;
+import 
org.apache.shardingsphere.test.e2e.mcp.llm.conversation.artifact.LLME2EAssertionReport;
+import org.apache.shardingsphere.test.e2e.mcp.llm.scenario.LLME2EScenario;
+import org.apache.shardingsphere.test.e2e.mcp.llm.scenario.LLMStructuredAnswer;
+import 
org.apache.shardingsphere.test.e2e.mcp.support.transport.MCPInteractionTraceRecord;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+class LLMMCPConversationArtifactsTest {
+    
+    @Test
+    void assertAddRawModelOutput() {
+        LLMMCPConversationArtifacts artifacts = new 
LLMMCPConversationArtifacts("provider", "model");
+        artifacts.addRawModelOutput("raw-output");
+        assertThat(artifacts.createArtifactBundle(createScenario(), 
LLME2EAssertionReport.isSuccess("ok")).getRawModelOutputs(), 
is(List.of("raw-output")));
+    }
+    
+    @Test
+    void assertAddInteractionTrace() {
+        LLMMCPConversationArtifacts artifacts = new 
LLMMCPConversationArtifacts("provider", "model");
+        MCPInteractionTraceRecord traceRecord = createTrace();
+        artifacts.addInteractionTrace(traceRecord);
+        assertThat(artifacts.getInteractionTrace(), is(List.of(traceRecord)));
+    }
+    
+    @Test
+    void assertAddRuntimeLogLine() {
+        LLMMCPConversationArtifacts artifacts = new 
LLMMCPConversationArtifacts("provider", "model");
+        artifacts.addRuntimeLogLine("runtime-log");
+        assertThat(artifacts.createArtifactBundle(createScenario(), 
LLME2EAssertionReport.isSuccess("ok")).getMcpRuntimeLogLines(), 
is(List.of("runtime-log")));
+    }
+    
+    @Test
+    void assertSetCapabilityFingerprints() {
+        LLMMCPConversationArtifacts artifacts = new 
LLMMCPConversationArtifacts("provider", "model");
+        artifacts.setCapabilityFingerprints(Map.of("tools", 1));
+        assertThat(artifacts.createArtifactBundle(createScenario(), 
LLME2EAssertionReport.isSuccess("ok")).getCapabilityFingerprints(), 
is(Map.of("tools", 1)));
+    }
+    
+    @Test
+    void assertSetCapabilityFingerprintsWithNull() {
+        LLMMCPConversationArtifacts artifacts = new 
LLMMCPConversationArtifacts("provider", "model");
+        artifacts.setCapabilityFingerprints(null);
+        assertThat(artifacts.createArtifactBundle(createScenario(), 
LLME2EAssertionReport.isSuccess("ok")).getCapabilityFingerprints(), 
is(Map.of()));
+    }
+    
+    @Test
+    void assertNextSequence() {
+        LLMMCPConversationArtifacts artifacts = new 
LLMMCPConversationArtifacts("provider", "model");
+        artifacts.addInteractionTrace(createTrace());
+        assertThat(artifacts.nextSequence(), is(2));
+    }
+    
+    @Test
+    void assertGetFinalAnswerJson() {
+        assertThat(new LLMMCPConversationArtifacts("provider", 
"model").getFinalAnswerJson(), is(""));
+    }
+    
+    @Test
+    void assertSetFinalAnswerJson() {
+        LLMMCPConversationArtifacts artifacts = new 
LLMMCPConversationArtifacts("provider", "model");
+        artifacts.setFinalAnswerJson("{\"ok\":true}");
+        assertThat(artifacts.getFinalAnswerJson(), is("{\"ok\":true}"));
+    }
+    
+    @Test
+    void assertCreateArtifactBundle() {
+        LLMMCPConversationArtifacts artifacts = new 
LLMMCPConversationArtifacts("provider", "model");
+        artifacts.setCapabilityFingerprints(Map.of("tools", 1));
+        artifacts.setFinalAnswerJson("{\"ok\":true}");
+        artifacts.addRawModelOutput("raw-output");
+        MCPInteractionTraceRecord traceRecord = createTrace();
+        artifacts.addInteractionTrace(traceRecord);
+        artifacts.addRuntimeLogLine("runtime-log");
+        LLME2EAssertionReport assertionReport = 
LLME2EAssertionReport.isSuccess("ok");
+        LLME2EArtifactBundle actual = 
artifacts.createArtifactBundle(createScenario(), assertionReport);
+        assertThat(actual.getScenarioId(), is("scenario"));
+        assertThat(actual.getSystemPrompt(), is("system"));
+        assertThat(actual.getUserPrompt(), is("user"));
+        assertThat(actual.getModelProvider(), is("provider"));
+        assertThat(actual.getModelName(), is("model"));
+        assertThat(actual.getCapabilityFingerprints(), is(Map.of("tools", 1)));
+        assertThat(actual.getFinalAnswerJson(), is("{\"ok\":true}"));
+        assertThat(actual.getRawModelOutputs(), is(List.of("raw-output")));
+        assertThat(actual.getInteractionTrace(), is(List.of(traceRecord)));
+        assertThat(actual.getMcpRuntimeLogLines(), is(List.of("runtime-log")));
+        assertThat(actual.getAssertionReport(), is(assertionReport));
+    }
+    
+    private LLME2EScenario createScenario() {
+        return new LLME2EScenario("scenario", "system", "user", new 
LLMStructuredAnswer("logic_db", "public", "orders", "SELECT 1", 1, List.of()), 
List.of(), List.of());
+    }
+    
+    private MCPInteractionTraceRecord createTrace() {
+        return new MCPInteractionTraceRecord(1, "tool_call", 
MCPInteractionTraceRecord.MODEL_TOOL_CALL_ORIGIN, 
"database_gateway_execute_query", Map.of(), Map.of(), true, 0L);
+    }
+}
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPFinalAnswerValidatorTest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPFinalAnswerValidatorTest.java
new file mode 100644
index 00000000000..bed7fdcacca
--- /dev/null
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPFinalAnswerValidatorTest.java
@@ -0,0 +1,138 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.test.e2e.mcp.llm.conversation;
+
+import 
org.apache.shardingsphere.test.e2e.mcp.llm.conversation.artifact.LLME2EAssertionReport;
+import org.apache.shardingsphere.test.e2e.mcp.llm.scenario.LLME2EScenario;
+import org.apache.shardingsphere.test.e2e.mcp.llm.scenario.LLMStructuredAnswer;
+import 
org.apache.shardingsphere.test.e2e.mcp.support.transport.MCPInteractionTraceRecord;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class LLMMCPFinalAnswerValidatorTest {
+    
+    @Test
+    void assertValidateSafely() {
+        LLME2EAssertionReport actual =
+                new 
LLMMCPFinalAnswerValidator().validateSafely(createScenario(List.of("database_gateway_execute_query")),
 createActualAnswer(2, List.of("database_gateway_execute_query")),
+                        List.of(createQueryTrace(1, List.of(List.of(2)))));
+        assertTrue(actual.isSuccess());
+        assertThat(actual.getMessage(), is("LLM MCP interaction passed."));
+    }
+    
+    @Test
+    void assertValidateSafelyWithMissingRequiredCoverage() {
+        LLME2EAssertionReport actual = new 
LLMMCPFinalAnswerValidator().validateSafely(createScenario(List.of("mcp_read_resource")),
 createActualAnswer(2, List.of("database_gateway_execute_query")),
+                List.of(createQueryTrace(1, List.of(List.of(2)))));
+        assertThat(actual.getFailureType(), 
is("missing_required_tool_coverage"));
+    }
+    
+    @ParameterizedTest(name = "{0}")
+    @MethodSource("mismatchedAnswerProvider")
+    void assertValidateSafelyWithMismatchedAnswerFields(final String name, 
final LLMStructuredAnswer actualAnswer, final String expectedMessage) {
+        LLME2EAssertionReport actual = new 
LLMMCPFinalAnswerValidator().validateSafely(createScenario(List.of()), 
actualAnswer, List.of(createQueryTrace(1, List.of(List.of(2)))));
+        assertThat(actual.getFailureType(), is("unexpected_query_result"));
+        assertThat(actual.getMessage(), is(expectedMessage));
+    }
+    
+    @Test
+    void assertValidateSafelyWithSchemaQualifiedQuery() {
+        LLME2EAssertionReport actual = new 
LLMMCPFinalAnswerValidator().validateSafely(createScenario(List.of()), new 
LLMStructuredAnswer(
+                "logic_db", "public", "orders", "SELECT COUNT(*) FROM 
public.orders", 2, List.of("database_gateway_execute_query")), 
List.of(createQueryTrace(1, List.of(List.of(2)))));
+        assertTrue(actual.isSuccess());
+    }
+    
+    @Test
+    void assertValidateSafelyWithStringTotalOrders() {
+        LLME2EAssertionReport actual = new 
LLMMCPFinalAnswerValidator().validateSafely(createScenario(List.of()), 
createActualAnswer(2, List.of("database_gateway_execute_query")),
+                List.of(createQueryTrace(1, List.of(List.of("2")))));
+        assertTrue(actual.isSuccess());
+    }
+    
+    @Test
+    void assertValidateSafelyWithNonResultSet() {
+        LLME2EAssertionReport actual = new 
LLMMCPFinalAnswerValidator().validateSafely(createScenario(List.of()), 
createActualAnswer(2, List.of("database_gateway_execute_query")), List.of(
+                new MCPInteractionTraceRecord(1, "tool_call", 
MCPInteractionTraceRecord.MODEL_TOOL_CALL_ORIGIN, 
"database_gateway_execute_query", Map.of(), Map.of("result_kind", 
"update_count"), true,
+                        0L)));
+        assertThat(actual.getFailureType(), is("unexpected_query_result"));
+        assertThat(actual.getMessage(), is("The database_gateway_execute_query 
trace does not contain a numeric result set."));
+    }
+    
+    @Test
+    void assertValidateSafelyWithNonNumericTotalOrders() {
+        LLME2EAssertionReport actual = new 
LLMMCPFinalAnswerValidator().validateSafely(createScenario(List.of()), 
createActualAnswer(2, List.of("database_gateway_execute_query")),
+                List.of(createQueryTrace(1, List.of(List.of("bad")))));
+        assertThat(actual.getFailureType(), is("unexpected_query_result"));
+        assertThat(actual.getMessage(), is("The database_gateway_execute_query 
trace does not contain a numeric result set."));
+    }
+    
+    @Test
+    void assertValidateSafelyWithMismatchedInteractionSequence() {
+        LLME2EAssertionReport actual = new 
LLMMCPFinalAnswerValidator().validateSafely(createScenario(List.of()), 
createActualAnswer(2, List.of("mcp_read_resource")),
+                List.of(createQueryTrace(1, List.of(List.of(2)))));
+        assertThat(actual.getFailureType(), is("unexpected_query_result"));
+        assertThat(actual.getMessage(), is("Final answer interactionSequence 
does not match the observed interaction trace."));
+    }
+    
+    @Test
+    void assertValidateSafelyWithConsecutiveInteractionCollapse() {
+        LLME2EAssertionReport actual = new 
LLMMCPFinalAnswerValidator().validateSafely(createScenario(List.of()), 
createActualAnswer(2, List.of("database_gateway_execute_query")),
+                List.of(createQueryTrace(1, List.of(List.of(2))), 
createQueryTrace(2, List.of(List.of(2)))));
+        assertTrue(actual.isSuccess());
+    }
+    
+    private static Stream<Object[]> mismatchedAnswerProvider() {
+        return Stream.of(
+                new Object[]{"database", new LLMStructuredAnswer("other_db", 
"public", "orders", "SELECT COUNT(*) FROM orders", 2, 
List.of("database_gateway_execute_query")),
+                        "Final answer database does not match expected 
database."},
+                new Object[]{"schema", new LLMStructuredAnswer("logic_db", 
"other_schema", "orders", "SELECT COUNT(*) FROM orders", 2, 
List.of("database_gateway_execute_query")),
+                        "Final answer schema does not match expected schema."},
+                new Object[]{"table", new LLMStructuredAnswer("logic_db", 
"public", "users", "SELECT COUNT(*) FROM orders", 2, 
List.of("database_gateway_execute_query")),
+                        "Final answer table does not match expected table."},
+                new Object[]{"query", new LLMStructuredAnswer("logic_db", 
"public", "orders", "SELECT COUNT(*) FROM users", 2, 
List.of("database_gateway_execute_query")),
+                        "Final answer query does not match expected query."},
+                new Object[]{"totalOrders", new 
LLMStructuredAnswer("logic_db", "public", "orders", "SELECT COUNT(*) FROM 
orders", 3, List.of("database_gateway_execute_query")),
+                        "Final answer totalOrders does not match the 
database_gateway_execute_query result."});
+    }
+    
+    private LLME2EScenario createScenario(final List<String> 
requiredToolNames) {
+        return new LLME2EScenario("scenario", "", "", createExpectedAnswer(), 
List.of(), requiredToolNames);
+    }
+    
+    private LLMStructuredAnswer createExpectedAnswer() {
+        return new LLMStructuredAnswer("logic_db", "public", "orders", "SELECT 
COUNT(*) FROM orders", 2, List.of("database_gateway_execute_query"));
+    }
+    
+    private LLMStructuredAnswer createActualAnswer(final int totalOrders, 
final List<String> interactionSequence) {
+        return new LLMStructuredAnswer("logic_db", "public", "orders", "SELECT 
COUNT(*) FROM orders", totalOrders, interactionSequence);
+    }
+    
+    private MCPInteractionTraceRecord createQueryTrace(final int sequence, 
final List<List<Object>> rows) {
+        return new MCPInteractionTraceRecord(sequence, "tool_call", 
MCPInteractionTraceRecord.MODEL_TOOL_CALL_ORIGIN, 
"database_gateway_execute_query", Map.of(),
+                Map.of("result_kind", "result_set", "rows", rows), true, 0L);
+    }
+}
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPInteractionCoverageTest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPInteractionCoverageTest.java
new file mode 100644
index 00000000000..5282ef9514e
--- /dev/null
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPInteractionCoverageTest.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.test.e2e.mcp.llm.conversation;
+
+import 
org.apache.shardingsphere.test.e2e.mcp.support.transport.MCPInteractionTraceRecord;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Map;
+
+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.assertTrue;
+
+class LLMMCPInteractionCoverageTest {
+    
+    @Test
+    void assertHasRequiredInteractionCoverage() {
+        
assertTrue(LLMMCPInteractionCoverage.hasRequiredInteractionCoverage(List.of("database_gateway_execute_query"),
 List.of(
+                createTrace("database_gateway_execute_query", true, 
Map.of("result_kind", "result_set")))));
+    }
+    
+    @Test
+    void assertFindMissingRequiredInteractionNames() {
+        
assertThat(LLMMCPInteractionCoverage.findMissingRequiredInteractionNames(List.of("database_gateway_execute_query",
 "mcp_read_resource"), List.of(
+                createTrace("database_gateway_execute_query", true, Map.of()),
+                createTrace("mcp_read_resource", false, Map.of()),
+                createTrace("mcp_list_resources", true, Map.of("error_code", 
"failed")))), is(List.of("mcp_read_resource")));
+    }
+    
+    @Test
+    void assertHasRequiredInteractionCoverageWithErrorPayload() {
+        
assertFalse(LLMMCPInteractionCoverage.hasRequiredInteractionCoverage(List.of("database_gateway_execute_query"),
 List.of(
+                createTrace("database_gateway_execute_query", true, 
Map.of("error_code", "failed")))));
+    }
+    
+    private MCPInteractionTraceRecord createTrace(final String targetName, 
final boolean valid, final Map<String, Object> structuredContent) {
+        return new MCPInteractionTraceRecord(1, "tool_call", 
MCPInteractionTraceRecord.MODEL_TOOL_CALL_ORIGIN, targetName, Map.of(), 
structuredContent, valid, 0L);
+    }
+}
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPJsonValuesTest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPJsonValuesTest.java
new file mode 100644
index 00000000000..8adb3d46ce4
--- /dev/null
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPJsonValuesTest.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.test.e2e.mcp.llm.conversation;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class LLMMCPJsonValuesTest {
+    
+    @Test
+    void assertParseToolArguments() {
+        assertThat(LLMMCPJsonValues.parseToolArguments("{\"sql\":\"SELECT 
1\",\"limit\":1}"), is(Map.of("sql", "SELECT 1", "limit", 1)));
+    }
+    
+    @Test
+    void assertParseToolArgumentsWithInvalidJson() {
+        assertThrows(IllegalArgumentException.class, () -> 
LLMMCPJsonValues.parseToolArguments("{invalid"));
+    }
+    
+    @Test
+    void assertCastToRows() {
+        assertThat(LLMMCPJsonValues.castToRows(List.of(List.of("orders", 2))), 
is(List.of(List.of("orders", 2))));
+    }
+    
+    @Test
+    void assertCastToMap() {
+        assertThat(LLMMCPJsonValues.castToMap(Map.of("database", "logic_db")), 
is(Map.of("database", "logic_db")));
+    }
+    
+    @Test
+    void assertCastToList() {
+        assertThat(LLMMCPJsonValues.<Map<String, 
Object>>castToList(List.of(Map.of("name", "orders"))), 
is(List.of(Map.of("name", "orders"))));
+    }
+    
+    @Test
+    void assertCastToListWithNull() {
+        assertThat(LLMMCPJsonValues.castToList(null), is(List.of()));
+    }
+    
+    @Test
+    void assertCastToStringMap() {
+        assertThat(LLMMCPJsonValues.castToStringMap(Map.of("schema", 
"public")), is(Map.of("schema", "public")));
+    }
+}
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPModelFacingToolResponseFormatterTest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPModelFacingToolResponseFormatterTest.java
index 6581919ea48..9dcfa63c71b 100644
--- 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPModelFacingToolResponseFormatterTest.java
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPModelFacingToolResponseFormatterTest.java
@@ -31,14 +31,13 @@ class LLMMCPModelFacingToolResponseFormatterTest {
     
     @Test
     void assertFormat() {
-        Map<String, Object> actual = 
JsonUtils.fromJsonString(LLMMCPModelFacingToolResponseFormatter.format(Map.of("resources",
 List.of(Map.of(
+        Map<String, Object> actual = format(Map.of("resources", List.of(Map.of(
                 "uri", "shardingsphere://databases",
                 "name", "logical-databases",
                 "title", "Logical Databases",
                 "description", "Long model-facing description.",
                 "mimeType", "application/json",
-                "_meta", Map.of("org.apache.shardingsphere/resource-kind", 
"list"))))), new TypeReference<>() {
-                });
+                "_meta", Map.of("org.apache.shardingsphere/resource-kind", 
"list")))));
         Map<String, Object> expected = Map.of("resources", List.of(Map.of(
                 "uri", "shardingsphere://databases",
                 "name", "logical-databases",
@@ -46,4 +45,75 @@ class LLMMCPModelFacingToolResponseFormatterTest {
                 "mimeType", "application/json")));
         assertThat(actual, is(expected));
     }
+    
+    @Test
+    void assertFormatWithCompactItems() {
+        Map<String, Object> actual = format(Map.of("items", List.of(
+                Map.of("database", "logic_db", "schema", "public", "ignored", 
"value"),
+                Map.of("table", "orders"),
+                Map.of("view", "order_view"),
+                Map.of("column", "status"),
+                Map.of("name", "order_id"),
+                Map.of("resource", "extra"))));
+        assertThat(actual, is(Map.of("items", List.of(
+                Map.of("database", "logic_db", "schema", "public"),
+                Map.of("table", "orders"),
+                Map.of("view", "order_view"),
+                Map.of("column", "status"),
+                Map.of("name", "order_id")))));
+    }
+    
+    @Test
+    void assertFormatWithPrompts() {
+        Map<String, Object> actual = format(Map.of("prompts", 
List.of(Map.of("name", "inspect_metadata"), Map.of("description", "ignored"))));
+        assertThat(actual, is(Map.of("prompts", List.of("inspect_metadata"))));
+    }
+    
+    @Test
+    void assertFormatWithPromptMessages() {
+        Map<String, Object> actual = format(Map.of("description", "Inspect 
metadata.", "messages", List.of(Map.of("role", "user"), Map.of("role", 
"assistant"))));
+        assertThat(actual, is(Map.of("description", "Inspect metadata.", 
"message_count", 2)));
+    }
+    
+    @Test
+    void assertFormatWithArtifactSummaries() {
+        Map<String, Object> actual = format(Map.of(
+                "manual_artifacts", List.of(Map.of(
+                        "ddl_artifacts", List.of("a", "b"),
+                        "index_plan", List.of("c"),
+                        "distsql_artifacts", List.of("d", "e", "f"))),
+                "exported_artifacts", List.of(Map.of("ddl_artifacts", 
List.of("g")))));
+        assertThat(actual, is(Map.of(
+                "manual_artifacts", List.of(Map.of("ddl_artifact_count", 2, 
"index_plan_count", 1, "distsql_artifact_count", 3)),
+                "exported_artifacts", List.of(Map.of("ddl_artifact_count", 
1)))));
+    }
+    
+    @Test
+    void assertFormatWithRecoveryAndNextActions() {
+        Map<String, Object> actual = format(Map.of(
+                "next_actions", List.of(
+                        Map.of("type", "tool_call", "tool_name", 
"database_gateway_execute_update", "title", "Execute", "reason", "approved",
+                                "arguments", Map.of("execution_mode", 
"execute")),
+                        Map.of("type", "resource_read", "resource_uri", 
"shardingsphere://databases")),
+                "recovery", Map.of(
+                        "recovery_category", "missing_context",
+                        "model_action", "retry",
+                        "suggested_arguments", Map.of("database", "logic_db"),
+                        "ignored", "value")));
+        assertThat(actual, is(Map.of(
+                "next_actions", List.of(
+                        Map.of("type", "tool_call", "title", "Execute", 
"reason", "approved"),
+                        Map.of("type", "resource_read", "resource_uri", 
"shardingsphere://databases")),
+                "recovery", Map.of("recovery_category", "missing_context", 
"model_action", "retry", "suggested_arguments", Map.of("database", 
"logic_db")))));
+    }
+    
+    @Test
+    void assertFormatWithOriginalPayloadFallback() {
+        assertThat(format(Map.of("unknown", "value")), is(Map.of("unknown", 
"value")));
+    }
+    
+    private Map<String, Object> format(final Map<String, Object> value) {
+        return 
JsonUtils.fromJsonString(LLMMCPModelFacingToolResponseFormatter.format(value), 
new TypeReference<>() {
+        });
+    }
 }
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPModelFacingToolResponseFormatterTest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPNextActionsTest.java
similarity index 50%
copy from 
test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPModelFacingToolResponseFormatterTest.java
copy to 
test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPNextActionsTest.java
index 6581919ea48..98080c412e8 100644
--- 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPModelFacingToolResponseFormatterTest.java
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPNextActionsTest.java
@@ -17,8 +17,6 @@
 
 package org.apache.shardingsphere.test.e2e.mcp.llm.conversation;
 
-import com.fasterxml.jackson.core.type.TypeReference;
-import org.apache.shardingsphere.infra.util.json.JsonUtils;
 import org.junit.jupiter.api.Test;
 
 import java.util.List;
@@ -27,23 +25,19 @@ import java.util.Map;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.is;
 
-class LLMMCPModelFacingToolResponseFormatterTest {
+class LLMMCPNextActionsTest {
     
     @Test
-    void assertFormat() {
-        Map<String, Object> actual = 
JsonUtils.fromJsonString(LLMMCPModelFacingToolResponseFormatter.format(Map.of("resources",
 List.of(Map.of(
-                "uri", "shardingsphere://databases",
-                "name", "logical-databases",
-                "title", "Logical Databases",
-                "description", "Long model-facing description.",
-                "mimeType", "application/json",
-                "_meta", Map.of("org.apache.shardingsphere/resource-kind", 
"list"))))), new TypeReference<>() {
-                });
-        Map<String, Object> expected = Map.of("resources", List.of(Map.of(
-                "uri", "shardingsphere://databases",
-                "name", "logical-databases",
-                "title", "Logical Databases",
-                "mimeType", "application/json")));
-        assertThat(actual, is(expected));
+    void assertGetNextActions() {
+        Map<String, Object> topLevelAction = Map.of("type", "resource_read", 
"resource_uri", "shardingsphere://databases");
+        Map<String, Object> recoveryAction = Map.of("type", "tool_call", 
"tool_name", "database_gateway_search_metadata");
+        assertThat(LLMMCPNextActions.getNextActions(Map.of(
+                "next_actions", List.of(topLevelAction, "ignored"),
+                "recovery", Map.of("next_actions", List.of(recoveryAction)))), 
is(List.of(topLevelAction, recoveryAction)));
+    }
+    
+    @Test
+    void assertGetNextActionsWithNoActions() {
+        assertThat(LLMMCPNextActions.getNextActions(Map.of("next_actions", 
"ignored", "recovery", Map.of("next_actions", "ignored"))), is(List.of()));
     }
 }
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPScenarioInferenceTest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPScenarioInferenceTest.java
new file mode 100644
index 00000000000..b4372df6833
--- /dev/null
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPScenarioInferenceTest.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.test.e2e.mcp.llm.conversation;
+
+import org.apache.shardingsphere.test.e2e.mcp.llm.scenario.LLME2EScenario;
+import org.apache.shardingsphere.test.e2e.mcp.llm.scenario.LLMStructuredAnswer;
+import 
org.apache.shardingsphere.test.e2e.mcp.support.transport.MCPInteractionTraceRecord;
+import org.junit.jupiter.api.Test;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+class LLMMCPScenarioInferenceTest {
+    
+    @Test
+    void assertFindExpectedResourceUriWithPromptMatch() {
+        LLME2EScenario scenario = createScenario("Read 
`shardingsphere://databases/logic_db/schemas/public/tables/orders`.");
+        assertThat(LLMMCPScenarioInference.findExpectedResourceUri(scenario), 
is("shardingsphere://databases/logic_db/schemas/public/tables/orders"));
+    }
+    
+    @Test
+    void assertFindExpectedResourceUriWithPromptFallback() {
+        LLME2EScenario scenario = createScenario("Read 
`shardingsphere://runtime`.");
+        assertThat(LLMMCPScenarioInference.findExpectedResourceUri(scenario), 
is("shardingsphere://runtime"));
+    }
+    
+    @Test
+    void assertFindExpectedResourceUriWithExpectedTable() {
+        
assertThat(LLMMCPScenarioInference.findExpectedResourceUri(createScenario("Inspect
 the table.")),
+                
is("shardingsphere://databases/logic_db/schemas/public/tables/orders"));
+    }
+    
+    @Test
+    void assertFindLatestPlanId() {
+        assertThat(LLMMCPScenarioInference.findLatestPlanId(List.of(
+                createTrace(1, "plan-a", true, Map.of()),
+                createTrace(2, "plan-b", true, Map.of("error_code", "failed")),
+                createTrace(3, "plan-c", true, Map.of()))), is("plan-c"));
+    }
+    
+    @Test
+    void assertFindLatestPlanIdWithNoValidPlan() {
+        
assertThat(LLMMCPScenarioInference.findLatestPlanId(List.of(createTrace(1, 
"plan-a", false, Map.of()))), is(""));
+    }
+    
+    @Test
+    void assertNormalizeComparableQuery() {
+        
assertThat(LLMMCPScenarioInference.normalizeComparableQuery(createAnswer(), " 
select count(*) from public.orders "),
+                is("SELECT COUNT(*) FROM ORDERS"));
+    }
+    
+    private LLME2EScenario createScenario(final String userPrompt) {
+        return new LLME2EScenario("scenario", "", userPrompt, createAnswer(), 
List.of(), List.of());
+    }
+    
+    private LLMStructuredAnswer createAnswer() {
+        return new LLMStructuredAnswer("logic_db", "public", "orders", "SELECT 
COUNT(*) FROM orders", 2, List.of());
+    }
+    
+    private MCPInteractionTraceRecord createTrace(final int sequence, final 
String planId, final boolean valid, final Map<String, Object> extraContent) {
+        Map<String, Object> structuredContent = new 
LinkedHashMap<>(extraContent);
+        structuredContent.put("plan_id", planId);
+        return new MCPInteractionTraceRecord(sequence, "tool_call", 
MCPInteractionTraceRecord.MODEL_TOOL_CALL_ORIGIN, 
"database_gateway_plan_mask_rule", Map.of(), structuredContent, valid, 0L);
+    }
+}
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPSideEffectNextActionTest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPSideEffectNextActionTest.java
new file mode 100644
index 00000000000..892801ea7b8
--- /dev/null
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPSideEffectNextActionTest.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.test.e2e.mcp.llm.conversation;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.Map;
+import java.util.stream.Stream;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+class LLMMCPSideEffectNextActionTest {
+    
+    @ParameterizedTest(name = "{0}")
+    @MethodSource("executionActionProvider")
+    void assertIsExecutionAction(final String name, final Map<String, Object> 
action, final boolean expected) {
+        assertThat(LLMMCPSideEffectNextAction.isExecutionAction(action), 
is(expected));
+    }
+    
+    private static Stream<Object[]> executionActionProvider() {
+        return Stream.of(
+                new Object[]{"execute update", Map.of("type", "tool_call", 
"tool_name", "database_gateway_execute_update", "arguments", 
Map.of("execution_mode", "execute")), true},
+                new Object[]{"review workflow", Map.of("type", "tool_call", 
"tool_name", "database_gateway_apply_workflow", "arguments", 
Map.of("execution_mode", "review-then-execute")), true},
+                new Object[]{"read only tool call", Map.of("type", 
"tool_call", "tool_name", "database_gateway_execute_query", "arguments", 
Map.of()), false},
+                new Object[]{"resource action", Map.of("type", 
"resource_read", "resource_uri", "shardingsphere://databases"), false});
+    }
+}
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPToolCallNormalizerTest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPToolCallNormalizerTest.java
new file mode 100644
index 00000000000..b54f27f3473
--- /dev/null
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPToolCallNormalizerTest.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.test.e2e.mcp.llm.conversation;
+
+import org.apache.shardingsphere.test.e2e.mcp.llm.scenario.LLME2EScenario;
+import org.apache.shardingsphere.test.e2e.mcp.llm.scenario.LLMStructuredAnswer;
+import 
org.apache.shardingsphere.test.e2e.mcp.support.transport.MCPInteractionActionNames;
+import 
org.apache.shardingsphere.test.e2e.mcp.support.transport.MCPInteractionTraceRecord;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+class LLMMCPToolCallNormalizerTest {
+    
+    @Test
+    void assertNormalizeWithReadOnlySqlRoutedToOfferedQueryTool() {
+        LLMMCPToolCallNormalizer.NormalizedToolCall actual = 
LLMMCPToolCallNormalizer.normalize(createScenario(""), 
"database_gateway_execute_update",
+                Map.of("sql", "SELECT COUNT(*) FROM orders", "execution_mode", 
"preview"), List.of("database_gateway_execute_query"), List.of());
+        assertThat(actual.name(), is("database_gateway_execute_query"));
+        assertFalse(actual.arguments().containsKey("execution_mode"));
+    }
+    
+    @Test
+    void assertNormalizeWithResourceUriArgument() {
+        LLMMCPToolCallNormalizer.NormalizedToolCall actual = 
LLMMCPToolCallNormalizer.normalize(createScenario("Read the orders table."), 
MCPInteractionActionNames.READ_RESOURCE,
+                Map.of("uri", "orders"), 
List.of(MCPInteractionActionNames.READ_RESOURCE), List.of());
+        assertThat(actual.arguments().get("uri"), 
is("shardingsphere://databases/logic_db/schemas/public/tables/orders"));
+    }
+    
+    @Test
+    void assertNormalizeWithSearchMetadataScopeArgument() {
+        LLMMCPToolCallNormalizer.NormalizedToolCall actual = 
LLMMCPToolCallNormalizer.normalize(
+                createScenario("Use logical database `logic_db` and schema 
`public` when the MCP action needs explicit runtime scope."),
+                "database_gateway_search_metadata", Map.of("query", "orders"), 
List.of("database_gateway_search_metadata"), List.of());
+        assertThat(actual.arguments().get("database"), is("logic_db"));
+        assertThat(actual.arguments().get("schema"), is("public"));
+    }
+    
+    @Test
+    void assertNormalizeWithExpectedQuerySchemaArgument() {
+        LLMMCPToolCallNormalizer.NormalizedToolCall actual = 
LLMMCPToolCallNormalizer.normalize(createScenario(""), 
"database_gateway_execute_query",
+                Map.of("database", "logic_db", "sql", "SELECT COUNT(*) FROM 
public.orders"), List.of("database_gateway_execute_query"), List.of());
+        assertThat(actual.arguments().get("schema"), is("public"));
+    }
+    
+    @Test
+    void assertNormalizeWithInitialPlanningPlanIdArgument() {
+        LLMMCPToolCallNormalizer.NormalizedToolCall actual = 
LLMMCPToolCallNormalizer.normalize(createScenario(""), 
"database_gateway_plan_mask_rule",
+                Map.of("plan_id", "plan_id", "table", "orders"), 
List.of("database_gateway_plan_mask_rule"), List.of());
+        assertFalse(actual.arguments().containsKey("plan_id"));
+    }
+    
+    @Test
+    void assertNormalizeWithWorkflowPlanIdArgument() {
+        LLMMCPToolCallNormalizer.NormalizedToolCall actual = 
LLMMCPToolCallNormalizer.normalize(createScenario(""), 
"database_gateway_apply_workflow",
+                Map.of("plan_id", "<plan_id>"), 
List.of("database_gateway_apply_workflow"), List.of(createPlanTrace()));
+        assertThat(actual.arguments().get("plan_id"), is("plan-1"));
+    }
+    
+    @Test
+    void assertNormalizeWithCompletionArguments() {
+        LLMMCPToolCallNormalizer.NormalizedToolCall actual = 
LLMMCPToolCallNormalizer.normalize(createScenario(""), 
MCPInteractionActionNames.COMPLETE,
+                Map.of("argument_name", "schema"), 
List.of(MCPInteractionActionNames.COMPLETE), List.of(createPromptTrace()));
+        assertThat(actual.arguments().get("reference"), is(Map.of("type", 
"ref/prompt", "name", "inspect_metadata")));
+    }
+    
+    private LLME2EScenario createScenario(final String userPrompt) {
+        return new LLME2EScenario("scenario", "", userPrompt, new 
LLMStructuredAnswer("logic_db", "public", "orders", "SELECT COUNT(*) FROM 
orders", 2, List.of()),
+                List.of(), List.of());
+    }
+    
+    private MCPInteractionTraceRecord createPlanTrace() {
+        return new MCPInteractionTraceRecord(1, "tool_call", 
MCPInteractionTraceRecord.MODEL_TOOL_CALL_ORIGIN, 
"database_gateway_plan_mask_rule", Map.of(), Map.of("plan_id", "plan-1"), true, 
0L);
+    }
+    
+    private MCPInteractionTraceRecord createPromptTrace() {
+        return new MCPInteractionTraceRecord(1, 
MCPInteractionActionNames.PROMPT_GET_KIND, 
MCPInteractionTraceRecord.PROTOCOL_BRIDGE_ORIGIN, 
MCPInteractionActionNames.GET_PROMPT,
+                Map.of("name", "inspect_metadata"), Map.of(), true, 0L);
+    }
+}
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPToolDefinitionFactoryTest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPToolDefinitionFactoryTest.java
index 54294f5bbd2..acfe7d4c57e 100644
--- 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPToolDefinitionFactoryTest.java
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPToolDefinitionFactoryTest.java
@@ -28,6 +28,7 @@ import java.util.Map;
 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 LLMMCPToolDefinitionFactoryTest {
     
@@ -59,6 +60,12 @@ class LLMMCPToolDefinitionFactoryTest {
         assertCompleteBridgeSchema(getParameters(findTool(actual, 
MCPInteractionActionNames.COMPLETE)));
     }
     
+    @Test
+    void assertCreateWithUnsupportedToolDescriptor() {
+        IllegalArgumentException actual = 
assertThrows(IllegalArgumentException.class, () -> new 
LLMMCPToolDefinitionFactory().create(List.of("unsupported_tool")));
+        assertThat(actual.getMessage(), is("Unsupported tool descriptor: 
unsupported_tool"));
+    }
+    
     private void assertOfficialToolDefinition(final Map<?, ?> toolDefinition, 
final MCPToolDescriptor toolDescriptor) {
         assertThat(toolDefinition.get("type"), is("function"));
         Map<?, ?> function = getFunction(toolDefinition);
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/scenario/LLMStructuredAnswerTest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/scenario/LLMStructuredAnswerTest.java
new file mode 100644
index 00000000000..fe0b6d61a26
--- /dev/null
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/scenario/LLMStructuredAnswerTest.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.test.e2e.mcp.llm.scenario;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class LLMStructuredAnswerTest {
+    
+    @Test
+    void assertFromJson() {
+        LLMStructuredAnswer actual = LLMStructuredAnswer.fromJson("""
+                {
+                  "database": " logic_db ",
+                  "schema": " public ",
+                  "table": " orders ",
+                  "query": " SELECT COUNT(*) FROM orders ",
+                  "totalOrders": 2,
+                  "interactionSequence": [
+                    {"tool": "mcp_read_resource"},
+                    {"targetName": "database_gateway_execute_query"},
+                    {"name": "mcp_complete"},
+                    "database_gateway_search_metadata"
+                  ]
+                }
+                """);
+        assertThat(actual.getDatabase(), is("logic_db"));
+        assertThat(actual.getSchema(), is("public"));
+        assertThat(actual.getTable(), is("orders"));
+        assertThat(actual.getQuery(), is("SELECT COUNT(*) FROM orders"));
+        assertThat(actual.getTotalOrders(), is(2));
+        assertThat(actual.getInteractionSequence(), 
is(List.of("mcp_read_resource", "database_gateway_execute_query", 
"mcp_complete", "database_gateway_search_metadata")));
+    }
+    
+    @Test
+    void assertFromJsonWithLegacyToolSequence() {
+        LLMStructuredAnswer actual = LLMStructuredAnswer.fromJson("""
+                
{"database":"logic_db","schema":"public","table":"orders","query":"SELECT 
1","totalOrders":"2","toolSequence":["database_gateway_execute_query"]}
+                """);
+        assertThat(actual.getTotalOrders(), is(2));
+        assertThat(actual.getInteractionSequence(), 
is(List.of("database_gateway_execute_query")));
+    }
+    
+    @Test
+    void assertFromJsonWithInvalidTotalOrders() {
+        assertThrows(IllegalArgumentException.class, () -> 
LLMStructuredAnswer.fromJson("""
+                
{"database":"logic_db","schema":"public","table":"orders","query":"SELECT 
1","totalOrders":"bad"}
+                """));
+    }
+    
+    @Test
+    void assertFromJsonWithInvalidJson() {
+        assertThrows(IllegalArgumentException.class, () -> 
LLMStructuredAnswer.fromJson("{invalid"));
+    }
+}
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/suite/usability/LLMUsabilitySuiteRunnerTest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/suite/usability/LLMUsabilitySuiteRunnerTest.java
new file mode 100644
index 00000000000..6d1dfd0dea7
--- /dev/null
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/suite/usability/LLMUsabilitySuiteRunnerTest.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.test.e2e.mcp.llm.suite.usability;
+
+import org.apache.shardingsphere.test.e2e.mcp.llm.config.LLME2EConfiguration;
+import 
org.apache.shardingsphere.test.e2e.mcp.llm.config.LLME2EConfiguration.RuntimeMode;
+import 
org.apache.shardingsphere.test.e2e.mcp.llm.conversation.LLMConversationExecutor;
+import 
org.apache.shardingsphere.test.e2e.mcp.llm.conversation.artifact.LLME2EArtifactBundle;
+import 
org.apache.shardingsphere.test.e2e.mcp.llm.conversation.artifact.LLME2EAssertionReport;
+import org.apache.shardingsphere.test.e2e.mcp.llm.scenario.LLME2EScenario;
+import org.apache.shardingsphere.test.e2e.mcp.llm.scenario.LLMStructuredAnswer;
+import 
org.apache.shardingsphere.test.e2e.mcp.llm.suite.usability.assessment.LLMUsabilityDimension;
+import 
org.apache.shardingsphere.test.e2e.mcp.llm.suite.usability.scenario.LLMUsabilityScenario;
+import 
org.apache.shardingsphere.test.e2e.mcp.support.transport.MCPInteractionTraceRecord;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class LLMUsabilitySuiteRunnerTest {
+    
+    @TempDir
+    private Path tempDir;
+    
+    @Test
+    void assertAssertCoreSuite() throws IOException {
+        LLME2EConfiguration configuration = createConfiguration();
+        new LLMUsabilitySuiteRunner().assertCoreSuite("core-suite", () -> 
List.of(createScenario(LLMUsabilityScenario.NATURAL_TASK_TAG)),
+                scenario -> createConversationResult(configuration, scenario), 
configuration);
+        
assertTrue(Files.isRegularFile(tempDir.resolve("run-id").resolve("core-suite").resolve("scorecard.json")));
+    }
+    
+    @Test
+    void assertAssertExtendedSuite() throws IOException {
+        LLME2EConfiguration configuration = createConfiguration();
+        new LLMUsabilitySuiteRunner().assertExtendedSuite("extended-suite", () 
-> List.of(createScenario(LLMUsabilityScenario.PROTOCOL_CONTRACT_TAG)),
+                scenario -> createConversationResult(configuration, scenario), 
configuration);
+        
assertTrue(Files.isRegularFile(tempDir.resolve("run-id").resolve("extended-suite").resolve("scorecard.json")));
+    }
+    
+    private LLMConversationExecutor.ConversationResult 
createConversationResult(final LLME2EConfiguration configuration, final 
LLME2EScenario scenario) throws IOException {
+        Path artifactDirectory = 
configuration.getArtifactRoot().resolve(configuration.getRunId()).resolve(scenario.getScenarioId());
+        createArtifactFiles(artifactDirectory);
+        List<MCPInteractionTraceRecord> trace = List.of(new 
MCPInteractionTraceRecord(1, "tool_call", 
MCPInteractionTraceRecord.MODEL_TOOL_CALL_ORIGIN,
+                "database_gateway_execute_query", Map.of(), 
Map.of("result_kind", "result_set"), true, 0L));
+        LLME2EArtifactBundle artifactBundle = new 
LLME2EArtifactBundle(scenario.getScenarioId(), scenario.getSystemPrompt(), 
scenario.getUserPrompt(), "provider", "model",
+                Map.of(), "{}", List.of("raw-output"), trace, 
List.of("runtime-log"), LLME2EAssertionReport.isSuccess("ok"));
+        return new LLMConversationExecutor.ConversationResult(artifactBundle, 
artifactDirectory);
+    }
+    
+    private void createArtifactFiles(final Path artifactDirectory) throws 
IOException {
+        Files.createDirectories(artifactDirectory);
+        for (String each : List.of("run-context.json", "system-prompt.md", 
"user-prompt.md", "raw-model-output.txt", "interaction-trace.json", 
"assertion-report.json", "mcp-runtime.log")) {
+            Files.writeString(artifactDirectory.resolve(each), "ok");
+        }
+    }
+    
+    private LLMUsabilityScenario createScenario(final String tag) {
+        LLME2EScenario llmScenario = new LLME2EScenario("scenario-" + tag, 
"system", "Count orders.",
+                new LLMStructuredAnswer("logic_db", "public", "orders", 
"SELECT COUNT(*) FROM orders", 2, List.of()),
+                List.of("database_gateway_execute_query"), 
List.of("database_gateway_execute_query"));
+        return new LLMUsabilityScenario("scenario-" + tag, 
LLMUsabilityDimension.TOOL, "runtime", List.of(tag), llmScenario,
+                List.of("database_gateway_execute_query"), List.of(), false, 
false, "");
+    }
+    
+    private LLME2EConfiguration createConfiguration() {
+        return new LLME2EConfiguration("http://127.0.0.1:8080/v1";, "provider", 
"model", "api-key", 1, 1, 1, tempDir, "run-id", RuntimeMode.EXTERNAL_DEBUG,
+                "runtime", "server-image", "base-image", "", new 
LLME2EConfiguration.ModelMetadata("repository", "model.gguf", "Q4", "revision", 
"sha256"));
+    }
+}
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/distribution/PackagedDistributionPluginFixtureSupportTest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/distribution/PackagedDistributionPluginFixtureSupportTest.java
index 3f858ea6315..13003fef7b6 100644
--- 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/distribution/PackagedDistributionPluginFixtureSupportTest.java
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/distribution/PackagedDistributionPluginFixtureSupportTest.java
@@ -28,11 +28,13 @@ import java.net.URL;
 import java.net.URLClassLoader;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.List;
 import java.util.jar.JarEntry;
 import java.util.jar.JarFile;
 import java.util.stream.Stream;
 
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsInAnyOrder;
 import static org.hamcrest.Matchers.hasItem;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -70,4 +72,16 @@ class PackagedDistributionPluginFixtureSupportTest {
             }
         }
     }
+    
+    @Test
+    void assertRemoveOfficialFeatureJars() throws IOException {
+        
Files.writeString(tempDir.resolve("shardingsphere-mcp-feature-encrypt-test.jar"),
 "");
+        
Files.writeString(tempDir.resolve("shardingsphere-mcp-feature-mask-test.jar"), 
"");
+        
Files.writeString(tempDir.resolve("shardingsphere-mcp-feature-other-test.jar"), 
"");
+        List<String> actual = 
PackagedDistributionPluginFixtureSupport.removeOfficialFeatureJars(tempDir);
+        assertThat(actual, 
containsInAnyOrder("shardingsphere-mcp-feature-encrypt-test.jar", 
"shardingsphere-mcp-feature-mask-test.jar"));
+        
assertTrue(Files.notExists(tempDir.resolve("shardingsphere-mcp-feature-encrypt-test.jar")));
+        
assertTrue(Files.notExists(tempDir.resolve("shardingsphere-mcp-feature-mask-test.jar")));
+        
assertTrue(Files.isRegularFile(tempDir.resolve("shardingsphere-mcp-feature-other-test.jar")));
+    }
 }
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/fixture/MCPWorkflowCustomEncryptAlgorithmFixtureTest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/fixture/MCPWorkflowCustomEncryptAlgorithmFixtureTest.java
new file mode 100644
index 00000000000..f392cf3d6c0
--- /dev/null
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/fixture/MCPWorkflowCustomEncryptAlgorithmFixtureTest.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.test.e2e.mcp.support.fixture;
+
+import org.apache.shardingsphere.encrypt.spi.EncryptAlgorithmMetaData;
+import 
org.apache.shardingsphere.infra.algorithm.core.config.AlgorithmConfiguration;
+import org.junit.jupiter.api.Test;
+
+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.assertTrue;
+
+class MCPWorkflowCustomEncryptAlgorithmFixtureTest {
+    
+    @Test
+    void assertEncrypt() {
+        assertThat(new 
MCPWorkflowCustomEncryptAlgorithmFixture().encrypt("plain", null), 
is("mcp_custom:plain"));
+    }
+    
+    @Test
+    void assertDecrypt() {
+        assertThat(new 
MCPWorkflowCustomEncryptAlgorithmFixture().decrypt("mcp_custom:plain", null), 
is("plain"));
+    }
+    
+    @Test
+    void assertDecryptWithRawValue() {
+        assertThat(new 
MCPWorkflowCustomEncryptAlgorithmFixture().decrypt("plain", null), is("plain"));
+    }
+    
+    @Test
+    void assertGetMetaData() {
+        EncryptAlgorithmMetaData actual = new 
MCPWorkflowCustomEncryptAlgorithmFixture().getMetaData();
+        assertTrue(actual.isSupportDecrypt());
+        assertFalse(actual.isSupportEquivalentFilter());
+        assertFalse(actual.isSupportLike());
+    }
+    
+    @Test
+    void assertToConfiguration() {
+        AlgorithmConfiguration actual = new 
MCPWorkflowCustomEncryptAlgorithmFixture().toConfiguration();
+        assertThat(actual.getType(), is("MCP_CUSTOM_REVERSIBLE"));
+        assertTrue(actual.getProps().isEmpty());
+    }
+    
+    @Test
+    void assertGetType() {
+        assertThat(new MCPWorkflowCustomEncryptAlgorithmFixture().getType(), 
is("MCP_CUSTOM_REVERSIBLE"));
+    }
+}
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/fixture/MCPWorkflowCustomMaskAlgorithmFixtureTest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/fixture/MCPWorkflowCustomMaskAlgorithmFixtureTest.java
new file mode 100644
index 00000000000..3dcc2341802
--- /dev/null
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/fixture/MCPWorkflowCustomMaskAlgorithmFixtureTest.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.test.e2e.mcp.support.fixture;
+
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+class MCPWorkflowCustomMaskAlgorithmFixtureTest {
+    
+    @Test
+    void assertMask() {
+        assertThat(new MCPWorkflowCustomMaskAlgorithmFixture().mask("plain"), 
is("mask:plain"));
+    }
+    
+    @Test
+    void assertGetType() {
+        assertThat(new MCPWorkflowCustomMaskAlgorithmFixture().getType(), 
is("MCP_MASK_CUSTOM"));
+    }
+}
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/fixture/plugin/PluginFixtureHandlerProviderTest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/fixture/plugin/PluginFixtureHandlerProviderTest.java
new file mode 100644
index 00000000000..762c2079d21
--- /dev/null
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/fixture/plugin/PluginFixtureHandlerProviderTest.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.test.e2e.mcp.support.fixture.plugin;
+
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+class PluginFixtureHandlerProviderTest {
+    
+    @Test
+    void assertGetToolHandlers() {
+        assertThat(new 
PluginFixtureHandlerProvider().getToolHandlers().iterator().next().getClass(), 
is(PluginFixturePingToolHandler.class));
+    }
+    
+    @Test
+    void assertGetResourceHandlers() {
+        assertThat(new 
PluginFixtureHandlerProvider().getResourceHandlers().iterator().next().getClass(),
 is(PluginFixtureStatusResourceHandler.class));
+    }
+}
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/fixture/plugin/PluginFixturePingToolHandlerTest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/fixture/plugin/PluginFixturePingToolHandlerTest.java
new file mode 100644
index 00000000000..68ef9172c99
--- /dev/null
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/fixture/plugin/PluginFixturePingToolHandlerTest.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.test.e2e.mcp.support.fixture.plugin;
+
+import org.apache.shardingsphere.mcp.api.tool.MCPToolCall;
+import org.apache.shardingsphere.mcp.core.context.MCPServiceHandlerContext;
+import org.junit.jupiter.api.Test;
+
+import java.util.Map;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+class PluginFixturePingToolHandlerTest {
+    
+    @Test
+    void assertGetContextType() {
+        assertThat(new PluginFixturePingToolHandler().getContextType(), 
is(MCPServiceHandlerContext.class));
+    }
+    
+    @Test
+    void assertGetToolName() {
+        assertThat(new PluginFixturePingToolHandler().getToolName(), 
is("fixture_ping"));
+    }
+    
+    @Test
+    void assertHandle() {
+        assertThat(new PluginFixturePingToolHandler().handle(null, new 
MCPToolCall("session", Map.of("message", "hello"))).toPayload(),
+                is(Map.of("status", "ready", "echo", "hello")));
+    }
+}
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/fixture/plugin/PluginFixtureStatusResourceHandlerTest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/fixture/plugin/PluginFixtureStatusResourceHandlerTest.java
new file mode 100644
index 00000000000..7c049e73e03
--- /dev/null
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/fixture/plugin/PluginFixtureStatusResourceHandlerTest.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.test.e2e.mcp.support.fixture.plugin;
+
+import org.apache.shardingsphere.mcp.api.resource.MCPUriVariables;
+import org.apache.shardingsphere.mcp.core.context.MCPServiceHandlerContext;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+class PluginFixtureStatusResourceHandlerTest {
+    
+    @Test
+    void assertGetContextType() {
+        assertThat(new PluginFixtureStatusResourceHandler().getContextType(), 
is(MCPServiceHandlerContext.class));
+    }
+    
+    @Test
+    void assertGetResourceUriTemplate() {
+        assertThat(new 
PluginFixtureStatusResourceHandler().getResourceUriTemplate(), 
is("shardingsphere://features/test-fixture/status"));
+    }
+    
+    @Test
+    void assertHandle() {
+        assertThat(new PluginFixtureStatusResourceHandler().handle(null, new 
MCPUriVariables(Map.of())).toPayload(),
+                is(Map.of("items", List.of(Map.of("feature", "test-fixture", 
"status", "ready")))));
+    }
+}
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/transport/MCPInteractionProtocolSupportTest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/transport/MCPInteractionProtocolSupportTest.java
new file mode 100644
index 00000000000..2556df58f5e
--- /dev/null
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/transport/MCPInteractionProtocolSupportTest.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.test.e2e.mcp.support.transport;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Map;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+class MCPInteractionProtocolSupportTest {
+    
+    @Test
+    void assertCreateInitializeRequestParams() {
+        
assertThat(MCPInteractionProtocolSupport.createInitializeRequestParams("client"),
 is(Map.of(
+                "protocolVersion", 
MCPInteractionProtocolSupport.PROTOCOL_VERSION,
+                "capabilities", Map.of(),
+                "clientInfo", Map.of("name", "client", "version", "1.0.0"))));
+    }
+    
+    @Test
+    void assertCreateJsonRpcRequest() {
+        assertThat(MCPInteractionProtocolSupport.createJsonRpcRequest("id", 
"tools/list", Map.of("cursor", "next")), is(Map.of(
+                "jsonrpc", "2.0",
+                "id", "id",
+                "method", "tools/list",
+                "params", Map.of("cursor", "next"))));
+    }
+    
+    @Test
+    void assertCreateJsonRpcNotification() {
+        
assertThat(MCPInteractionProtocolSupport.createJsonRpcNotification("notifications/initialized",
 Map.of()), is(Map.of(
+                "jsonrpc", "2.0",
+                "method", "notifications/initialized",
+                "params", Map.of())));
+    }
+    
+    @Test
+    void assertCreateJsonRpcRequestBody() {
+        Map<String, Object> actual = 
MCPInteractionPayloads.parseJsonPayload(MCPInteractionProtocolSupport.createJsonRpcRequestBody("id",
 "tools/list", Map.of()));
+        assertThat(actual, is(Map.of("jsonrpc", "2.0", "id", "id", "method", 
"tools/list", "params", Map.of())));
+    }
+    
+    @Test
+    void assertCreateJsonRpcNotificationBody() {
+        Map<String, Object> actual = 
MCPInteractionPayloads.parseJsonPayload(MCPInteractionProtocolSupport.createJsonRpcNotificationBody("notifications/initialized",
 Map.of()));
+        assertThat(actual, is(Map.of("jsonrpc", "2.0", "method", 
"notifications/initialized", "params", Map.of())));
+    }
+}
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/transport/MCPInteractionTraceRecordTest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/transport/MCPInteractionTraceRecordTest.java
new file mode 100644
index 00000000000..17e9c29a29c
--- /dev/null
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/transport/MCPInteractionTraceRecordTest.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.test.e2e.mcp.support.transport;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Map;
+
+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.assertTrue;
+
+class MCPInteractionTraceRecordTest {
+    
+    @Test
+    void assertCreateResourceRead() {
+        MCPInteractionTraceRecord actual = 
MCPInteractionTraceRecord.createResourceRead(1, "shardingsphere://databases", 
Map.of("items", 1), 12L);
+        assertThat(actual.getSequence(), is(1));
+        assertThat(actual.getActionKind(), 
is(MCPInteractionActionNames.RESOURCE_READ_KIND));
+        assertThat(actual.getActionOrigin(), 
is(MCPInteractionTraceRecord.PROTOCOL_BRIDGE_ORIGIN));
+        assertThat(actual.getTargetName(), 
is(MCPInteractionActionNames.READ_RESOURCE));
+        assertThat(actual.getArguments(), is(Map.of("uri", 
"shardingsphere://databases")));
+        assertThat(actual.getStructuredContent(), is(Map.of("items", 1)));
+        assertTrue(actual.isValid());
+        assertThat(actual.getLatencyMillis(), is(12L));
+    }
+    
+    @Test
+    void assertCreateCompletion() {
+        MCPInteractionTraceRecord actual = 
MCPInteractionTraceRecord.createCompletion(2, Map.of("argument_name", 
"schema"), Map.of("completion", "public"), 7L);
+        assertThat(actual.getSequence(), is(2));
+        assertThat(actual.getActionKind(), 
is(MCPInteractionActionNames.COMPLETION_KIND));
+        assertThat(actual.getActionOrigin(), 
is(MCPInteractionTraceRecord.PROTOCOL_BRIDGE_ORIGIN));
+        assertThat(actual.getTargetName(), 
is(MCPInteractionActionNames.COMPLETE));
+        assertThat(actual.getArguments(), is(Map.of("argument_name", 
"schema")));
+        assertThat(actual.getStructuredContent(), is(Map.of("completion", 
"public")));
+        assertTrue(actual.isValid());
+        assertThat(actual.getLatencyMillis(), is(7L));
+    }
+    
+    @Test
+    void assertCreateInvalidAction() {
+        MCPInteractionTraceRecord actual = 
MCPInteractionTraceRecord.createInvalidAction(3, "tool_call", 
"database_gateway_execute_update", Map.of("sql", "UPDATE t SET c = 1"), 
"unsafe_sql");
+        assertThat(actual.getSequence(), is(3));
+        assertThat(actual.getActionKind(), is("tool_call"));
+        assertThat(actual.getActionOrigin(), 
is(MCPInteractionTraceRecord.MODEL_TOOL_CALL_ORIGIN));
+        assertThat(actual.getTargetName(), 
is("database_gateway_execute_update"));
+        assertThat(actual.getArguments(), is(Map.of("sql", "UPDATE t SET c = 
1")));
+        assertThat(actual.getStructuredContent(), is(Map.of("error_code", 
"unsafe_sql")));
+        assertFalse(actual.isValid());
+        assertThat(actual.getLatencyMillis(), is(0L));
+    }
+}
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/transport/MCPPayloadAssertionsTest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/transport/MCPPayloadAssertionsTest.java
new file mode 100644
index 00000000000..8dddb818bc6
--- /dev/null
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/transport/MCPPayloadAssertionsTest.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.test.e2e.mcp.support.transport;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+class MCPPayloadAssertionsTest {
+    
+    @Test
+    void assertAssertSingleItemValue() {
+        MCPPayloadAssertions.assertSingleItemValue(Map.of("items", 
List.of(Map.of("name", "orders"))), "name", "orders");
+    }
+    
+    @Test
+    void assertAssertItemValues() {
+        MCPPayloadAssertions.assertItemValues(createPayload(), "name", 
List.of("orders", "users"));
+    }
+    
+    @Test
+    void assertGetSingleItem() {
+        assertThat(MCPPayloadAssertions.getSingleItem(Map.of("items", 
List.of(Map.of("name", "orders")))), is(Map.of("name", "orders")));
+    }
+    
+    @Test
+    void assertFindItem() {
+        assertThat(MCPPayloadAssertions.findItem(createPayload(), "name", 
"users"), is(Map.of("name", "users", "type", "table")));
+    }
+    
+    @Test
+    void assertGetItemValues() {
+        assertThat(MCPPayloadAssertions.getItemValues(createPayload(), 
"name"), is(List.of("orders", "users")));
+    }
+    
+    @Test
+    void assertGetItems() {
+        assertThat(MCPPayloadAssertions.getItems(createPayload()), 
is(List.of(Map.of("name", "orders", "type", "table"), Map.of("name", "users", 
"type", "table"))));
+    }
+    
+    @Test
+    void assertGetMap() {
+        assertThat(MCPPayloadAssertions.getMap(Map.of("name", "orders")), 
is(Map.of("name", "orders")));
+    }
+    
+    @Test
+    void assertGetMapList() {
+        assertThat(MCPPayloadAssertions.getMapList(List.of(Map.of("name", 
"orders"))), is(List.of(Map.of("name", "orders"))));
+    }
+    
+    @Test
+    void assertAssertToolDefinition() {
+        MCPPayloadAssertions.assertToolDefinition(List.of(Map.of(
+                "name", "database_gateway_execute_query",
+                "title", "Execute Query",
+                "inputSchema", Map.of(
+                        "type", "object",
+                        "required", List.of("sql"),
+                        "properties", Map.of("sql", Map.of("type", 
"string"))))),
+                "database_gateway_execute_query", "Execute Query", "sql", 
"sql", "string");
+    }
+    
+    private Map<String, Object> createPayload() {
+        return Map.of("items", List.of(Map.of("name", "orders", "type", 
"table"), Map.of("name", "users", "type", "table")));
+    }
+}
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/transport/client/AbstractMCPInteractionClientTest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/transport/client/AbstractMCPInteractionClientTest.java
new file mode 100644
index 00000000000..3265fbddf7d
--- /dev/null
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/transport/client/AbstractMCPInteractionClientTest.java
@@ -0,0 +1,159 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.test.e2e.mcp.support.transport.client;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+class AbstractMCPInteractionClientTest {
+    
+    @Test
+    void assertCall() throws IOException, InterruptedException {
+        FakeMCPInteractionClient client = new 
FakeMCPInteractionClient(Map.of("result", Map.of("structuredContent", 
Map.of("status", "ok"))));
+        assertThat(client.call("fixture_ping", Map.of("message", "hello")), 
is(Map.of("status", "ok")));
+        assertThat(client.requestId, is("fixture_ping-1"));
+        assertThat(client.method, is("tools/call"));
+        assertThat(client.params, is(Map.of("name", "fixture_ping", 
"arguments", Map.of("message", "hello"))));
+        assertThat(client.openCount, is(1));
+    }
+    
+    @Test
+    void assertListTools() throws IOException, InterruptedException {
+        FakeMCPInteractionClient client = new 
FakeMCPInteractionClient(Map.of("result", Map.of("tools", 
List.of(Map.of("name", "fixture_ping")))));
+        assertThat(client.listTools(), is(List.of(Map.of("name", 
"fixture_ping"))));
+        assertThat(client.requestId, is("tools-list-1"));
+        assertThat(client.method, is("tools/list"));
+        assertThat(client.params, is(Map.of()));
+    }
+    
+    @Test
+    void assertListResources() throws IOException, InterruptedException {
+        FakeMCPInteractionClient client = new 
FakeMCPInteractionClient(Map.of("result", Map.of("resources", 
List.of(Map.of("uri", "shardingsphere://databases")))));
+        assertThat(client.listResources(), is(Map.of("resources", 
List.of(Map.of("uri", "shardingsphere://databases")))));
+        assertThat(client.requestId, is("resources-list-1"));
+        assertThat(client.method, is("resources/list"));
+    }
+    
+    @Test
+    void assertListResourceTemplates() throws IOException, 
InterruptedException {
+        FakeMCPInteractionClient client = new 
FakeMCPInteractionClient(Map.of("result", Map.of("resourceTemplates", 
List.of(Map.of("uriTemplate", "shardingsphere://databases/{database}")))));
+        assertThat(client.listResourceTemplates(), 
is(Map.of("resourceTemplates", List.of(Map.of("uriTemplate", 
"shardingsphere://databases/{database}")))));
+        assertThat(client.requestId, is("resources-templates-list-1"));
+        assertThat(client.method, is("resources/templates/list"));
+    }
+    
+    @Test
+    void assertReadResource() throws IOException, InterruptedException {
+        FakeMCPInteractionClient client = new 
FakeMCPInteractionClient(Map.of("result", Map.of("contents", 
List.of(Map.of("text", "{\"items\":[{\"name\":\"orders\"}]}")))));
+        assertThat(client.readResource("shardingsphere://databases"), 
is(Map.of("items", List.of(Map.of("name", "orders")))));
+        assertThat(client.requestId, is("resources-read-1"));
+        assertThat(client.method, is("resources/read"));
+        assertThat(client.params, is(Map.of("uri", 
"shardingsphere://databases")));
+    }
+    
+    @Test
+    void assertSendRawRequest() throws IOException, InterruptedException {
+        FakeMCPInteractionClient client = new 
FakeMCPInteractionClient(Map.of("result", Map.of("ok", true)));
+        assertThat(client.sendRawRequest("id", "custom/method", Map.of("name", 
"value")), is(Map.of("result", Map.of("ok", true))));
+        assertThat(client.requestId, is("id"));
+        assertThat(client.method, is("custom/method"));
+        assertThat(client.params, is(Map.of("name", "value")));
+    }
+    
+    @Test
+    void assertListPrompts() throws IOException, InterruptedException {
+        FakeMCPInteractionClient client = new 
FakeMCPInteractionClient(Map.of("result", Map.of("prompts", 
List.of(Map.of("name", "inspect_metadata")))));
+        assertThat(client.listPrompts(), is(Map.of("prompts", 
List.of(Map.of("name", "inspect_metadata")))));
+        assertThat(client.requestId, is("prompts-list-1"));
+        assertThat(client.method, is("prompts/list"));
+    }
+    
+    @Test
+    void assertGetPrompt() throws IOException, InterruptedException {
+        FakeMCPInteractionClient client = new 
FakeMCPInteractionClient(Map.of("result", Map.of("messages", 
List.of(Map.of("role", "user")))));
+        assertThat(client.getPrompt("inspect_metadata", Map.of("database", 
"logic_db")), is(Map.of("messages", List.of(Map.of("role", "user")))));
+        assertThat(client.requestId, is("prompts-get-1"));
+        assertThat(client.method, is("prompts/get"));
+        assertThat(client.params, is(Map.of("name", "inspect_metadata", 
"arguments", Map.of("database", "logic_db"))));
+    }
+    
+    @Test
+    void assertComplete() throws IOException, InterruptedException {
+        FakeMCPInteractionClient client = new 
FakeMCPInteractionClient(Map.of("result", Map.of("completion", "public")));
+        assertThat(client.complete(Map.of("type", "ref/prompt", "name", 
"inspect_metadata"), "schema", "pub", Map.of("database", "logic_db")), 
is(Map.of("completion", "public")));
+        assertThat(client.requestId, is("completion-complete-1"));
+        assertThat(client.method, is("completion/complete"));
+        assertThat(client.params, is(Map.of(
+                "ref", Map.of("type", "ref/prompt", "name", 
"inspect_metadata"),
+                "argument", Map.of("name", "schema", "value", "pub"),
+                "context", Map.of("arguments", Map.of("database", 
"logic_db")))));
+    }
+    
+    @Test
+    void assertCompleteWithoutContextArguments() throws IOException, 
InterruptedException {
+        FakeMCPInteractionClient client = new 
FakeMCPInteractionClient(Map.of("result", Map.of("completion", "public")));
+        client.complete(Map.of("type", "ref/prompt", "name", 
"inspect_metadata"), "schema", "pub", Map.of());
+        assertThat(client.params, is(Map.of(
+                "ref", Map.of("type", "ref/prompt", "name", 
"inspect_metadata"),
+                "argument", Map.of("name", "schema", "value", "pub"))));
+    }
+    
+    private static final class FakeMCPInteractionClient extends 
AbstractMCPInteractionClient {
+        
+        private final Map<String, Object> response;
+        
+        private int openCount;
+        
+        private String requestId;
+        
+        private String method;
+        
+        private Map<String, Object> params = Map.of();
+        
+        private FakeMCPInteractionClient(final Map<String, Object> response) {
+            this.response = response;
+        }
+        
+        @Override
+        public void open() {
+        }
+        
+        @Override
+        public void close() {
+        }
+        
+        @Override
+        protected void ensureOpened() {
+            openCount++;
+        }
+        
+        @Override
+        protected Map<String, Object> sendRequest(final String requestId, 
final String method, final Map<String, Object> params) {
+            this.requestId = requestId;
+            this.method = method;
+            this.params = params;
+            return response;
+        }
+    }
+}
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/transport/client/MCPHttpInteractionClientTest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/transport/client/MCPHttpInteractionClientTest.java
new file mode 100644
index 00000000000..619c2917bcb
--- /dev/null
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/transport/client/MCPHttpInteractionClientTest.java
@@ -0,0 +1,254 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.test.e2e.mcp.support.transport.client;
+
+import org.junit.jupiter.api.Test;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.SSLSession;
+import java.io.IOException;
+import java.net.Authenticator;
+import java.net.CookieHandler;
+import java.net.ProxySelector;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpHeaders;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class MCPHttpInteractionClientTest {
+    
+    private static final URI ENDPOINT_URI = 
URI.create("http://127.0.0.1:8080/mcp";);
+    
+    @Test
+    void assertOpen() throws IOException, InterruptedException {
+        FakeHttpClient httpClient = new FakeHttpClient();
+        httpClient.addResponse(200, Map.of("MCP-Session-Id", 
List.of("session"), "MCP-Protocol-Version", List.of("protocol")), 
"{\"result\":{\"serverInfo\":{\"name\":\"test\"}}}");
+        httpClient.addResponse(202, Map.of(), "");
+        MCPHttpInteractionClient client = new 
MCPHttpInteractionClient(ENDPOINT_URI, httpClient);
+        client.open();
+        assertThat(client.getInitializePayload(), is(Map.of("result", 
Map.of("serverInfo", Map.of("name", "test")))));
+        assertThat(httpClient.requests.size(), is(2));
+        assertThat(httpClient.requests.get(0).method(), is("POST"));
+        
assertThat(httpClient.requests.get(1).headers().firstValue("MCP-Session-Id").orElse(""),
 is("session"));
+        
assertThat(httpClient.requests.get(1).headers().firstValue("MCP-Protocol-Version").orElse(""),
 is("protocol"));
+    }
+    
+    @Test
+    void assertOpenWithErrorStatus() {
+        FakeHttpClient httpClient = new FakeHttpClient();
+        httpClient.addResponse(500, Map.of(), "{}");
+        IllegalStateException actual = 
assertThrows(IllegalStateException.class, () -> new 
MCPHttpInteractionClient(ENDPOINT_URI, httpClient).open());
+        assertThat(actual.getMessage(), is("Failed to initialize MCP 
session."));
+    }
+    
+    @Test
+    void assertOpenWithJsonRpcError() {
+        FakeHttpClient httpClient = new FakeHttpClient();
+        httpClient.addResponse(200, Map.of("MCP-Session-Id", 
List.of("session")), "{\"error\":{\"message\":\"denied\"}}");
+        IllegalStateException actual = 
assertThrows(IllegalStateException.class, () -> new 
MCPHttpInteractionClient(ENDPOINT_URI, httpClient).open());
+        assertThat(actual.getMessage(), is("Failed to initialize MCP session: 
denied"));
+    }
+    
+    @Test
+    void assertOpenWithMissingSessionHeader() {
+        FakeHttpClient httpClient = new FakeHttpClient();
+        httpClient.addResponse(200, Map.of(), "{\"result\":{}}");
+        IllegalStateException actual = 
assertThrows(IllegalStateException.class, () -> new 
MCPHttpInteractionClient(ENDPOINT_URI, httpClient).open());
+        assertThat(actual.getMessage(), is("MCP initialize response does not 
contain MCP-Session-Id header."));
+    }
+    
+    @Test
+    void assertGetInitializePayloadBeforeOpen() {
+        assertThat(new MCPHttpInteractionClient(ENDPOINT_URI, new 
FakeHttpClient()).getInitializePayload(), is(Map.of()));
+    }
+    
+    @Test
+    void assertClose() throws IOException, InterruptedException {
+        FakeHttpClient httpClient = new FakeHttpClient();
+        httpClient.addResponse(200, Map.of("MCP-Session-Id", 
List.of("session")), "{\"result\":{}}");
+        httpClient.addResponse(202, Map.of(), "");
+        httpClient.addResponse(200, Map.of(), "");
+        MCPHttpInteractionClient client = new 
MCPHttpInteractionClient(ENDPOINT_URI, httpClient);
+        client.open();
+        client.close();
+        assertThat(httpClient.requests.get(2).method(), is("DELETE"));
+        
assertThat(httpClient.requests.get(2).headers().firstValue("MCP-Session-Id").orElse(""),
 is("session"));
+        assertThat(client.getInitializePayload(), is(Map.of()));
+    }
+    
+    @Test
+    void assertCloseBeforeOpen() throws IOException, InterruptedException {
+        FakeHttpClient httpClient = new FakeHttpClient();
+        new MCPHttpInteractionClient(ENDPOINT_URI, httpClient).close();
+        assertThat(httpClient.requests, is(List.of()));
+    }
+    
+    private static final class FakeHttpClient extends HttpClient {
+        
+        private final Deque<QueuedResponse> responses = new ArrayDeque<>();
+        
+        private final List<HttpRequest> requests = new LinkedList<>();
+        
+        private void addResponse(final int statusCode, final Map<String, 
List<String>> headers, final String body) {
+            responses.addLast(new QueuedResponse(statusCode, headers, body));
+        }
+        
+        @Override
+        public Optional<CookieHandler> cookieHandler() {
+            return Optional.empty();
+        }
+        
+        @Override
+        public Optional<Duration> connectTimeout() {
+            return Optional.empty();
+        }
+        
+        @Override
+        public HttpClient.Redirect followRedirects() {
+            return HttpClient.Redirect.NEVER;
+        }
+        
+        @Override
+        public Optional<ProxySelector> proxy() {
+            return Optional.empty();
+        }
+        
+        @Override
+        public SSLContext sslContext() {
+            return null;
+        }
+        
+        @Override
+        public SSLParameters sslParameters() {
+            return null;
+        }
+        
+        @Override
+        public Optional<Authenticator> authenticator() {
+            return Optional.empty();
+        }
+        
+        @Override
+        public HttpClient.Version version() {
+            return HttpClient.Version.HTTP_1_1;
+        }
+        
+        @Override
+        public Optional<Executor> executor() {
+            return Optional.empty();
+        }
+        
+        @Override
+        @SuppressWarnings("unchecked")
+        public <T> HttpResponse<T> send(final HttpRequest request, final 
HttpResponse.BodyHandler<T> responseBodyHandler) throws IOException {
+            requests.add(request);
+            if (responses.isEmpty()) {
+                throw new IOException("No queued HTTP response.");
+            }
+            QueuedResponse response = responses.removeFirst();
+            return (HttpResponse<T>) new StringHttpResponse(request, 
response.statusCode(), response.headers(), response.body());
+        }
+        
+        @Override
+        public <T> CompletableFuture<HttpResponse<T>> sendAsync(final 
HttpRequest request, final HttpResponse.BodyHandler<T> responseBodyHandler) {
+            throw new UnsupportedOperationException();
+        }
+        
+        @Override
+        public <T> CompletableFuture<HttpResponse<T>> sendAsync(final 
HttpRequest request, final HttpResponse.BodyHandler<T> responseBodyHandler,
+                                                                final 
HttpResponse.PushPromiseHandler<T> pushPromiseHandler) {
+            throw new UnsupportedOperationException();
+        }
+    }
+    
+    private record QueuedResponse(int statusCode, Map<String, List<String>> 
headers, String body) {
+    }
+    
+    private static final class StringHttpResponse implements 
HttpResponse<String> {
+        
+        private final HttpRequest request;
+        
+        private final int statusCode;
+        
+        private final Map<String, List<String>> rawHeaders;
+        
+        private final String body;
+        
+        private StringHttpResponse(final HttpRequest request, final int 
statusCode, final Map<String, List<String>> rawHeaders, final String body) {
+            this.request = request;
+            this.statusCode = statusCode;
+            this.rawHeaders = rawHeaders;
+            this.body = body;
+        }
+        
+        @Override
+        public int statusCode() {
+            return statusCode;
+        }
+        
+        @Override
+        public HttpRequest request() {
+            return request;
+        }
+        
+        @Override
+        public Optional<HttpResponse<String>> previousResponse() {
+            return Optional.empty();
+        }
+        
+        @Override
+        public HttpHeaders headers() {
+            return HttpHeaders.of(rawHeaders, (key, value) -> true);
+        }
+        
+        @Override
+        public String body() {
+            return body;
+        }
+        
+        @Override
+        public Optional<SSLSession> sslSession() {
+            return Optional.empty();
+        }
+        
+        @Override
+        public URI uri() {
+            return request.uri();
+        }
+        
+        @Override
+        public HttpClient.Version version() {
+            return HttpClient.Version.HTTP_1_1;
+        }
+    }
+}
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/transport/client/MCPHttpTransportTestSupportTest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/transport/client/MCPHttpTransportTestSupportTest.java
new file mode 100644
index 00000000000..20bcf5f39d9
--- /dev/null
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/transport/client/MCPHttpTransportTestSupportTest.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.test.e2e.mcp.support.transport.client;
+
+import 
org.apache.shardingsphere.test.e2e.mcp.support.transport.MCPInteractionPayloads;
+import 
org.apache.shardingsphere.test.e2e.mcp.support.transport.MCPInteractionProtocolSupport;
+import org.junit.jupiter.api.Test;
+
+import java.net.URI;
+import java.net.http.HttpRequest;
+import java.util.Map;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+class MCPHttpTransportTestSupportTest {
+    
+    @Test
+    void assertCreateJsonRequestBuilder() {
+        HttpRequest actual = 
MCPHttpTransportTestSupport.createJsonRequestBuilder(URI.create("http://127.0.0.1:8080/mcp";)).build();
+        assertThat(actual.uri(), is(URI.create("http://127.0.0.1:8080/mcp";)));
+        assertThat(actual.headers().firstValue("Content-Type").orElse(""), 
is("application/json"));
+        assertThat(actual.headers().firstValue("Accept").orElse(""), 
is("application/json, text/event-stream"));
+    }
+    
+    @Test
+    void assertCreateSessionRequestBuilder() {
+        HttpRequest actual = 
MCPHttpTransportTestSupport.createSessionRequestBuilder(URI.create("http://127.0.0.1:8080/mcp";),
 "session", "protocol").build();
+        assertThat(actual.headers().firstValue("MCP-Session-Id").orElse(""), 
is("session"));
+        
assertThat(actual.headers().firstValue("MCP-Protocol-Version").orElse(""), 
is("protocol"));
+    }
+    
+    @Test
+    void assertCreateInitializeRequestParams() {
+        
assertThat(MCPHttpTransportTestSupport.createInitializeRequestParams("client"),
+                
is(MCPInteractionProtocolSupport.createInitializeRequestParams("client")));
+    }
+    
+    @Test
+    void assertCreateJsonRpcRequestBody() {
+        
assertThat(MCPInteractionPayloads.parseJsonPayload(MCPHttpTransportTestSupport.createJsonRpcRequestBody("id",
 "tools/list", Map.of())),
+                is(Map.of("jsonrpc", "2.0", "id", "id", "method", 
"tools/list", "params", Map.of())));
+    }
+    
+    @Test
+    void assertCreateJsonRpcNotificationBody() {
+        
assertThat(MCPInteractionPayloads.parseJsonPayload(MCPHttpTransportTestSupport.createJsonRpcNotificationBody("notifications/initialized",
 Map.of())),
+                is(Map.of("jsonrpc", "2.0", "method", 
"notifications/initialized", "params", Map.of())));
+    }
+}
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/transport/client/MCPStdioLogbackConfigurationTest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/transport/client/MCPStdioLogbackConfigurationTest.java
new file mode 100644
index 00000000000..b27acb72810
--- /dev/null
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/transport/client/MCPStdioLogbackConfigurationTest.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.test.e2e.mcp.support.transport.client;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+
+class MCPStdioLogbackConfigurationTest {
+    
+    @TempDir
+    private Path tempDir;
+    
+    @Test
+    void assertCreateForConfig() throws IOException {
+        Path actual = 
MCPStdioLogbackConfiguration.createForConfig(tempDir.resolve("server.yaml"), 
"logback-test.xml");
+        assertThat(actual, is(tempDir.resolve("logback-test.xml")));
+        assertThat(Files.readString(actual), 
containsString("<target>System.err</target>"));
+    }
+}

Reply via email to