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 8c1f9e0ffa9 Unify runtime protection and diagnostics metadata (#38748)
8c1f9e0ffa9 is described below

commit 8c1f9e0ffa9069cbb571e5aca5b887dac9923b26
Author: Liang Zhang <[email protected]>
AuthorDate: Fri May 29 09:25:50 2026 +0800

    Unify runtime protection and diagnostics metadata (#38748)
---
 .../handler/capability/RuntimeStatusHandler.java   |  4 +-
 .../execute/SQLExecutionToolHandlerSupport.java    | 22 ++---
 .../capability/RuntimeStatusHandlerTest.java       | 21 +++++
 .../capability/ServerCapabilitiesHandlerTest.java  |  1 +
 .../support/security/MCPClientSafetyPolicy.java    | 22 ++---
 ...Policy.java => MCPRuntimeProtectionPolicy.java} | 76 ++++++++++++-----
 .../MCPModelFirstContractPayloadBuilderTest.java   |  1 +
 .../security/MCPClientSafetyPolicyTest.java        | 31 +++++--
 .../security/MCPRuntimeProtectionPolicyTest.java   | 96 ++++++++++++++++++++++
 9 files changed, 217 insertions(+), 57 deletions(-)

diff --git 
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/resource/handler/capability/RuntimeStatusHandler.java
 
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/resource/handler/capability/RuntimeStatusHandler.java
index 758d116fb12..8e892f84ad3 100644
--- 
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/resource/handler/capability/RuntimeStatusHandler.java
+++ 
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/resource/handler/capability/RuntimeStatusHandler.java
@@ -31,6 +31,7 @@ import 
org.apache.shardingsphere.mcp.support.protocol.MCPResourceHintUtils;
 import org.apache.shardingsphere.mcp.support.protocol.MCPResponseMode;
 import org.apache.shardingsphere.mcp.support.protocol.response.MCPMapResponse;
 import org.apache.shardingsphere.mcp.support.resource.MCPUriPathSegmentUtils;
+import 
org.apache.shardingsphere.mcp.support.security.MCPRuntimeProtectionPolicy;
 
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -58,7 +59,7 @@ public final class RuntimeStatusHandler implements 
MCPResourceHandler<MCPDatabas
     public MCPResponse handle(final MCPDatabaseHandlerContext handlerContext, 
final MCPUriVariables uriVariables) {
         List<MCPDatabaseMetadata> databases = 
handlerContext.getMetadataQueryFacade().queryDatabases();
         boolean hasConfiguredDatabase = !databases.isEmpty();
-        Map<String, Object> result = new LinkedHashMap<>(13, 1F);
+        Map<String, Object> result = new LinkedHashMap<>(14, 1F);
         result.put("response_mode", MCPResponseMode.RUNTIME);
         result.put("server_status", hasConfiguredDatabase ? "ready" : 
"configuration_required");
         result.put("status", hasConfiguredDatabase ? "available" : 
"configuration_required");
@@ -67,6 +68,7 @@ public final class RuntimeStatusHandler implements 
MCPResourceHandler<MCPDatabas
         result.put("configured_database_count", databases.size());
         result.put("databases", databases.stream().map(each -> 
createDatabaseStatus(handlerContext, each)).toList());
         result.put("readiness", createReadiness(hasConfiguredDatabase));
+        result.put("runtime_protection", 
MCPRuntimeProtectionPolicy.createRuntimeProtectionPayload());
         result.put("redaction_summary", Map.of("categories", List.of(), 
"redacted_count", 0, "marker", "******"));
         result.put("diagnostics", createDiagnostics(hasConfiguredDatabase));
         result.put("capability_fingerprint", 
MCPDescriptorCatalogIndex.getDescriptorCatalogFingerprint());
diff --git 
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/SQLExecutionToolHandlerSupport.java
 
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/SQLExecutionToolHandlerSupport.java
index 095a21cd13b..97ba784e7ce 100644
--- 
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/SQLExecutionToolHandlerSupport.java
+++ 
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/SQLExecutionToolHandlerSupport.java
@@ -25,20 +25,13 @@ import 
org.apache.shardingsphere.mcp.core.protocol.exception.MCPInvalidToolArgum
 import org.apache.shardingsphere.mcp.core.tool.request.MCPToolArguments;
 import 
org.apache.shardingsphere.mcp.support.database.capability.SupportedMCPStatement;
 import 
org.apache.shardingsphere.mcp.support.database.tool.request.SQLExecutionRequest;
+import 
org.apache.shardingsphere.mcp.support.security.MCPRuntimeProtectionPolicy;
 
 import java.util.Map;
 
 @NoArgsConstructor(access = AccessLevel.PRIVATE)
 final class SQLExecutionToolHandlerSupport {
     
-    private static final int DEFAULT_MAX_ROWS = 100;
-    
-    private static final int MAX_ROWS_LIMIT = 5000;
-    
-    private static final int DEFAULT_TIMEOUT_MILLISECONDS = 0;
-    
-    private static final int MAX_TIMEOUT_MILLISECONDS = 300000;
-    
     static boolean isReadOnlyStatement(final ClassificationResult 
classificationResult) {
         if (SupportedMCPStatement.QUERY == 
classificationResult.getStatementClass()) {
             return true;
@@ -49,7 +42,8 @@ final class SQLExecutionToolHandlerSupport {
     
     static void checkExecutionArguments(final MCPToolArguments toolArguments, 
final String sourceTool) {
         resolveMaxRows(toolArguments, sourceTool);
-        getIntegerArgument(toolArguments, sourceTool, "timeout_ms", 
DEFAULT_TIMEOUT_MILLISECONDS, DEFAULT_TIMEOUT_MILLISECONDS, 
MAX_TIMEOUT_MILLISECONDS, DEFAULT_TIMEOUT_MILLISECONDS);
+        getIntegerArgument(toolArguments, sourceTool, "timeout_ms", 
MCPRuntimeProtectionPolicy.DEFAULT_TIMEOUT_MILLISECONDS, 
MCPRuntimeProtectionPolicy.DEFAULT_TIMEOUT_MILLISECONDS,
+                MCPRuntimeProtectionPolicy.MAX_TIMEOUT_MILLISECONDS, 
MCPRuntimeProtectionPolicy.DEFAULT_TIMEOUT_MILLISECONDS);
     }
     
     static SQLExecutionRequest createExecutionRequest(final MCPToolCall 
toolCall, final MCPToolArguments toolArguments, final String sql, final String 
sourceTool) {
@@ -63,8 +57,9 @@ final class SQLExecutionToolHandlerSupport {
     private static SQLExecutionRequest createExecutionRequest(final 
MCPToolCall toolCall, final MCPToolArguments toolArguments, final String 
schema, final String sql, final String sourceTool,
                                                               final boolean 
readOnlyExecution) {
         return new SQLExecutionRequest(toolCall.getSessionId(), 
toolArguments.getStringArgument("database"), schema, sql,
-                resolveMaxRows(toolArguments, sourceTool), 
getIntegerArgument(toolArguments, sourceTool, "timeout_ms", 
DEFAULT_TIMEOUT_MILLISECONDS, DEFAULT_TIMEOUT_MILLISECONDS,
-                        MAX_TIMEOUT_MILLISECONDS, 
DEFAULT_TIMEOUT_MILLISECONDS),
+                resolveMaxRows(toolArguments, sourceTool), 
getIntegerArgument(toolArguments, sourceTool, "timeout_ms", 
MCPRuntimeProtectionPolicy.DEFAULT_TIMEOUT_MILLISECONDS,
+                        
MCPRuntimeProtectionPolicy.DEFAULT_TIMEOUT_MILLISECONDS, 
MCPRuntimeProtectionPolicy.MAX_TIMEOUT_MILLISECONDS,
+                        
MCPRuntimeProtectionPolicy.DEFAULT_TIMEOUT_MILLISECONDS),
                 readOnlyExecution);
     }
     
@@ -73,8 +68,9 @@ final class SQLExecutionToolHandlerSupport {
     }
     
     private static int resolveMaxRows(final MCPToolArguments toolArguments, 
final String sourceTool) {
-        int result = getIntegerArgument(toolArguments, sourceTool, "max_rows", 
DEFAULT_MAX_ROWS, 0, MAX_ROWS_LIMIT, DEFAULT_MAX_ROWS);
-        return 0 == result ? DEFAULT_MAX_ROWS : result;
+        int result = getIntegerArgument(toolArguments, sourceTool, "max_rows", 
MCPRuntimeProtectionPolicy.DEFAULT_MAX_ROWS, 0, 
MCPRuntimeProtectionPolicy.MAX_ROWS_LIMIT,
+                MCPRuntimeProtectionPolicy.DEFAULT_MAX_ROWS);
+        return 0 == result ? MCPRuntimeProtectionPolicy.DEFAULT_MAX_ROWS : 
result;
     }
     
     private static int getIntegerArgument(final MCPToolArguments 
toolArguments, final String sourceTool, final String argumentPath, final int 
defaultValue, final int minimumValue,
diff --git 
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/capability/RuntimeStatusHandlerTest.java
 
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/capability/RuntimeStatusHandlerTest.java
index 12a54da1e7c..38d66935792 100644
--- 
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/capability/RuntimeStatusHandlerTest.java
+++ 
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/capability/RuntimeStatusHandlerTest.java
@@ -20,6 +20,7 @@ package 
org.apache.shardingsphere.mcp.core.resource.handler.capability;
 import org.apache.shardingsphere.mcp.api.resource.MCPUriVariables;
 import org.apache.shardingsphere.mcp.core.context.MCPRequestScope;
 import org.apache.shardingsphere.mcp.core.resource.ResourceTestDataFactory;
+import 
org.apache.shardingsphere.mcp.support.security.MCPRuntimeProtectionPolicy;
 import org.junit.jupiter.api.Test;
 
 import java.util.List;
@@ -45,6 +46,7 @@ class RuntimeStatusHandlerTest {
             assertTrue(((List<?>) actual.get("databases")).stream().map(each 
-> ((Map<?, ?>) each).get("database")).anyMatch("logic_db"::equals));
             assertThat(((Map<?, ?>) 
actual.get("redaction_summary")).get("marker"), is("******"));
             assertRuntimeDiagnostics(actual, "ready");
+            assertRuntimeProtection(actual);
             
assertTrue(String.valueOf(actual.get("capability_fingerprint")).matches("[0-9a-f]{64}"));
             assertRuntimeCapability((List<?>) actual.get("databases"), 
"logic_db");
             assertThat(extractResourceUris((List<?>) 
actual.get("resources_to_read")), is(List.of("shardingsphere://capabilities", 
"shardingsphere://databases")));
@@ -73,6 +75,7 @@ class RuntimeStatusHandlerTest {
             assertFalse((Boolean) readiness.get("ready"));
             assertThat(readiness.get("reason"), is("No runtime databases are 
configured."));
             assertRuntimeDiagnostics(actual, "invalid_configuration");
+            assertRuntimeProtection(actual);
             assertThat(extractResourceUris((List<?>) 
actual.get("resources_to_read")), is(List.of("shardingsphere://capabilities")));
             List<?> nextActions = (List<?>) actual.get("next_actions");
             assertThat(((Map<?, ?>) nextActions.get(0)).get("type"), 
is("resource_read"));
@@ -97,6 +100,24 @@ class RuntimeStatusHandlerTest {
         assertTrue((Boolean) ((Map<?, ?>) 
actualOperatorNextActions.get(3)).get("secret_safe"));
     }
     
+    private void assertRuntimeProtection(final Map<String, Object> payload) {
+        Map<?, ?> actualRuntimeProtection = (Map<?, ?>) 
payload.get("runtime_protection");
+        Map<?, ?> actualToolCallLimit = (Map<?, ?>) 
actualRuntimeProtection.get("tool_call_limit");
+        assertThat(actualToolCallLimit.get("scope"), is("session"));
+        assertThat(actualToolCallLimit.get("property"), 
is(MCPRuntimeProtectionPolicy.MAX_TOOL_CALLS_PER_SESSION_PROPERTY));
+        Map<?, ?> actualSQLExecutionLimits = (Map<?, ?>) 
actualRuntimeProtection.get("sql_execution_limits");
+        Map<?, ?> actualMaxRows = (Map<?, ?>) 
actualSQLExecutionLimits.get("max_rows");
+        assertThat(actualMaxRows.get("default_value"), 
is(MCPRuntimeProtectionPolicy.DEFAULT_MAX_ROWS));
+        assertThat(actualMaxRows.get("maximum_value"), 
is(MCPRuntimeProtectionPolicy.MAX_ROWS_LIMIT));
+        assertThat(actualMaxRows.get("applied_field"), is("applied_max_rows"));
+        assertThat(actualMaxRows.get("truncation_field"), is("truncated"));
+        Map<?, ?> actualTimeout = (Map<?, ?>) 
actualSQLExecutionLimits.get("timeout_ms");
+        assertThat(actualTimeout.get("default_value"), 
is(MCPRuntimeProtectionPolicy.DEFAULT_TIMEOUT_MILLISECONDS));
+        assertThat(actualTimeout.get("maximum_value"), 
is(MCPRuntimeProtectionPolicy.MAX_TIMEOUT_MILLISECONDS));
+        assertThat(actualTimeout.get("applied_field"), 
is("applied_timeout_ms"));
+        assertThat(actualTimeout.get("zero_means"), is("server_default"));
+    }
+    
     private void assertRuntimeCapability(final List<?> databases, final String 
databaseName) {
         Map<?, ?> actualDatabase = databases.stream().map(each -> (Map<?, ?>) 
each).filter(each -> 
databaseName.equals(each.get("database"))).findFirst().orElseThrow();
         assertThat(actualDatabase.get("metadata_visibility"), is("ready"));
diff --git 
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/capability/ServerCapabilitiesHandlerTest.java
 
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/capability/ServerCapabilitiesHandlerTest.java
index 673f93ae490..eb84a97f46a 100644
--- 
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/capability/ServerCapabilitiesHandlerTest.java
+++ 
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/capability/ServerCapabilitiesHandlerTest.java
@@ -196,6 +196,7 @@ class ServerCapabilitiesHandlerTest {
         assertThat(actualClientSafetyPolicy.get("identity_scope"), 
is("mcp_session"));
         
assertTrue(String.valueOf(actualClientSafetyPolicy.get("transport_scope")).contains("trusted
 session attribution"));
         assertThat(((Map<?, ?>) 
actualClientSafetyPolicy.get("tool_call_limit")).get("scope"), is("session"));
+        assertThat(((Map<?, ?>) ((Map<?, ?>) 
actualClientSafetyPolicy.get("runtime_protection")).get("tool_call_limit")).get("scope"),
 is("session"));
         
assertTrue(String.valueOf(actualClientSafetyPolicy.get("abuse_guard")).contains("counted
 before dispatch"));
     }
     
diff --git 
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/security/MCPClientSafetyPolicy.java
 
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/security/MCPClientSafetyPolicy.java
index d6f74352886..7af98c3a84a 100644
--- 
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/security/MCPClientSafetyPolicy.java
+++ 
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/security/MCPClientSafetyPolicy.java
@@ -19,7 +19,6 @@ package org.apache.shardingsphere.mcp.support.security;
 
 import lombok.AccessLevel;
 import lombok.NoArgsConstructor;
-import org.apache.shardingsphere.mcp.support.protocol.MCPPayloadFieldNames;
 
 import java.util.LinkedHashMap;
 import java.util.Map;
@@ -30,9 +29,9 @@ import java.util.Map;
 @NoArgsConstructor(access = AccessLevel.PRIVATE)
 public final class MCPClientSafetyPolicy {
     
-    public static final int DEFAULT_MAX_TOOL_CALLS_PER_SESSION = 10000;
+    public static final int DEFAULT_MAX_TOOL_CALLS_PER_SESSION = 
MCPRuntimeProtectionPolicy.DEFAULT_MAX_TOOL_CALLS_PER_SESSION;
     
-    public static final String MAX_TOOL_CALLS_PER_SESSION_PROPERTY = 
"shardingsphere.mcp.maxToolCallsPerSession";
+    public static final String MAX_TOOL_CALLS_PER_SESSION_PROPERTY = 
MCPRuntimeProtectionPolicy.MAX_TOOL_CALLS_PER_SESSION_PROPERTY;
     
     /**
      * Get maximum tool calls per MCP session.
@@ -40,8 +39,7 @@ public final class MCPClientSafetyPolicy {
      * @return maximum tool calls per MCP session
      */
     public static int getMaxToolCallsPerSession() {
-        Integer configuredValue = 
Integer.getInteger(MAX_TOOL_CALLS_PER_SESSION_PROPERTY, 
DEFAULT_MAX_TOOL_CALLS_PER_SESSION);
-        return configuredValue > 0 ? configuredValue : 
DEFAULT_MAX_TOOL_CALLS_PER_SESSION;
+        return MCPRuntimeProtectionPolicy.getMaxToolCallsPerSession();
     }
     
     /**
@@ -50,24 +48,16 @@ public final class MCPClientSafetyPolicy {
      * @return model-facing safety policy payload
      */
     public static Map<String, Object> createModelFacingPayload() {
-        Map<String, Object> result = new LinkedHashMap<>(5, 1F);
+        Map<String, Object> result = new LinkedHashMap<>(6, 1F);
         result.put("identity_scope", "mcp_session");
         result.put("transport_scope",
                 "HTTP transport can bind trusted session attribution when 
configured; "
                         + "authentication and authorization remain outside the 
runtime in this release. "
                         + "STDIO inherits the local process boundary.");
-        result.put("tool_call_limit", createToolCallLimitPayload());
+        result.put("tool_call_limit", 
MCPRuntimeProtectionPolicy.createToolCallLimitPayload());
+        result.put("runtime_protection", 
MCPRuntimeProtectionPolicy.createRuntimeProtectionPayload());
         result.put("abuse_guard", "Every tool call is counted before dispatch, 
including invalid calls, so runaway model loops stop at the session quota.");
         result.put("external_model_boundary", "The MCP runtime never calls 
external model providers; live LLM E2E clients call configured endpoints 
outside the server.");
         return result;
     }
-    
-    private static Map<String, Object> createToolCallLimitPayload() {
-        Map<String, Object> result = new LinkedHashMap<>(4, 1F);
-        result.put("scope", "session");
-        result.put("max_calls", getMaxToolCallsPerSession());
-        result.put("property", MAX_TOOL_CALLS_PER_SESSION_PROPERTY);
-        result.put(MCPPayloadFieldNames.RECOVERY, "Close and recreate the MCP 
session after the quota is exhausted.");
-        return result;
-    }
 }
diff --git 
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/security/MCPClientSafetyPolicy.java
 
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/security/MCPRuntimeProtectionPolicy.java
similarity index 50%
copy from 
mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/security/MCPClientSafetyPolicy.java
copy to 
mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/security/MCPRuntimeProtectionPolicy.java
index d6f74352886..352ad15ec38 100644
--- 
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/security/MCPClientSafetyPolicy.java
+++ 
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/security/MCPRuntimeProtectionPolicy.java
@@ -17,23 +17,31 @@
 
 package org.apache.shardingsphere.mcp.support.security;
 
-import lombok.AccessLevel;
-import lombok.NoArgsConstructor;
 import org.apache.shardingsphere.mcp.support.protocol.MCPPayloadFieldNames;
 
 import java.util.LinkedHashMap;
 import java.util.Map;
 
 /**
- * MCP client-facing safety policy.
+ * MCP runtime protection policy.
  */
-@NoArgsConstructor(access = AccessLevel.PRIVATE)
-public final class MCPClientSafetyPolicy {
+public final class MCPRuntimeProtectionPolicy {
     
     public static final int DEFAULT_MAX_TOOL_CALLS_PER_SESSION = 10000;
     
     public static final String MAX_TOOL_CALLS_PER_SESSION_PROPERTY = 
"shardingsphere.mcp.maxToolCallsPerSession";
     
+    public static final int DEFAULT_MAX_ROWS = 100;
+    
+    public static final int MAX_ROWS_LIMIT = 5000;
+    
+    public static final int DEFAULT_TIMEOUT_MILLISECONDS = 0;
+    
+    public static final int MAX_TIMEOUT_MILLISECONDS = 300000;
+    
+    private MCPRuntimeProtectionPolicy() {
+    }
+    
     /**
      * Get maximum tool calls per MCP session.
      *
@@ -45,24 +53,11 @@ public final class MCPClientSafetyPolicy {
     }
     
     /**
-     * Create model-facing safety policy payload.
+     * Create tool call limit payload.
      *
-     * @return model-facing safety policy payload
+     * @return tool call limit payload
      */
-    public static Map<String, Object> createModelFacingPayload() {
-        Map<String, Object> result = new LinkedHashMap<>(5, 1F);
-        result.put("identity_scope", "mcp_session");
-        result.put("transport_scope",
-                "HTTP transport can bind trusted session attribution when 
configured; "
-                        + "authentication and authorization remain outside the 
runtime in this release. "
-                        + "STDIO inherits the local process boundary.");
-        result.put("tool_call_limit", createToolCallLimitPayload());
-        result.put("abuse_guard", "Every tool call is counted before dispatch, 
including invalid calls, so runaway model loops stop at the session quota.");
-        result.put("external_model_boundary", "The MCP runtime never calls 
external model providers; live LLM E2E clients call configured endpoints 
outside the server.");
-        return result;
-    }
-    
-    private static Map<String, Object> createToolCallLimitPayload() {
+    public static Map<String, Object> createToolCallLimitPayload() {
         Map<String, Object> result = new LinkedHashMap<>(4, 1F);
         result.put("scope", "session");
         result.put("max_calls", getMaxToolCallsPerSession());
@@ -70,4 +65,43 @@ public final class MCPClientSafetyPolicy {
         result.put(MCPPayloadFieldNames.RECOVERY, "Close and recreate the MCP 
session after the quota is exhausted.");
         return result;
     }
+    
+    /**
+     * Create runtime protection payload.
+     *
+     * @return runtime protection payload
+     */
+    public static Map<String, Object> createRuntimeProtectionPayload() {
+        Map<String, Object> result = new LinkedHashMap<>(2, 1F);
+        result.put("tool_call_limit", createToolCallLimitPayload());
+        result.put("sql_execution_limits", createSQLExecutionLimitsPayload());
+        return result;
+    }
+    
+    private static Map<String, Object> createSQLExecutionLimitsPayload() {
+        Map<String, Object> result = new LinkedHashMap<>(2, 1F);
+        result.put("max_rows", createMaxRowsPayload());
+        result.put("timeout_ms", createTimeoutPayload());
+        return result;
+    }
+    
+    private static Map<String, Object> createMaxRowsPayload() {
+        Map<String, Object> result = new LinkedHashMap<>(5, 1F);
+        result.put("default_value", DEFAULT_MAX_ROWS);
+        result.put("maximum_value", MAX_ROWS_LIMIT);
+        result.put("applied_field", "applied_max_rows");
+        result.put("truncation_field", "truncated");
+        result.put(MCPPayloadFieldNames.RECOVERY, "Retry with a narrower 
SELECT, stronger WHERE clause, or smaller projection when rows are truncated.");
+        return result;
+    }
+    
+    private static Map<String, Object> createTimeoutPayload() {
+        Map<String, Object> result = new LinkedHashMap<>(5, 1F);
+        result.put("default_value", DEFAULT_TIMEOUT_MILLISECONDS);
+        result.put("maximum_value", MAX_TIMEOUT_MILLISECONDS);
+        result.put("applied_field", "applied_timeout_ms");
+        result.put("zero_means", "server_default");
+        result.put(MCPPayloadFieldNames.RECOVERY, "Retry with a bounded 
timeout_ms value or omit timeout_ms to use the server default.");
+        return result;
+    }
 }
diff --git 
a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/descriptor/MCPModelFirstContractPayloadBuilderTest.java
 
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/descriptor/MCPModelFirstContractPayloadBuilderTest.java
index 93a67c66645..450a78cef04 100644
--- 
a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/descriptor/MCPModelFirstContractPayloadBuilderTest.java
+++ 
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/descriptor/MCPModelFirstContractPayloadBuilderTest.java
@@ -87,6 +87,7 @@ class MCPModelFirstContractPayloadBuilderTest {
         assertThat(actualClientSafetyPolicy.get("identity_scope"), 
is("mcp_session"));
         
assertTrue(String.valueOf(actualClientSafetyPolicy.get("transport_scope")).contains("trusted
 session attribution"));
         
assertThat(castToMap(actualClientSafetyPolicy.get("tool_call_limit")).get("scope"),
 is("session"));
+        
assertThat(castToMap(castToMap(actualClientSafetyPolicy.get("runtime_protection")).get("tool_call_limit")).get("scope"),
 is("session"));
         
assertTrue(String.valueOf(actualClientSafetyPolicy.get("external_model_boundary")).contains("never
 calls external model providers"));
     }
     
diff --git 
a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/security/MCPClientSafetyPolicyTest.java
 
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/security/MCPClientSafetyPolicyTest.java
index 8e910d02d25..e5b66a41b9b 100644
--- 
a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/security/MCPClientSafetyPolicyTest.java
+++ 
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/security/MCPClientSafetyPolicyTest.java
@@ -18,8 +18,12 @@
 package org.apache.shardingsphere.mcp.support.security;
 
 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.util.Map;
+import java.util.stream.Stream;
 
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.is;
@@ -27,14 +31,17 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 
 class MCPClientSafetyPolicyTest {
     
-    @Test
-    void assertGetMaxToolCallsPerSession() {
+    @ParameterizedTest(name = "{0}")
+    @MethodSource("assertGetMaxToolCallsPerSessionCases")
+    void assertGetMaxToolCallsPerSession(final String name, final String 
configuredValue, final int expectedMaxToolCallsPerSession) {
         String previous = 
System.getProperty(MCPClientSafetyPolicy.MAX_TOOL_CALLS_PER_SESSION_PROPERTY);
         try {
-            
System.setProperty(MCPClientSafetyPolicy.MAX_TOOL_CALLS_PER_SESSION_PROPERTY, 
"8");
-            assertThat(MCPClientSafetyPolicy.getMaxToolCallsPerSession(), 
is(8));
-            
System.setProperty(MCPClientSafetyPolicy.MAX_TOOL_CALLS_PER_SESSION_PROPERTY, 
"0");
-            assertThat(MCPClientSafetyPolicy.getMaxToolCallsPerSession(), 
is(MCPClientSafetyPolicy.DEFAULT_MAX_TOOL_CALLS_PER_SESSION));
+            if (null == configuredValue) {
+                
System.clearProperty(MCPClientSafetyPolicy.MAX_TOOL_CALLS_PER_SESSION_PROPERTY);
+            } else {
+                
System.setProperty(MCPClientSafetyPolicy.MAX_TOOL_CALLS_PER_SESSION_PROPERTY, 
configuredValue);
+            }
+            assertThat(MCPClientSafetyPolicy.getMaxToolCallsPerSession(), 
is(expectedMaxToolCallsPerSession));
         } finally {
             restoreProperty(previous);
         }
@@ -49,6 +56,18 @@ class MCPClientSafetyPolicyTest {
         Map<?, ?> actualToolCallLimit = (Map<?, ?>) 
actual.get("tool_call_limit");
         assertThat(actualToolCallLimit.get("scope"), is("session"));
         assertThat(actualToolCallLimit.get("property"), 
is(MCPClientSafetyPolicy.MAX_TOOL_CALLS_PER_SESSION_PROPERTY));
+        Map<?, ?> actualRuntimeProtection = (Map<?, ?>) 
actual.get("runtime_protection");
+        assertThat(((Map<?, ?>) 
actualRuntimeProtection.get("tool_call_limit")).get("scope"), is("session"));
+        Map<?, ?> actualSQLExecutionLimits = (Map<?, ?>) 
actualRuntimeProtection.get("sql_execution_limits");
+        assertThat(((Map<?, ?>) 
actualSQLExecutionLimits.get("max_rows")).get("applied_field"), 
is("applied_max_rows"));
+        assertThat(((Map<?, ?>) 
actualSQLExecutionLimits.get("timeout_ms")).get("applied_field"), 
is("applied_timeout_ms"));
+    }
+    
+    private static Stream<Arguments> assertGetMaxToolCallsPerSessionCases() {
+        return Stream.of(
+                Arguments.of("configured value", "8", 8),
+                Arguments.of("non-positive value falls back to default", "0", 
MCPClientSafetyPolicy.DEFAULT_MAX_TOOL_CALLS_PER_SESSION),
+                Arguments.of("missing value falls back to default", null, 
MCPClientSafetyPolicy.DEFAULT_MAX_TOOL_CALLS_PER_SESSION));
     }
     
     private void restoreProperty(final String previous) {
diff --git 
a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/security/MCPRuntimeProtectionPolicyTest.java
 
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/security/MCPRuntimeProtectionPolicyTest.java
new file mode 100644
index 00000000000..8da426a91a9
--- /dev/null
+++ 
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/security/MCPRuntimeProtectionPolicyTest.java
@@ -0,0 +1,96 @@
+/*
+ * 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.mcp.support.security;
+
+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.util.Map;
+import java.util.stream.Stream;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+class MCPRuntimeProtectionPolicyTest {
+    
+    @ParameterizedTest(name = "{0}")
+    @MethodSource("assertGetMaxToolCallsPerSessionCases")
+    void assertGetMaxToolCallsPerSession(final String name, final String 
configuredValue, final int expectedMaxToolCallsPerSession) {
+        String previous = 
System.getProperty(MCPRuntimeProtectionPolicy.MAX_TOOL_CALLS_PER_SESSION_PROPERTY);
+        try {
+            if (null == configuredValue) {
+                
System.clearProperty(MCPRuntimeProtectionPolicy.MAX_TOOL_CALLS_PER_SESSION_PROPERTY);
+            } else {
+                
System.setProperty(MCPRuntimeProtectionPolicy.MAX_TOOL_CALLS_PER_SESSION_PROPERTY,
 configuredValue);
+            }
+            assertThat(MCPRuntimeProtectionPolicy.getMaxToolCallsPerSession(), 
is(expectedMaxToolCallsPerSession));
+        } finally {
+            restoreProperty(previous);
+        }
+    }
+    
+    @Test
+    void assertCreateToolCallLimitPayload() {
+        String previous = 
System.getProperty(MCPRuntimeProtectionPolicy.MAX_TOOL_CALLS_PER_SESSION_PROPERTY);
+        try {
+            
System.setProperty(MCPRuntimeProtectionPolicy.MAX_TOOL_CALLS_PER_SESSION_PROPERTY,
 "9");
+            Map<String, Object> actual = 
MCPRuntimeProtectionPolicy.createToolCallLimitPayload();
+            assertThat(actual.get("scope"), is("session"));
+            assertThat(actual.get("property"), 
is(MCPRuntimeProtectionPolicy.MAX_TOOL_CALLS_PER_SESSION_PROPERTY));
+            assertThat(actual.get("max_calls"), is(9));
+            assertThat(actual.get("recovery"), is("Close and recreate the MCP 
session after the quota is exhausted."));
+        } finally {
+            restoreProperty(previous);
+        }
+    }
+    
+    @Test
+    void assertCreateRuntimeProtectionPayload() {
+        Map<String, Object> actual = 
MCPRuntimeProtectionPolicy.createRuntimeProtectionPayload();
+        Map<?, ?> actualToolCallLimit = (Map<?, ?>) 
actual.get("tool_call_limit");
+        assertThat(actualToolCallLimit.get("scope"), is("session"));
+        Map<?, ?> actualSQLExecutionLimits = (Map<?, ?>) 
actual.get("sql_execution_limits");
+        Map<?, ?> actualMaxRows = (Map<?, ?>) 
actualSQLExecutionLimits.get("max_rows");
+        assertThat(actualMaxRows.get("default_value"), 
is(MCPRuntimeProtectionPolicy.DEFAULT_MAX_ROWS));
+        assertThat(actualMaxRows.get("maximum_value"), 
is(MCPRuntimeProtectionPolicy.MAX_ROWS_LIMIT));
+        assertThat(actualMaxRows.get("applied_field"), is("applied_max_rows"));
+        assertThat(actualMaxRows.get("truncation_field"), is("truncated"));
+        Map<?, ?> actualTimeout = (Map<?, ?>) 
actualSQLExecutionLimits.get("timeout_ms");
+        assertThat(actualTimeout.get("default_value"), 
is(MCPRuntimeProtectionPolicy.DEFAULT_TIMEOUT_MILLISECONDS));
+        assertThat(actualTimeout.get("maximum_value"), 
is(MCPRuntimeProtectionPolicy.MAX_TIMEOUT_MILLISECONDS));
+        assertThat(actualTimeout.get("applied_field"), 
is("applied_timeout_ms"));
+        assertThat(actualTimeout.get("zero_means"), is("server_default"));
+    }
+    
+    private static Stream<Arguments> assertGetMaxToolCallsPerSessionCases() {
+        return Stream.of(
+                Arguments.of("configured value", "8", 8),
+                Arguments.of("non-positive value falls back to default", "0", 
MCPRuntimeProtectionPolicy.DEFAULT_MAX_TOOL_CALLS_PER_SESSION),
+                Arguments.of("missing value falls back to default", null, 
MCPRuntimeProtectionPolicy.DEFAULT_MAX_TOOL_CALLS_PER_SESSION));
+    }
+    
+    private void restoreProperty(final String previous) {
+        if (null == previous) {
+            
System.clearProperty(MCPRuntimeProtectionPolicy.MAX_TOOL_CALLS_PER_SESSION_PROPERTY);
+        } else {
+            
System.setProperty(MCPRuntimeProtectionPolicy.MAX_TOOL_CALLS_PER_SESSION_PROPERTY,
 previous);
+        }
+    }
+}

Reply via email to