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 3c5c1d85b39 Improve MCP tool argument validation and HTTP contract 
coverage (#39020)
3c5c1d85b39 is described below

commit 3c5c1d85b3902ac36d86dd6704d36f6c1e1e9002
Author: Liang Zhang <[email protected]>
AuthorDate: Mon Jul 6 17:25:13 2026 +0800

    Improve MCP tool argument validation and HTTP contract coverage (#39020)
    
    * Improve ShardingSphere MCP prompt completions
    
    - add supported prompt completion targets for planning prompts
    - support storage unit completion aliases used by feature prompts
    - clarify execute_update as preview-or-execute side-effecting SQL
    
    * Improve MCP tool argument validation and HTTP contract coverage
    
    - Validate integer bounds and schema-valued additionalProperties in MCP 
tool arguments
    - Apply execution argument validation to execute_update preview mode
    - Return explicit 405 for unsupported Streamable HTTP GET event streams
    - Add focused unit and E2E coverage for tool errors, rate limits, and HTTP 
transport contracts
---
 .../server/http/StreamableHttpMCPServlet.java      | 23 +++++-
 .../server/http/StreamableHttpMCPServletTest.java  | 20 ++----
 .../core/tool/handler/MCPToolArgumentContract.java | 75 +++++++++++++++----
 .../handler/execute/ExecuteUpdateToolHandler.java  |  1 +
 .../tool/handler/ToolDefinitionRegistryTest.java   | 34 +++++++++
 .../execute/ExecuteUpdateToolHandlerTest.java      | 14 ++++
 .../AbstractHttpProtocolOnlyE2ETest.java           | 11 +++
 .../HttpTransportProtocolContractE2ETest.java      | 84 ++++++++++++++++++++++
 .../client/MCPHttpTransportTestSupport.java        | 18 +++++
 9 files changed, 251 insertions(+), 29 deletions(-)

diff --git 
a/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/transport/server/http/StreamableHttpMCPServlet.java
 
b/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/transport/server/http/StreamableHttpMCPServlet.java
index 21c7d125e02..fc849f2c811 100644
--- 
a/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/transport/server/http/StreamableHttpMCPServlet.java
+++ 
b/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/transport/server/http/StreamableHttpMCPServlet.java
@@ -61,6 +61,8 @@ final class StreamableHttpMCPServlet extends HttpServlet 
implements McpStreamabl
     
     private static final String JSON_CONTENT_TYPE = "application/json";
     
+    private static final String EVENT_STREAM_CONTENT_TYPE = 
"text/event-stream";
+    
     private final HttpServletStreamableServerTransportProvider delegate;
     
     private final McpJsonMapper jsonMapper;
@@ -135,8 +137,25 @@ final class StreamableHttpMCPServlet extends HttpServlet 
implements McpStreamabl
     }
     
     @Override
-    protected void doGet(final HttpServletRequest request, final 
HttpServletResponse response) throws IOException, ServletException {
-        serviceRequest(request, response);
+    protected void doGet(final HttpServletRequest request, final 
HttpServletResponse response) throws IOException {
+        setUtf8Encoding(request, response);
+        if (!isEventStreamAccepted(request)) {
+            response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Accept 
must include text/event-stream.");
+            return;
+        }
+        if (validateTransportSecurity(request, response)) {
+            response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, 
"HTTP GET event streams are not supported.");
+        }
+    }
+    
+    private boolean isEventStreamAccepted(final HttpServletRequest request) {
+        String acceptHeader = 
Objects.toString(request.getHeader(HttpHeaders.ACCEPT), "");
+        for (String each : acceptHeader.split(",")) {
+            if (EVENT_STREAM_CONTENT_TYPE.equalsIgnoreCase(each.split(";", 
2)[0].trim())) {
+                return true;
+            }
+        }
+        return false;
     }
     
     private void serviceWithApplicationClassLoader(final HttpServletRequest 
request, final HttpServletResponse response) throws IOException, 
ServletException {
diff --git 
a/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/server/http/StreamableHttpMCPServletTest.java
 
b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/server/http/StreamableHttpMCPServletTest.java
index 0d09037d737..1c8b82859a7 100644
--- 
a/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/server/http/StreamableHttpMCPServletTest.java
+++ 
b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/server/http/StreamableHttpMCPServletTest.java
@@ -182,22 +182,15 @@ class StreamableHttpMCPServletTest {
         HttpServletResponse response = mock(HttpServletResponse.class);
         when(delegate.closeGracefully()).thenReturn(Mono.empty());
         StreamableHttpMCPServlet actual = createServlet(delegate, 
mock(MCPSessionManager.class), mock(MCPSessionExecutionCoordinator.class));
-        doAnswer(invocation -> 
assertDelegatedRequest(invocation.getArgument(0), invocation.getArgument(1), 
request, response))
-                .when(delegate).service(any(HttpServletRequest.class), 
any(HttpServletResponse.class));
         actual.service(request, response);
         verify(request).setCharacterEncoding("UTF-8");
         verify(response).setCharacterEncoding("UTF-8");
-    }
-    
-    private Object assertDelegatedRequest(final HttpServletRequest 
actualRequest, final HttpServletResponse actualResponse, final 
HttpServletRequest request, final HttpServletResponse response) {
-        assertThat(actualRequest, is(request));
-        assertThat(actualResponse, is(response));
-        assertThat(Thread.currentThread().getContextClassLoader(), 
is(StreamableHttpMCPServlet.class.getClassLoader()));
-        return null;
+        verify(response).sendError(HttpServletResponse.SC_BAD_REQUEST, "Accept 
must include text/event-stream.");
+        verify(delegate, never()).service(any(HttpServletRequest.class), 
any(HttpServletResponse.class));
     }
     
     @Test
-    void assertServiceGetWithExistingAcceptHeader() throws ServletException, 
IOException, ReflectiveOperationException {
+    void assertRejectUnsupportedEventStreamGet() throws ServletException, 
IOException, ReflectiveOperationException {
         HttpServletStreamableServerTransportProvider delegate = 
mock(HttpServletStreamableServerTransportProvider.class);
         HttpServletRequest request = mock(HttpServletRequest.class);
         when(request.getMethod()).thenReturn("GET");
@@ -205,14 +198,11 @@ class StreamableHttpMCPServletTest {
         HttpServletResponse response = mock(HttpServletResponse.class);
         when(delegate.closeGracefully()).thenReturn(Mono.empty());
         StreamableHttpMCPServlet actual = createServlet(delegate, 
mock(MCPSessionManager.class), mock(MCPSessionExecutionCoordinator.class));
-        doAnswer(invocation -> {
-            assertThat(invocation.getArgument(0), is(request));
-            assertThat(((HttpServletRequest) 
invocation.getArgument(0)).getHeader(HttpHeaders.ACCEPT), 
is("text/event-stream"));
-            return null;
-        }).when(delegate).service(any(HttpServletRequest.class), 
any(HttpServletResponse.class));
         actual.service(request, response);
         verify(request).setCharacterEncoding("UTF-8");
         verify(response).setCharacterEncoding("UTF-8");
+        verify(response).sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, 
"HTTP GET event streams are not supported.");
+        verify(delegate, never()).service(any(HttpServletRequest.class), 
any(HttpServletResponse.class));
     }
     
     @Test
diff --git 
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/MCPToolArgumentContract.java
 
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/MCPToolArgumentContract.java
index c3477c43e7a..2f15c959d03 100644
--- 
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/MCPToolArgumentContract.java
+++ 
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/MCPToolArgumentContract.java
@@ -20,9 +20,11 @@ package org.apache.shardingsphere.mcp.core.tool.handler;
 import lombok.AccessLevel;
 import lombok.RequiredArgsConstructor;
 import org.apache.shardingsphere.infra.exception.ShardingSpherePreconditions;
+import 
org.apache.shardingsphere.mcp.api.protocol.exception.MCPInvalidRequestException;
 import 
org.apache.shardingsphere.mcp.core.protocol.exception.MCPExecutionModeRequiredException;
 import 
org.apache.shardingsphere.mcp.core.protocol.exception.MCPInvalidApprovedStepsException;
 import 
org.apache.shardingsphere.mcp.core.protocol.exception.MCPInvalidExecutionModeException;
+import 
org.apache.shardingsphere.mcp.core.protocol.exception.MCPInvalidToolArgumentException;
 import 
org.apache.shardingsphere.mcp.core.protocol.exception.MCPMissingToolArgumentException;
 import 
org.apache.shardingsphere.mcp.core.protocol.exception.MCPToolArgumentContractViolationException;
 import org.apache.shardingsphere.mcp.support.protocol.MCPPayloadFieldNames;
@@ -52,6 +54,12 @@ final class MCPToolArgumentContract {
     
     private static final String ENUM = "enum";
     
+    private static final String MINIMUM = "minimum";
+    
+    private static final String MAXIMUM = "maximum";
+    
+    private static final String DEFAULT_VALUE = "default";
+    
     private static final String STRING = "string";
     
     private static final String INTEGER = "integer";
@@ -74,13 +82,9 @@ final class MCPToolArgumentContract {
     
     private void validateObject(final Map<?, ?> arguments, final Map<?, ?> 
schema, final String path, final Map<String, Object> rootArguments) {
         validateRequiredArguments(arguments, schema, path, rootArguments);
-        validateUnknownArguments(arguments, schema, path, rootArguments);
         for (Entry<?, ?> entry : arguments.entrySet()) {
             String argumentName = Objects.toString(entry.getKey(), "");
-            Map<?, ?> property = findProperty(schema, argumentName);
-            if (!property.isEmpty()) {
-                validateArgument(entry.getValue(), property, appendPath(path, 
argumentName), rootArguments);
-            }
+            validatePropertyArgument(entry.getValue(), schema, 
appendPath(path, argumentName), rootArguments, argumentName);
         }
     }
     
@@ -107,15 +111,18 @@ final class MCPToolArgumentContract {
         ShardingSpherePreconditions.checkState(!actualValue.isEmpty(), () -> 
createMissingArgumentException(rootArguments, path));
     }
     
-    private void validateUnknownArguments(final Map<?, ?> arguments, final 
Map<?, ?> schema, final String path, final Map<String, Object> rootArguments) {
-        if (!Boolean.FALSE.equals(schema.get(ADDITIONAL_PROPERTIES))) {
+    private void validatePropertyArgument(final Object value, final Map<?, ?> 
schema, final String argumentPath, final Map<String, Object> rootArguments, 
final String argumentName) {
+        Map<?, ?> property = findProperty(schema, argumentName);
+        if (!property.isEmpty()) {
+            validateArgument(value, property, argumentPath, rootArguments);
             return;
         }
-        for (Object each : arguments.keySet()) {
-            String argumentName = Objects.toString(each, "");
-            if (findProperty(schema, argumentName).isEmpty()) {
-                throw createContractViolationException(rootArguments, 
appendPath(path, argumentName), "unknown_argument", "", List.of());
-            }
+        Object additionalProperties = schema.get(ADDITIONAL_PROPERTIES);
+        if (Boolean.FALSE.equals(additionalProperties)) {
+            throw createContractViolationException(rootArguments, 
argumentPath, "unknown_argument", "", List.of());
+        }
+        if (additionalProperties instanceof Map<?, ?>) {
+            validateArgument(value, (Map<?, ?>) additionalProperties, 
argumentPath, rootArguments);
         }
     }
     
@@ -125,6 +132,9 @@ final class MCPToolArgumentContract {
             ShardingSpherePreconditions.checkState(isValidType(value, 
expectedType),
                     () -> createContractViolationException(rootArguments, 
path, "invalid_argument_type", expectedType, List.of()));
         }
+        if (INTEGER.equals(expectedType)) {
+            validateIntegerRange(value, schema, path);
+        }
         validateEnumValue(value, schema, path, rootArguments);
         if (ARRAY.equals(expectedType)) {
             validateArrayArgument((Collection<?>) value, schema, path, 
rootArguments);
@@ -153,6 +163,47 @@ final class MCPToolArgumentContract {
         return !OBJECT.equals(expectedType) || value instanceof Map<?, ?>;
     }
     
+    private void validateIntegerRange(final Object value, final Map<?, ?> 
schema, final String path) {
+        if (!(schema.get(MINIMUM) instanceof Number) || !(schema.get(MAXIMUM) 
instanceof Number)) {
+            return;
+        }
+        BigInteger actualValue = toBigInteger(value);
+        BigInteger minimumValue = toBigInteger(schema.get(MINIMUM));
+        BigInteger maximumValue = toBigInteger(schema.get(MAXIMUM));
+        
ShardingSpherePreconditions.checkState(isIntegerWithinRange(actualValue, 
minimumValue, maximumValue), () -> createInvalidIntegerArgumentException(value, 
schema, path));
+    }
+    
+    private RuntimeException createInvalidIntegerArgumentException(final 
Object value, final Map<?, ?> schema, final String path) {
+        int minimum = getIntegerSchemaValue(schema, MINIMUM);
+        int maximum = getIntegerSchemaValue(schema, MAXIMUM);
+        return new MCPInvalidToolArgumentException(toolName, toolName, path, 
minimum, maximum, getSuggestedIntegerValue(value, schema, minimum, maximum),
+                new MCPInvalidRequestException(String.format("%s is out of 
range.", path)));
+    }
+    
+    private BigInteger toBigInteger(final Object value) {
+        return value instanceof BigInteger ? (BigInteger) value : 
BigInteger.valueOf(((Number) value).longValue());
+    }
+    
+    private int getIntegerSchemaValue(final Map<?, ?> schema, final String 
key) {
+        return ((Number) schema.get(key)).intValue();
+    }
+    
+    private int getSuggestedIntegerValue(final Object value, final Map<?, ?> 
schema, final int minimumValue, final int maximumValue) {
+        Object defaultValue = schema.get(DEFAULT_VALUE);
+        if (isValidType(defaultValue, INTEGER) && 
isIntegerWithinRange(defaultValue, minimumValue, maximumValue)) {
+            return ((Number) defaultValue).intValue();
+        }
+        return toBigInteger(value).compareTo(BigInteger.valueOf(minimumValue)) 
< 0 ? minimumValue : maximumValue;
+    }
+    
+    private boolean isIntegerWithinRange(final Object value, final int 
minimumValue, final int maximumValue) {
+        return isIntegerWithinRange(toBigInteger(value), 
BigInteger.valueOf(minimumValue), BigInteger.valueOf(maximumValue));
+    }
+    
+    private boolean isIntegerWithinRange(final BigInteger value, final 
BigInteger minimumValue, final BigInteger maximumValue) {
+        return value.compareTo(minimumValue) >= 0 && 
value.compareTo(maximumValue) <= 0;
+    }
+    
     private void validateEnumValue(final Object value, final Map<?, ?> schema, 
final String path, final Map<String, Object> rootArguments) {
         Object enumValues = schema.get(ENUM);
         if (enumValues instanceof Collection<?> && !((Collection<?>) 
enumValues).contains(value)) {
diff --git 
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteUpdateToolHandler.java
 
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteUpdateToolHandler.java
index 6c287fa9cd4..eacaa424c14 100644
--- 
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteUpdateToolHandler.java
+++ 
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteUpdateToolHandler.java
@@ -72,6 +72,7 @@ public final class ExecuteUpdateToolHandler implements 
MCPToolHandler<MCPDatabas
     public MCPResponse handle(final MCPDatabaseHandlerContext databaseContext, 
final MCPToolCall toolCall) {
         MCPToolArguments toolArguments = new 
MCPToolArguments(toolCall.getArguments());
         String executionMode = resolveExecutionMode(toolArguments);
+        SQLExecutionToolHandlerSupport.checkExecutionArguments(toolArguments, 
TOOL_NAME);
         String sql = toolArguments.getStringArgument("sql");
         ClassificationResult classificationResult = 
checkUpdateStatement(toolArguments, sql);
         if (EXECUTION_MODE_PREVIEW.equals(executionMode)) {
diff --git 
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/ToolDefinitionRegistryTest.java
 
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/ToolDefinitionRegistryTest.java
index 5be43b2e496..ec8fd4649d2 100644
--- 
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/ToolDefinitionRegistryTest.java
+++ 
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/ToolDefinitionRegistryTest.java
@@ -29,6 +29,7 @@ import 
org.apache.shardingsphere.mcp.core.context.MCPRuntimeContext;
 import 
org.apache.shardingsphere.mcp.core.protocol.exception.MCPExecutionModeRequiredException;
 import 
org.apache.shardingsphere.mcp.core.protocol.exception.MCPInvalidApprovedStepsException;
 import 
org.apache.shardingsphere.mcp.core.protocol.exception.MCPInvalidExecutionModeException;
+import 
org.apache.shardingsphere.mcp.core.protocol.exception.MCPInvalidToolArgumentException;
 import 
org.apache.shardingsphere.mcp.core.protocol.exception.MCPToolArgumentContractViolationException;
 import 
org.apache.shardingsphere.mcp.core.protocol.exception.UnsupportedToolException;
 import org.apache.shardingsphere.mcp.core.resource.ResourceTestDataFactory;
@@ -139,6 +140,20 @@ class ToolDefinitionRegistryTest {
         assertThat(actual.getSuggestedArguments(), is(Map.of("query", 
"order")));
     }
     
+    @Test
+    void assertDispatchWithIntegerAboveMaximum() {
+        MCPInvalidToolArgumentException actual = 
assertThrows(MCPInvalidToolArgumentException.class,
+                () -> dispatch("database_gateway_execute_update", 
Map.of("database", "logic_db", "sql", "UPDATE orders SET status = 'PAID' WHERE 
order_id = 1",
+                        "execution_mode", "preview", "max_rows", 5001)));
+        assertThat(actual.getMessage(), is("max_rows must be an integer 
between 0 and 5000."));
+        assertThat(actual.getSourceTool(), 
is("database_gateway_execute_update"));
+        assertThat(actual.getTargetTool(), 
is("database_gateway_execute_update"));
+        assertThat(actual.getArgumentPath(), is("max_rows"));
+        assertThat(actual.getMinimumValue(), is(0));
+        assertThat(actual.getMaximumValue(), is(5000));
+        assertThat(actual.getSuggestedValue(), is(100));
+    }
+    
     @Test
     void assertDispatchWithInvalidEnumArgument() {
         MCPToolArgumentContractViolationException actual = 
assertThrows(MCPToolArgumentContractViolationException.class,
@@ -200,6 +215,19 @@ class ToolDefinitionRegistryTest {
         assertThat(actual.getSuggestedArguments(), is(Map.of()));
     }
     
+    @Test
+    void assertValidateWithSchemaAdditionalProperties() {
+        MCPToolDescriptor descriptor = 
createAdditionalPropertiesFixtureToolDescriptor();
+        MCPToolArgumentContractViolationException actual = 
assertThrows(MCPToolArgumentContractViolationException.class,
+                () -> new MCPToolArgumentContract(descriptor.getName(), 
descriptor.getInputSchema()).validate(Map.of("options", Map.of("mode", 1))));
+        assertThat(actual.getMessage(), is("options.mode must be a string."));
+        assertThat(actual.getToolName(), is("fixture_tool"));
+        assertThat(actual.getArgumentPath(), is("options.mode"));
+        assertThat(actual.getCategory(), is("invalid_argument_type"));
+        assertThat(actual.getExpectedType(), is("string"));
+        assertThat(actual.getSuggestedArguments(), is(Map.of()));
+    }
+    
     @Test
     void assertGetSupportedToolsWithNoToolHandlers() {
         try (MockedStatic<ShardingSphereServiceLoader> mocked = 
mockStatic(ShardingSphereServiceLoader.class)) {
@@ -224,6 +252,12 @@ class ToolDefinitionRegistryTest {
                 "additionalProperties", false), Collections.emptyMap(), new 
MCPToolAnnotations("Fixture Tool", true, false, true, true), 
Collections.emptyMap());
     }
     
+    private static MCPToolDescriptor 
createAdditionalPropertiesFixtureToolDescriptor() {
+        Map<String, Object> optionSchema = Map.of("type", "object", 
"additionalProperties", Map.of("type", "string"));
+        return new MCPToolDescriptor("fixture_tool", "Fixture Tool", "Fixture 
tool.", Map.of("type", "object", "properties", Map.of("options", optionSchema), 
"required", List.of(),
+                "additionalProperties", false), Collections.emptyMap(), new 
MCPToolAnnotations("Fixture Tool", true, false, true, true), 
Collections.emptyMap());
+    }
+    
     private static void assertToolFields(final MCPToolDescriptor descriptor, 
final List<String> expectedFieldNames) {
         
assertThat(getInputProperties(descriptor).keySet().stream().map(Object::toString).toList(),
 is(expectedFieldNames));
     }
diff --git 
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteUpdateToolHandlerTest.java
 
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteUpdateToolHandlerTest.java
index cf636605810..64abba2e0b5 100644
--- 
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteUpdateToolHandlerTest.java
+++ 
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteUpdateToolHandlerTest.java
@@ -21,6 +21,7 @@ import 
org.apache.shardingsphere.mcp.api.protocol.exception.MCPInvalidRequestExc
 import 
org.apache.shardingsphere.mcp.api.protocol.exception.MCPUnsupportedException;
 import org.apache.shardingsphere.mcp.api.protocol.response.MCPResponse;
 import org.apache.shardingsphere.mcp.api.tool.MCPToolCall;
+import 
org.apache.shardingsphere.mcp.core.protocol.exception.MCPInvalidToolArgumentException;
 import 
org.apache.shardingsphere.mcp.support.database.MCPDatabaseHandlerContext;
 import 
org.apache.shardingsphere.mcp.support.database.capability.SupportedMCPStatement;
 import 
org.apache.shardingsphere.mcp.support.database.spi.MCPFeatureExecutionFacade;
@@ -140,6 +141,19 @@ class ExecuteUpdateToolHandlerTest {
         verifyNoInteractions(executionFacade);
     }
     
+    @Test
+    void assertRejectPreviewWithInvalidTimeout() {
+        MCPFeatureExecutionFacade executionFacade = 
mock(MCPFeatureExecutionFacade.class);
+        MCPDatabaseHandlerContext databaseContext = 
mock(MCPDatabaseHandlerContext.class);
+        when(databaseContext.getExecutionFacade()).thenReturn(executionFacade);
+        MCPInvalidToolArgumentException actual = 
assertThrows(MCPInvalidToolArgumentException.class, () -> new 
ExecuteUpdateToolHandler().handle(databaseContext, new MCPToolCall("session-1",
+                Map.of("database", "logic_db", "schema", "public", "sql", 
"update orders set status = 'PAID'", "execution_mode", "preview", "timeout_ms", 
300001))));
+        assertThat(actual.getMessage(), is("timeout_ms must be an integer 
between 0 and 300000."));
+        assertThat(actual.getArgumentPath(), is("timeout_ms"));
+        assertThat(actual.getSuggestedValue(), is(0));
+        verifyNoInteractions(executionFacade);
+    }
+    
     @Test
     void assertRejectUnknownExecutionMode() {
         MCPFeatureExecutionFacade executionFacade = 
mock(MCPFeatureExecutionFacade.class);
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/programmatic/AbstractHttpProtocolOnlyE2ETest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/programmatic/AbstractHttpProtocolOnlyE2ETest.java
index 261e3bd5354..5fc539d0eb8 100644
--- 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/programmatic/AbstractHttpProtocolOnlyE2ETest.java
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/programmatic/AbstractHttpProtocolOnlyE2ETest.java
@@ -27,6 +27,7 @@ import 
org.apache.shardingsphere.test.e2e.mcp.support.transport.client.MCPHttpTr
 import org.junit.jupiter.api.AfterEach;
 
 import java.io.IOException;
+import java.io.InputStream;
 import java.net.URI;
 import java.net.http.HttpClient;
 import java.net.http.HttpResponse;
@@ -92,6 +93,12 @@ abstract class AbstractHttpProtocolOnlyE2ETest {
         return MCPHttpTransportTestSupport.sendJsonRpcRequest(httpClient, 
getEndpointUri(), headers, "resource-1", "resources/read", Map.of("uri", 
"shardingsphere://capabilities"));
     }
     
+    protected final HttpResponse<String> sendToolCallRequest(final HttpClient 
httpClient, final String sessionId,
+                                                             final String 
toolName, final Map<String, Object> arguments) throws IOException, 
InterruptedException {
+        return MCPHttpTransportTestSupport.sendJsonRpcRequest(httpClient, 
getEndpointUri(), createSessionHeaders(sessionId), toolName + "-1", 
"tools/call",
+                Map.of("name", toolName, "arguments", arguments));
+    }
+    
     protected final HttpResponse<String> sendDeleteRequest(final HttpClient 
httpClient, final Map<String, String> headers) throws IOException, 
InterruptedException {
         return MCPHttpTransportTestSupport.sendDeleteRequest(httpClient, 
getEndpointUri(), headers);
     }
@@ -105,6 +112,10 @@ abstract class AbstractHttpProtocolOnlyE2ETest {
         return MCPHttpTransportTestSupport.openEventStream(httpClient, 
getEndpointUri(), headers);
     }
     
+    protected final HttpResponse<InputStream> openEventStreamInputStream(final 
HttpClient httpClient, final Map<String, String> headers) throws IOException, 
InterruptedException {
+        return 
MCPHttpTransportTestSupport.openEventStreamInputStream(httpClient, 
getEndpointUri(), headers);
+    }
+    
     protected final Map<String, Object> parseJsonBody(final String 
responseBody) {
         return MCPInteractionPayloads.parseJsonPayload(responseBody);
     }
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 05db95835de..e50fe15b5f7 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
@@ -17,10 +17,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.client.MCPHttpTransportTestSupport;
 import org.junit.jupiter.api.Test;
 
 import java.io.IOException;
+import java.io.InputStream;
 import java.net.http.HttpClient;
 import java.net.http.HttpRequest;
 import java.net.http.HttpResponse;
@@ -84,6 +86,65 @@ class HttpTransportProtocolContractE2ETest extends 
AbstractHttpProtocolOnlyE2ETe
         assertThat(actual.statusCode(), is(400));
     }
     
+    @Test
+    void assertRejectUnsupportedActiveEventStream() throws IOException, 
InterruptedException {
+        launchHttpTransport();
+        HttpClient httpClient = HttpClient.newHttpClient();
+        String sessionId = initializeSession(httpClient);
+        Map<String, String> headers = new 
LinkedHashMap<>(createSessionHeaders(sessionId));
+        headers.put("Accept", "text/event-stream");
+        HttpResponse<InputStream> actual = 
openEventStreamInputStream(httpClient, headers);
+        try (InputStream ignored = actual.body()) {
+            assertThat(actual.statusCode(), is(405));
+        }
+    }
+    
+    @Test
+    void assertReturnToolErrorForInvalidIntegerArgument() throws IOException, 
InterruptedException {
+        launchHttpTransport();
+        HttpClient httpClient = HttpClient.newHttpClient();
+        String sessionId = initializeSession(httpClient);
+        HttpResponse<String> actual = sendToolCallRequest(httpClient, 
sessionId, "database_gateway_execute_update", createInvalidUpdateArguments());
+        Map<String, Object> actualRecovery = assertToolErrorRecovery(actual, 
"invalid_integer_argument");
+        assertThat(actualRecovery.get("argument_path"), is("max_rows"));
+        assertThat(actualRecovery.get("minimum_value"), is(0));
+        assertThat(actualRecovery.get("maximum_value"), is(5000));
+        assertThat(actualRecovery.get("suggested_value"), is(100));
+    }
+    
+    @Test
+    void assertReturnToolErrorForSchemaAdditionalProperties() throws 
IOException, InterruptedException {
+        launchHttpTransport();
+        HttpClient httpClient = HttpClient.newHttpClient();
+        String sessionId = initializeSession(httpClient);
+        HttpResponse<String> actual = sendToolCallRequest(httpClient, 
sessionId, "database_gateway_plan_readwrite_splitting_rule",
+                Map.of("load_balancer_properties", Map.of("weight", 1)));
+        Map<String, Object> actualRecovery = assertToolErrorRecovery(actual, 
"invalid_argument_type");
+        assertThat(actualRecovery.get("argument_path"), 
is("load_balancer_properties.weight"));
+        assertThat(actualRecovery.get("expected_type"), is("string"));
+    }
+    
+    @Test
+    void assertEnforceToolCallLimitPerSession() throws IOException, 
InterruptedException {
+        String propertyName = 
MCPClientSafetyPolicy.MAX_TOOL_CALLS_PER_SESSION_PROPERTY;
+        String originalValue = System.getProperty(propertyName);
+        System.setProperty(propertyName, "1");
+        try {
+            launchHttpTransport();
+            HttpClient httpClient = HttpClient.newHttpClient();
+            String sessionId = initializeSession(httpClient);
+            assertToolErrorRecovery(sendToolCallRequest(httpClient, sessionId, 
"database_gateway_execute_update", createInvalidUpdateArguments()), 
"invalid_integer_argument");
+            Map<String, Object> actualRecovery = assertToolErrorRecovery(
+                    sendToolCallRequest(httpClient, sessionId, 
"database_gateway_execute_update", createInvalidUpdateArguments()), 
"tool_call_limit_exceeded");
+            assertThat(actualRecovery.get("session_id"), is(sessionId));
+            assertThat(actualRecovery.get("max_tool_calls_per_session"), 
is(1));
+            String otherSessionId = initializeSession(httpClient);
+            assertToolErrorRecovery(sendToolCallRequest(httpClient, 
otherSessionId, "database_gateway_execute_update", 
createInvalidUpdateArguments()), "invalid_integer_argument");
+        } finally {
+            restoreProperty(propertyName, originalValue);
+        }
+    }
+    
     @Test
     void assertAcceptInitializeWithUnsupportedProtocolVersion() throws 
IOException, InterruptedException {
         launchHttpTransport();
@@ -138,4 +199,27 @@ class HttpTransportProtocolContractE2ETest extends 
AbstractHttpProtocolOnlyE2ETe
         assertThat(actual.statusCode(), is(404));
         
assertThat(String.valueOf(parseJsonBody(actual.body()).get("message")), 
is("Session not found: " + sessionId));
     }
+    
+    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"));
+        assertTrue((boolean) result.get("isError"));
+        Map<String, Object> structuredContent = 
castToMap(result.get("structuredContent"));
+        assertThat(structuredContent.get("response_mode"), is("recovery"));
+        Map<String, Object> actualRecovery = 
castToMap(structuredContent.get("recovery"));
+        assertThat(actualRecovery.get("category"), is(expectedCategory));
+        return actualRecovery;
+    }
+    
+    private Map<String, Object> createInvalidUpdateArguments() {
+        return Map.of("database", "logic_db", "sql", "UPDATE orders SET status 
= 'PAID' WHERE order_id = 1", "execution_mode", "preview", "max_rows", 5001);
+    }
+    
+    private void restoreProperty(final String propertyName, final String 
originalValue) {
+        if (null == originalValue) {
+            System.clearProperty(propertyName);
+        } else {
+            System.setProperty(propertyName, originalValue);
+        }
+    }
 }
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/transport/client/MCPHttpTransportTestSupport.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/transport/client/MCPHttpTransportTestSupport.java
index 9de3c8c2b43..3b94679f68a 100644
--- 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/transport/client/MCPHttpTransportTestSupport.java
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/transport/client/MCPHttpTransportTestSupport.java
@@ -22,6 +22,7 @@ import lombok.NoArgsConstructor;
 import 
org.apache.shardingsphere.test.e2e.mcp.support.transport.MCPInteractionProtocolSupport;
 
 import java.io.IOException;
+import java.io.InputStream;
 import java.net.URI;
 import java.net.http.HttpClient;
 import java.net.http.HttpRequest;
@@ -131,6 +132,23 @@ public final class MCPHttpTransportTestSupport {
         return httpClient.send(requestBuilder.build(), 
HttpResponse.BodyHandlers.ofString());
     }
     
+    /**
+     * Open an event stream without waiting for the whole response body.
+     *
+     * @param httpClient HTTP client
+     * @param endpointUri MCP endpoint URI
+     * @param headers request headers
+     * @return HTTP response
+     * @throws IOException I/O exception
+     * @throws InterruptedException interrupted exception
+     */
+    public static HttpResponse<InputStream> openEventStreamInputStream(final 
HttpClient httpClient, final URI endpointUri,
+                                                                       final 
Map<String, String> headers) throws IOException, InterruptedException {
+        HttpRequest.Builder requestBuilder = 
HttpRequest.newBuilder(endpointUri).GET();
+        applyHeaders(requestBuilder, headers);
+        return httpClient.send(requestBuilder.build(), 
HttpResponse.BodyHandlers.ofInputStream());
+    }
+    
     /**
      * Send a JSON-RPC request.
      *


Reply via email to