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 801447ba3ce Add MCP protocol contract coverage (#39042)
801447ba3ce is described below

commit 801447ba3ce887f51bf751724263ff0ed469dfa9
Author: Liang Zhang <[email protected]>
AuthorDate: Wed Jul 8 01:51:26 2026 +0800

    Add MCP protocol contract coverage (#39042)
---
 .../mcp/registry/MCPRegistryMetadataCommand.java   |  4 +-
 .../registry/MCPRegistryMetadataCommandTest.java   | 12 ++-
 .../HttpTransportProtocolContractE2ETest.java      | 88 ++++++++++++++++++++++
 3 files changed, 101 insertions(+), 3 deletions(-)

diff --git 
a/mcp/registry/src/main/java/org/apache/shardingsphere/mcp/registry/MCPRegistryMetadataCommand.java
 
b/mcp/registry/src/main/java/org/apache/shardingsphere/mcp/registry/MCPRegistryMetadataCommand.java
index 8c100abc9f1..0820581a036 100644
--- 
a/mcp/registry/src/main/java/org/apache/shardingsphere/mcp/registry/MCPRegistryMetadataCommand.java
+++ 
b/mcp/registry/src/main/java/org/apache/shardingsphere/mcp/registry/MCPRegistryMetadataCommand.java
@@ -42,7 +42,7 @@ public final class MCPRegistryMetadataCommand {
     
     private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
     
-    private static final String SCHEMA_URL = 
"https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json";;
+    private static final String REGISTRY_SCHEMA_URL = 
"https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json";;
     
     private static final String SERVER_NAME = 
"io.github.apache/shardingsphere-mcp";
     
@@ -155,7 +155,7 @@ public final class MCPRegistryMetadataCommand {
     }
     
     private static void validateServerJson(final Map<String, Object> server, 
final boolean allowSnapshot) {
-        
ShardingSpherePreconditions.checkState(SCHEMA_URL.equals(server.get("$schema")),
 () -> new IllegalArgumentException("server.json must use the official MCP 
Registry schema."));
+        
ShardingSpherePreconditions.checkState(REGISTRY_SCHEMA_URL.equals(server.get("$schema")),
 () -> new IllegalArgumentException("server.json must use the official MCP 
Registry schema."));
         
ShardingSpherePreconditions.checkState(SERVER_NAME.equals(server.get("name")),
                 () -> new IllegalArgumentException("server.json name must 
match the published ShardingSphere MCP server name."));
         validateString(server, "description", 100);
diff --git 
a/mcp/registry/src/test/java/org/apache/shardingsphere/mcp/registry/MCPRegistryMetadataCommandTest.java
 
b/mcp/registry/src/test/java/org/apache/shardingsphere/mcp/registry/MCPRegistryMetadataCommandTest.java
index f3456e7a357..70e63c9bc01 100644
--- 
a/mcp/registry/src/test/java/org/apache/shardingsphere/mcp/registry/MCPRegistryMetadataCommandTest.java
+++ 
b/mcp/registry/src/test/java/org/apache/shardingsphere/mcp/registry/MCPRegistryMetadataCommandTest.java
@@ -32,12 +32,15 @@ import java.util.Map;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.is;
 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
 class MCPRegistryMetadataCommandTest {
     
     private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
     
+    private static final String REGISTRY_SCHEMA_URL = 
"https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json";;
+    
     private static final String PACKAGE_SHAPE_ERROR_MESSAGE = "server.json 
packages must contain exactly one stdio OCI package and one streamable-http OCI 
package.";
     
     @TempDir
@@ -168,6 +171,13 @@ class MCPRegistryMetadataCommandTest {
         assertDoesNotThrow(() -> MCPRegistryMetadataCommand.execute("--path", 
serverPath.toString(), "--validate-only", "--allow-snapshot"));
     }
     
+    @Test
+    void 
assertSourceMetadataKeepsRegistrySchemaSeparateFromRuntimeProtocolVersion() 
throws IOException {
+        Map<String, Object> actual = 
readServerJson(resolveMCPDirectory().resolve("server.json"));
+        assertThat(actual.get("$schema"), is(REGISTRY_SCHEMA_URL));
+        assertFalse(actual.containsKey("protocolVersion"));
+    }
+    
     @Test
     void assertExecuteValidatesDockerfileMetadata() throws IOException {
         Path serverPath = createServerJson(createServerMetadata());
@@ -276,7 +286,7 @@ class MCPRegistryMetadataCommandTest {
     
     private Map<String, Object> createServerMetadata() {
         Map<String, Object> result = new LinkedHashMap<>(6, 1F);
-        result.put("$schema", 
"https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json";);
+        result.put("$schema", REGISTRY_SCHEMA_URL);
         result.put("name", "io.github.apache/shardingsphere-mcp");
         result.put("title", "Apache ShardingSphere MCP");
         result.put("description", "MCP Server for Apache ShardingSphere 
metadata discovery, SQL preview, and rule workflows");
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/programmatic/HttpTransportProtocolContractE2ETest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/programmatic/HttpTransportProtocolContractE2ETest.java
index 576124974e0..8159ab6d1dc 100644
--- 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/programmatic/HttpTransportProtocolContractE2ETest.java
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/programmatic/HttpTransportProtocolContractE2ETest.java
@@ -18,8 +18,12 @@
 package org.apache.shardingsphere.test.e2e.mcp.runtime.programmatic;
 
 import org.apache.shardingsphere.mcp.support.security.MCPClientSafetyPolicy;
+import 
org.apache.shardingsphere.test.e2e.mcp.support.transport.MCPInteractionPayloads;
 import 
org.apache.shardingsphere.test.e2e.mcp.support.transport.client.MCPHttpTransportTestSupport;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
 
 import java.io.IOException;
 import java.io.InputStream;
@@ -27,7 +31,9 @@ import java.net.http.HttpClient;
 import java.net.http.HttpRequest;
 import java.net.http.HttpResponse;
 import java.util.LinkedHashMap;
+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;
@@ -50,6 +56,7 @@ class HttpTransportProtocolContractE2ETest extends 
AbstractHttpProtocolOnlyE2ETe
         Map<String, Object> actualResult = 
castToMap(actualPayload.get("result"));
         assertThat(String.valueOf(actualPayload.get("jsonrpc")), is("2.0"));
         assertThat(String.valueOf(actualResult.get("protocolVersion")), 
is(getProtocolVersion()));
+        assertServerCapabilities(castToMap(actualResult.get("capabilities")));
         assertThat(sendInitializedNotification(httpClient, 
actualSessionId).statusCode(), is(202));
     }
     
@@ -124,6 +131,48 @@ class HttpTransportProtocolContractE2ETest extends 
AbstractHttpProtocolOnlyE2ETe
         assertThat(actualRecovery.get("expected_type"), is("string"));
     }
     
+    @Test
+    void assertReturnJsonRpcErrorForUnsupportedTool() throws IOException, 
InterruptedException {
+        launchHttpTransport();
+        HttpClient httpClient = HttpClient.newHttpClient();
+        String sessionId = initializeSession(httpClient);
+        HttpResponse<String> actual = sendRawPostRequest(httpClient, 
createSessionHeaders(sessionId), 
MCPHttpTransportTestSupport.createJsonRpcRequestBody(
+                "missing-tool-1", "tools/call", Map.of("name", 
"database_gateway_missing_tool", "arguments", Map.of())));
+        assertThat(actual.statusCode(), is(200));
+        Map<String, Object> actualPayload = parseJsonBody(actual.body());
+        assertTrue(MCPInteractionPayloads.hasJsonRpcError(actualPayload));
+        assertFalse(actualPayload.containsKey("result"));
+        Map<String, Object> actualError = 
castToMap(actualPayload.get("error"));
+        assertTrue(actualError.containsKey("code"));
+        assertTrue(actualError.containsKey("message"));
+    }
+    
+    @Test
+    void assertExposeToolInputSchemaConstraints() throws IOException, 
InterruptedException {
+        launchHttpTransport();
+        HttpClient httpClient = HttpClient.newHttpClient();
+        String sessionId = initializeSession(httpClient);
+        Map<String, Object> actualResult = sendInitializedRequest(httpClient, 
sessionId, "tools-list-constraints", "tools/list", Map.of());
+        Map<String, Object> actualTool = 
MCPInteractionPayloads.castToList(actualResult.get("tools")).stream()
+                .filter(each -> 
"database_gateway_execute_update".equals(each.get("name"))).findFirst().orElseThrow();
+        Map<String, Object> actualProperties = 
castToMap(castToMap(actualTool.get("inputSchema")).get("properties"));
+        Map<String, Object> actualMaxRows = 
castToMap(actualProperties.get("max_rows"));
+        assertThat(actualMaxRows.get("minimum"), is(0));
+        assertThat(actualMaxRows.get("maximum"), is(5000));
+        
assertThat(castToMap(actualProperties.get("execution_mode")).get("enum"), 
is(List.of("execute", "preview")));
+    }
+    
+    @ParameterizedTest(name = "{0}")
+    @MethodSource("listMethodCases")
+    void assertListMethodCompletesWithoutPaginationCursor(final String method, 
final String resultKey) throws IOException, InterruptedException {
+        launchHttpTransport();
+        HttpClient httpClient = HttpClient.newHttpClient();
+        String sessionId = initializeSession(httpClient);
+        Map<String, Object> actualResult = sendInitializedRequest(httpClient, 
sessionId, method + "-1", method, Map.of());
+        
assertFalse(MCPInteractionPayloads.castToList(actualResult.get(resultKey)).isEmpty());
+        assertFalse(actualResult.containsKey("nextCursor"));
+    }
+    
     @Test
     void assertEnforceToolCallLimitPerSession() throws IOException, 
InterruptedException {
         String propertyName = 
MCPClientSafetyPolicy.MAX_TOOL_CALLS_PER_SESSION_PROPERTY;
@@ -167,6 +216,15 @@ class HttpTransportProtocolContractE2ETest extends 
AbstractHttpProtocolOnlyE2ETe
         assertThat(actual.statusCode(), is(400));
     }
     
+    @Test
+    void assertRejectFollowUpRequestWithoutProtocolHeader() throws 
IOException, InterruptedException {
+        launchHttpTransport();
+        HttpClient httpClient = HttpClient.newHttpClient();
+        String sessionId = initializeSession(httpClient);
+        HttpResponse<String> actual = sendCapabilitiesRequest(httpClient, 
Map.of("MCP-Session-Id", sessionId));
+        assertThat(actual.statusCode(), is(400));
+    }
+    
     @Test
     void assertRejectFollowUpRequestWithMissingSession() throws IOException, 
InterruptedException {
         launchHttpTransport();
@@ -200,6 +258,36 @@ class HttpTransportProtocolContractE2ETest extends 
AbstractHttpProtocolOnlyE2ETe
         
assertThat(String.valueOf(parseJsonBody(actual.body()).get("message")), 
is("Session not found: " + sessionId));
     }
     
+    private static Stream<Arguments> listMethodCases() {
+        return Stream.of(
+                Arguments.of("tools/list", "tools"),
+                Arguments.of("resources/list", "resources"),
+                Arguments.of("resources/templates/list", "resourceTemplates"),
+                Arguments.of("prompts/list", "prompts"));
+    }
+    
+    private void assertServerCapabilities(final Map<String, Object> 
capabilities) {
+        assertTrue(capabilities.containsKey("completions"));
+        assertFalse(capabilities.containsKey("experimental"));
+        assertFalse(capabilities.containsKey("tasks"));
+        Map<String, Object> resources = 
castToMap(capabilities.get("resources"));
+        assertFalse((boolean) resources.get("subscribe"));
+        assertFalse((boolean) resources.get("listChanged"));
+        Map<String, Object> tools = castToMap(capabilities.get("tools"));
+        assertFalse((boolean) tools.get("listChanged"));
+        Map<String, Object> prompts = castToMap(capabilities.get("prompts"));
+        assertFalse((boolean) prompts.get("listChanged"));
+    }
+    
+    private Map<String, Object> sendInitializedRequest(final HttpClient 
httpClient, final String sessionId, final String requestId, final String method,
+                                                       final Map<String, 
Object> params) throws IOException, InterruptedException {
+        HttpResponse<String> actual = sendRawPostRequest(httpClient, 
createSessionHeaders(sessionId), 
MCPHttpTransportTestSupport.createJsonRpcRequestBody(requestId, method, 
params));
+        assertThat(actual.statusCode(), is(200));
+        Map<String, Object> actualPayload = parseJsonBody(actual.body());
+        assertFalse(MCPInteractionPayloads.hasJsonRpcError(actualPayload));
+        return castToMap(actualPayload.get("result"));
+    }
+    
     private Map<String, Object> assertToolErrorRecovery(final 
HttpResponse<String> response, final String expectedCategory) {
         assertThat(response.statusCode(), is(200));
         Map<String, Object> result = 
castToMap(parseJsonBody(response.body()).get("result"));

Reply via email to