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>"));
+ }
+}