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