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 150323ca988 Refactor MCP model-facing payload contract (#39004)
150323ca988 is described below

commit 150323ca988748a5c1a328669d119b4c657a98c6
Author: Liang Zhang <[email protected]>
AuthorDate: Sun Jul 5 15:41:13 2026 +0800

    Refactor MCP model-facing payload contract (#39004)
    
    * Refine MCP resource URI template API contract
    
    * Refactor MCP model-facing payload contract
    
    - Add shared MCP model-facing payload contract for canonical fields and 
next_actions
    - Reuse the contract in descriptor validation and MCP E2E assertions
    - Move encrypt DistSQL descriptor checks into the encrypt feature test
    - Derive MCP E2E read-only tool planning from descriptor annotations
    - Preserve model-critical payload fields in E2E response formatting
---
 .../encrypt/EncryptDescriptorContractTest.java     |  74 +++++++++++++
 .../MCPToolDescriptorCatalogValidator.java         |  44 --------
 .../descriptor/MCPToolOutputSchemaValidator.java   |  56 ++--------
 .../protocol/MCPModelFacingPayloadContract.java    | 115 +++++++++++++++++++++
 .../MCPModelFacingPayloadContractTest.java         |  66 ++++++++++++
 .../LLMMCPConversationTurnPlanner.java             |  40 +++++--
 .../LLMMCPConversationTurnPlannerTest.java         |  24 +++--
 .../LLMMCPModelFacingToolResponseFormatter.java    |  22 ++--
 ...LLMMCPModelFacingToolResponseFormatterTest.java |  31 ++++++
 .../assertion/MCPModelContractAssertions.java      |  36 ++-----
 10 files changed, 368 insertions(+), 140 deletions(-)

diff --git 
a/mcp/features/encrypt/src/test/java/org/apache/shardingsphere/mcp/feature/encrypt/EncryptDescriptorContractTest.java
 
b/mcp/features/encrypt/src/test/java/org/apache/shardingsphere/mcp/feature/encrypt/EncryptDescriptorContractTest.java
new file mode 100644
index 00000000000..0bbec63b449
--- /dev/null
+++ 
b/mcp/features/encrypt/src/test/java/org/apache/shardingsphere/mcp/feature/encrypt/EncryptDescriptorContractTest.java
@@ -0,0 +1,74 @@
+/*
+ * 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.feature.encrypt;
+
+import org.apache.shardingsphere.mcp.api.tool.descriptor.MCPToolDescriptor;
+import org.apache.shardingsphere.mcp.support.descriptor.MCPDescriptorCatalog;
+import 
org.apache.shardingsphere.mcp.support.descriptor.MCPDescriptorCatalogLoader;
+import org.junit.jupiter.api.Test;
+
+import java.util.Collection;
+import java.util.Locale;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+class EncryptDescriptorContractTest {
+    
+    @Test
+    void assertEncryptDistSQLExamples() {
+        
assertEncryptDistSQLExampleValue(findToolDescriptor().getOutputSchema().get("examples"));
+    }
+    
+    private MCPToolDescriptor findToolDescriptor() {
+        MCPDescriptorCatalog catalog = MCPDescriptorCatalogLoader.load();
+        return catalog.getProtocolDescriptors().getToolDescriptors().stream()
+                .filter(each -> 
EncryptFeatureDefinition.PLAN_TOOL_NAME.equals(each.getName())).findFirst().orElseThrow();
+    }
+    
+    private void assertEncryptDistSQLExampleValue(final Object value) {
+        if (value instanceof Map) {
+            assertEncryptDistSQLExampleMap((Map<?, ?>) value);
+        } else if (value instanceof Collection) {
+            for (Object each : (Collection<?>) value) {
+                assertEncryptDistSQLExampleValue(each);
+            }
+        }
+    }
+    
+    private void assertEncryptDistSQLExampleMap(final Map<?, ?> value) {
+        Object sql = value.get("sql");
+        if (null != sql && isEncryptRuleDistSQL(sql.toString())) {
+            assertEncryptRuleDistSQL(sql.toString());
+        }
+        for (Object each : value.values()) {
+            assertEncryptDistSQLExampleValue(each);
+        }
+    }
+    
+    private boolean isEncryptRuleDistSQL(final String sql) {
+        String actualSQL = sql.toUpperCase(Locale.ENGLISH);
+        return actualSQL.contains("CREATE ENCRYPT RULE") || 
actualSQL.contains("ALTER ENCRYPT RULE");
+    }
+    
+    private void assertEncryptRuleDistSQL(final String sql) {
+        String actualSQL = sql.toLowerCase(Locale.ENGLISH);
+        assertFalse(actualSQL.contains("type(name=aes"));
+        assertFalse(actualSQL.contains("'aes-key-value'") && 
!actualSQL.contains("'digest-algorithm-name'"));
+    }
+}
diff --git 
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPToolDescriptorCatalogValidator.java
 
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPToolDescriptorCatalogValidator.java
index 66e4f175c6e..71cfe22ba95 100644
--- 
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPToolDescriptorCatalogValidator.java
+++ 
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPToolDescriptorCatalogValidator.java
@@ -27,7 +27,6 @@ import 
org.apache.shardingsphere.mcp.support.workflow.model.WorkflowFieldNames;
 import java.util.Collection;
 import java.util.LinkedHashMap;
 import java.util.List;
-import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
@@ -224,49 +223,6 @@ public final class MCPToolDescriptorCatalogValidator {
             
MCPToolDescriptorValidationUtils.validateRequiredWorkflowPlanOutputFields(descriptor);
         }
         
MCPToolDescriptorValidationUtils.validateRequiredWorkflowPlanMetaFields(descriptor);
-        validateEncryptDistSQLExamples(descriptor);
-    }
-    
-    private static void validateEncryptDistSQLExamples(final MCPToolDescriptor 
descriptor) {
-        Object examples = descriptor.getOutputSchema().get("examples");
-        if (examples instanceof Collection) {
-            for (Object each : (Collection<?>) examples) {
-                validateEncryptDistSQLExample(descriptor, each);
-            }
-        }
-    }
-    
-    private static void validateEncryptDistSQLExample(final MCPToolDescriptor 
descriptor, final Object value) {
-        if (value instanceof Map) {
-            validateEncryptDistSQLExampleMap(descriptor, (Map<?, ?>) value);
-        } else if (value instanceof Collection) {
-            for (Object each : (Collection<?>) value) {
-                validateEncryptDistSQLExample(descriptor, each);
-            }
-        }
-    }
-    
-    private static void validateEncryptDistSQLExampleMap(final 
MCPToolDescriptor descriptor, final Map<?, ?> value) {
-        Object sql = value.get("sql");
-        if (null != sql && isEncryptRuleDistSQL(sql.toString())) {
-            validateEncryptRuleDistSQL(descriptor, sql.toString());
-        }
-        for (Object each : value.values()) {
-            validateEncryptDistSQLExample(descriptor, each);
-        }
-    }
-    
-    private static boolean isEncryptRuleDistSQL(final String sql) {
-        String actualSQL = sql.toUpperCase(Locale.ENGLISH);
-        return actualSQL.contains("CREATE ENCRYPT RULE") || 
actualSQL.contains("ALTER ENCRYPT RULE");
-    }
-    
-    private static void validateEncryptRuleDistSQL(final MCPToolDescriptor 
descriptor, final String sql) {
-        String actualSQL = sql.toLowerCase(Locale.ENGLISH);
-        
ShardingSpherePreconditions.checkState(!actualSQL.contains("type(name=aes"),
-                () -> new IllegalStateException(String.format("Tool `%s` 
output example executable encrypt DistSQL must quote algorithm type as a string 
literal.", descriptor.getName())));
-        
ShardingSpherePreconditions.checkState(!actualSQL.contains("'aes-key-value'") 
|| actualSQL.contains("'digest-algorithm-name'"),
-                () -> new IllegalStateException(String.format("Tool `%s` 
output example executable AES DistSQL must include `digest-algorithm-name`.", 
descriptor.getName())));
     }
     
     private static void validateRelatedResourceUris(final MCPToolDescriptor 
descriptor, final Set<String> resourceIdentifiers, final Set<String> 
shardingSphereResourceIdentifiers) {
diff --git 
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPToolOutputSchemaValidator.java
 
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPToolOutputSchemaValidator.java
index cdd38749f76..5c65d80ca91 100644
--- 
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPToolOutputSchemaValidator.java
+++ 
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPToolOutputSchemaValidator.java
@@ -19,36 +19,20 @@ package org.apache.shardingsphere.mcp.support.descriptor;
 
 import org.apache.shardingsphere.infra.exception.ShardingSpherePreconditions;
 import org.apache.shardingsphere.mcp.api.tool.descriptor.MCPToolDescriptor;
+import 
org.apache.shardingsphere.mcp.support.protocol.MCPModelFacingPayloadContract;
 import org.apache.shardingsphere.mcp.support.protocol.MCPPayloadFieldNames;
 import org.apache.shardingsphere.mcp.support.protocol.MCPResponseMode;
 
 import java.util.Collection;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
-import java.util.Set;
 
 /**
  * Validator for model-facing MCP tool output schema contracts.
  */
 public final class MCPToolOutputSchemaValidator {
     
-    private static final Collection<String> REMOVED_MODEL_FACING_FIELDS = 
Set.of(
-            "target_tool", "target_resource", "required_arguments", 
"action_kind", "suggested_next_tool", "suggested_next_tools", 
"recommended_next_tool",
-            "recommended_recovery", "suggested_next_action", 
"approved_by_user", "requires_user_approval", "approval_required", 
"user_overrides");
-    
-    private static final Map<String, Collection<String>> 
NEXT_ACTION_ALLOWED_FIELDS = createNextActionAllowedFields();
-    
-    private static final Map<String, Collection<String>> 
NEXT_ACTION_REQUIRED_FIELDS = createNextActionRequiredFields();
-    
-    private static final Collection<String> NEXT_ACTION_SCHEMA_ALLOWED_FIELDS 
= createNextActionSchemaAllowedFields();
-    
-    private static final Collection<String> MODEL_CRITICAL_HINT_FIELDS = 
List.of(
-            MCPPayloadFieldNames.NEXT_ACTIONS, 
MCPPayloadFieldNames.RESOURCES_TO_READ, MCPPayloadFieldNames.RESOURCE, 
MCPPayloadFieldNames.PARENT_RESOURCE,
-            MCPPayloadFieldNames.NEXT_RESOURCES, "manual_artifact_summary", 
"manual_follow_up", "empty_state", "ambiguity_state", 
MCPPayloadFieldNames.RECOVERY, "recovery_guidance",
-            "remediation");
-    
     private static final Collection<String> CONTINUATION_MODES = 
List.of("none", "pagination", "metadata_search");
     
     private static final Collection<String> RECOVERY_CATEGORIES = 
List.of("not_found", "ambiguous", "empty_scope", "missing_context", 
"validation", "terminal",
@@ -57,32 +41,6 @@ public final class MCPToolOutputSchemaValidator {
     private MCPToolOutputSchemaValidator() {
     }
     
-    private static Map<String, Collection<String>> 
createNextActionAllowedFields() {
-        return Map.of(
-                "resource_read", Set.of("order", "type", "title", 
"resource_uri", "reason", "depends_on"),
-                "tool_call", Set.of("order", "type", "title", "tool_name", 
"arguments", "reason", "depends_on"),
-                "completion", Set.of("order", "type", "title", "ref", 
"argument", "context", "missing_context_arguments", "resume_ref", 
"resume_arguments", "reason", "depends_on"),
-                "ask_user", Set.of("order", "type", "title", "question", 
"required_inputs", "reason", "depends_on"),
-                "terminal", Set.of("order", "type", "title", "reason", 
"depends_on"));
-    }
-    
-    private static Map<String, Collection<String>> 
createNextActionRequiredFields() {
-        return Map.of(
-                "resource_read", Set.of("order", "type", "title", 
"resource_uri"),
-                "tool_call", Set.of("order", "type", "title", "tool_name", 
"arguments"),
-                "completion", Set.of("order", "type", "title", "ref", 
"argument"),
-                "ask_user", Set.of("order", "type", "title", "question"),
-                "terminal", Set.of("order", "type", "title"));
-    }
-    
-    private static Collection<String> createNextActionSchemaAllowedFields() {
-        Set<String> result = new HashSet<>();
-        for (Collection<String> each : NEXT_ACTION_ALLOWED_FIELDS.values()) {
-            result.addAll(each);
-        }
-        return result;
-    }
-    
     /**
      * Validate one tool output schema.
      *
@@ -191,7 +149,7 @@ public final class MCPToolOutputSchemaValidator {
     }
     
     private static void validateNoRemovedModelFacingField(final 
MCPToolDescriptor descriptor, final String fieldName) {
-        
ShardingSpherePreconditions.checkState(!REMOVED_MODEL_FACING_FIELDS.contains(fieldName),
+        
ShardingSpherePreconditions.checkState(!MCPModelFacingPayloadContract.isRemovedFieldName(fieldName),
                 () -> new IllegalStateException(String.format("Tool `%s` 
model-facing contract must use canonical fields instead of removed `%s`.", 
descriptor.getName(), fieldName)));
     }
     
@@ -207,10 +165,10 @@ public final class MCPToolOutputSchemaValidator {
     
     private static void validateConcreteNextAction(final MCPToolDescriptor 
descriptor, final Map<?, ?> action) {
         String type = String.valueOf(action.get("type"));
-        Collection<String> allowedFields = 
NEXT_ACTION_ALLOWED_FIELDS.get(type);
-        ShardingSpherePreconditions.checkState(null != allowedFields,
+        Collection<String> allowedFields = 
MCPModelFacingPayloadContract.getNextActionAllowedFields(type);
+        ShardingSpherePreconditions.checkState(!allowedFields.isEmpty(),
                 () -> new IllegalStateException(String.format("Tool `%s` 
next_actions example uses unknown type `%s`.", descriptor.getName(), type)));
-        for (String each : NEXT_ACTION_REQUIRED_FIELDS.get(type)) {
+        for (String each : 
MCPModelFacingPayloadContract.getNextActionRequiredFields(type)) {
             ShardingSpherePreconditions.checkState(action.containsKey(each),
                     () -> new IllegalStateException(String.format("Tool `%s` 
next_actions example `%s` must contain `%s`.", descriptor.getName(), type, 
each)));
         }
@@ -224,7 +182,7 @@ public final class MCPToolOutputSchemaValidator {
     private static void validateModelCriticalOutputHints(final 
MCPToolDescriptor descriptor, final Map<?, ?> properties) {
         for (Entry<?, ?> entry : properties.entrySet()) {
             String fieldName = String.valueOf(entry.getKey());
-            if (MODEL_CRITICAL_HINT_FIELDS.contains(fieldName)) {
+            if 
(MCPModelFacingPayloadContract.getModelCriticalFieldNames().contains(fieldName))
 {
                 validateModelCriticalOutputHint(descriptor, fieldName, 
entry.getValue());
             }
             validateNestedModelCriticalOutputHints(descriptor, 
entry.getValue());
@@ -273,7 +231,7 @@ public final class MCPToolOutputSchemaValidator {
         }
         for (Object each : ((Map<?, ?>) properties).keySet()) {
             String fieldName = String.valueOf(each);
-            
ShardingSpherePreconditions.checkState(NEXT_ACTION_SCHEMA_ALLOWED_FIELDS.contains(fieldName),
+            
ShardingSpherePreconditions.checkState(MCPModelFacingPayloadContract.getNextActionSchemaAllowedFields().contains(fieldName),
                     () -> new IllegalStateException(String.format("Tool `%s` 
next_actions item contains unsupported field `%s`.", descriptor.getName(), 
fieldName)));
         }
     }
diff --git 
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/protocol/MCPModelFacingPayloadContract.java
 
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/protocol/MCPModelFacingPayloadContract.java
new file mode 100644
index 00000000000..16d5f697dc7
--- /dev/null
+++ 
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/protocol/MCPModelFacingPayloadContract.java
@@ -0,0 +1,115 @@
+/*
+ * 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.protocol;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * MCP model-facing payload contract.
+ */
+public final class MCPModelFacingPayloadContract {
+    
+    private static final Collection<String> REMOVED_MODEL_FACING_FIELDS = 
Set.of(
+            "target_tool", "target_resource", "required_arguments", 
"action_kind", "suggested_next_tool", "suggested_next_tools", 
"recommended_next_tool",
+            "recommended_recovery", "suggested_next_action", 
"approved_by_user", "requires_user_approval", "approval_required", 
"user_overrides");
+    
+    private static final Map<String, Collection<String>> 
NEXT_ACTION_REQUIRED_FIELDS = Map.of(
+            "resource_read", Set.of("order", "type", "title", "resource_uri"),
+            "tool_call", Set.of("order", "type", "title", "tool_name", 
"arguments"),
+            "completion", Set.of("order", "type", "title", "ref", "argument"),
+            "ask_user", Set.of("order", "type", "title", "question"),
+            "terminal", Set.of("order", "type", "title"));
+    
+    private static final Map<String, Collection<String>> 
NEXT_ACTION_ALLOWED_FIELDS = Map.of(
+            "resource_read", Set.of("order", "type", "title", "resource_uri", 
"reason", "depends_on"),
+            "tool_call", Set.of("order", "type", "title", "tool_name", 
"arguments", "reason", "depends_on"),
+            "completion", Set.of("order", "type", "title", "ref", "argument", 
"context", "missing_context_arguments", "resume_ref", "resume_arguments", 
"reason", "depends_on"),
+            "ask_user", Set.of("order", "type", "title", "question", 
"required_inputs", "reason", "depends_on"),
+            "terminal", Set.of("order", "type", "title", "reason", 
"depends_on"));
+    
+    private static final Collection<String> NEXT_ACTION_SCHEMA_ALLOWED_FIELDS 
= createNextActionSchemaAllowedFields();
+    
+    private static final Collection<String> MODEL_CRITICAL_FIELD_NAMES = 
List.of(
+            MCPPayloadFieldNames.NEXT_ACTIONS, 
MCPPayloadFieldNames.RESOURCES_TO_READ, MCPPayloadFieldNames.RESOURCE, 
MCPPayloadFieldNames.PARENT_RESOURCE,
+            MCPPayloadFieldNames.NEXT_RESOURCES, "manual_artifact_summary", 
"manual_follow_up", "empty_state", "ambiguity_state", 
MCPPayloadFieldNames.RECOVERY, "recovery_guidance",
+            "remediation");
+    
+    private MCPModelFacingPayloadContract() {
+    }
+    
+    private static Collection<String> createNextActionSchemaAllowedFields() {
+        Set<String> result = new LinkedHashSet<>();
+        for (Collection<String> each : NEXT_ACTION_ALLOWED_FIELDS.values()) {
+            result.addAll(each);
+        }
+        return Collections.unmodifiableSet(result);
+    }
+    
+    /**
+     * Check whether a field name is a removed model-facing alias.
+     *
+     * @param fieldName field name
+     * @return true if removed, otherwise false
+     */
+    public static boolean isRemovedFieldName(final String fieldName) {
+        return REMOVED_MODEL_FACING_FIELDS.contains(fieldName);
+    }
+    
+    /**
+     * Get required fields for a canonical next_actions item type.
+     *
+     * @param actionType action type
+     * @return required field names
+     */
+    public static Collection<String> getNextActionRequiredFields(final String 
actionType) {
+        return NEXT_ACTION_REQUIRED_FIELDS.getOrDefault(actionType, List.of());
+    }
+    
+    /**
+     * Get allowed fields for a canonical next_actions item type.
+     *
+     * @param actionType action type
+     * @return allowed field names
+     */
+    public static Collection<String> getNextActionAllowedFields(final String 
actionType) {
+        return NEXT_ACTION_ALLOWED_FIELDS.getOrDefault(actionType, List.of());
+    }
+    
+    /**
+     * Get fields allowed by the next_actions item output schema.
+     *
+     * @return allowed schema field names
+     */
+    public static Collection<String> getNextActionSchemaAllowedFields() {
+        return NEXT_ACTION_SCHEMA_ALLOWED_FIELDS;
+    }
+    
+    /**
+     * Get model-critical field names that must stay visible to MCP clients 
and E2E model prompts.
+     *
+     * @return model-critical field names
+     */
+    public static Collection<String> getModelCriticalFieldNames() {
+        return MODEL_CRITICAL_FIELD_NAMES;
+    }
+}
diff --git 
a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/protocol/MCPModelFacingPayloadContractTest.java
 
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/protocol/MCPModelFacingPayloadContractTest.java
new file mode 100644
index 00000000000..09707bbec39
--- /dev/null
+++ 
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/protocol/MCPModelFacingPayloadContractTest.java
@@ -0,0 +1,66 @@
+/*
+ * 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.protocol;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Collection;
+import java.util.Set;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class MCPModelFacingPayloadContractTest {
+    
+    @Test
+    void assertIsRemovedFieldNameWithRemovedAlias() {
+        
assertTrue(MCPModelFacingPayloadContract.isRemovedFieldName("target_tool"));
+    }
+    
+    @Test
+    void assertIsRemovedFieldNameWithCanonicalField() {
+        
assertFalse(MCPModelFacingPayloadContract.isRemovedFieldName(MCPPayloadFieldNames.NEXT_ACTIONS));
+    }
+    
+    @Test
+    void assertGetNextActionRequiredFields() {
+        
assertThat(MCPModelFacingPayloadContract.getNextActionRequiredFields("tool_call"),
 is(Set.of("order", "type", "title", "tool_name", "arguments")));
+    }
+    
+    @Test
+    void assertGetNextActionAllowedFields() {
+        
assertThat(MCPModelFacingPayloadContract.getNextActionAllowedFields("completion"),
+                is(Set.of("order", "type", "title", "ref", "argument", 
"context", "missing_context_arguments", "resume_ref", "resume_arguments", 
"reason", "depends_on")));
+    }
+    
+    @Test
+    void assertGetNextActionSchemaAllowedFields() {
+        Collection<String> actual = 
MCPModelFacingPayloadContract.getNextActionSchemaAllowedFields();
+        assertTrue(actual.contains("resource_uri"));
+        assertFalse(actual.contains("target_tool"));
+    }
+    
+    @Test
+    void assertGetModelCriticalFieldNames() {
+        Collection<String> actual = 
MCPModelFacingPayloadContract.getModelCriticalFieldNames();
+        assertTrue(actual.contains(MCPPayloadFieldNames.NEXT_ACTIONS));
+        assertTrue(actual.contains("manual_follow_up"));
+    }
+}
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPConversationTurnPlanner.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPConversationTurnPlanner.java
index ef07ee89fe8..e6a8c80b4d3 100644
--- 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPConversationTurnPlanner.java
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPConversationTurnPlanner.java
@@ -17,20 +17,46 @@
 
 package org.apache.shardingsphere.test.e2e.mcp.llm.conversation;
 
+import org.apache.shardingsphere.mcp.api.tool.descriptor.MCPToolDescriptor;
+import org.apache.shardingsphere.mcp.core.tool.handler.ToolDefinitionRegistry;
 import org.apache.shardingsphere.test.e2e.mcp.llm.scenario.LLME2EScenario;
 import 
org.apache.shardingsphere.test.e2e.mcp.support.transport.MCPInteractionActionNames;
 import 
org.apache.shardingsphere.test.e2e.mcp.support.transport.MCPInteractionTraceRecord;
 
+import java.util.LinkedHashSet;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Set;
 import java.util.stream.Collectors;
 
 final class LLMMCPConversationTurnPlanner {
     
     private final LLMMCPConversationInstructionFactory instructionFactory;
     
+    private final Set<String> readOnlyToolNames;
+    
     LLMMCPConversationTurnPlanner(final LLMMCPConversationInstructionFactory 
instructionFactory) {
+        this(instructionFactory, createReadOnlyToolNames());
+    }
+    
+    LLMMCPConversationTurnPlanner(final LLMMCPConversationInstructionFactory 
instructionFactory, final Set<String> readOnlyToolNames) {
         this.instructionFactory = instructionFactory;
+        this.readOnlyToolNames = readOnlyToolNames;
+    }
+    
+    private static Set<String> createReadOnlyToolNames() {
+        Set<String> result = new LinkedHashSet<>();
+        result.add(MCPInteractionActionNames.LIST_RESOURCES);
+        result.add(MCPInteractionActionNames.READ_RESOURCE);
+        result.add(MCPInteractionActionNames.LIST_PROMPTS);
+        result.add(MCPInteractionActionNames.GET_PROMPT);
+        result.add(MCPInteractionActionNames.COMPLETE);
+        for (MCPToolDescriptor each : 
ToolDefinitionRegistry.getSupportedToolDescriptors()) {
+            if (each.getAnnotations().isReadOnlyHint()) {
+                result.add(each.getName());
+            }
+        }
+        return result;
     }
     
     List<String> createTurnToolNames(final LLME2EScenario scenario, final 
List<MCPInteractionTraceRecord> interactionTrace) {
@@ -39,9 +65,9 @@ final class LLMMCPConversationTurnPlanner {
             return immediateActionToolNames;
         }
         if 
(instructionFactory.hasSideEffectExecutionNextAction(interactionTrace)) {
-            List<String> readOnlyToolNames = 
findMissingReadOnlyToolNames(scenario, interactionTrace);
-            if (!readOnlyToolNames.isEmpty()) {
-                return List.of(readOnlyToolNames.getFirst());
+            List<String> missingReadOnlyToolNames = 
findMissingReadOnlyToolNames(scenario, interactionTrace);
+            if (!missingReadOnlyToolNames.isEmpty()) {
+                return List.of(missingReadOnlyToolNames.getFirst());
             }
         }
         List<String> missingToolNames = findMissingAllowedToolNames(scenario, 
interactionTrace);
@@ -79,12 +105,6 @@ final class LLMMCPConversationTurnPlanner {
     }
     
     private boolean isReadOnlyToolName(final String toolName) {
-        return MCPInteractionActionNames.LIST_RESOURCES.equals(toolName)
-                || MCPInteractionActionNames.READ_RESOURCE.equals(toolName)
-                || MCPInteractionActionNames.LIST_PROMPTS.equals(toolName)
-                || MCPInteractionActionNames.GET_PROMPT.equals(toolName)
-                || MCPInteractionActionNames.COMPLETE.equals(toolName)
-                || "database_gateway_search_metadata".equals(toolName)
-                || "database_gateway_execute_query".equals(toolName);
+        return readOnlyToolNames.contains(toolName);
     }
 }
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPConversationTurnPlannerTest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPConversationTurnPlannerTest.java
index c779c0656ad..374e9e3ab4a 100644
--- 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPConversationTurnPlannerTest.java
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPConversationTurnPlannerTest.java
@@ -25,6 +25,7 @@ import org.junit.jupiter.api.Test;
 
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.is;
@@ -34,7 +35,7 @@ class LLMMCPConversationTurnPlannerTest {
     @Test
     void assertCreateTurnToolNamesWithImmediateResourceAction() {
         LLMMCPConversationInstructionFactory instructionFactory = new 
LLMMCPConversationInstructionFactory();
-        LLMMCPConversationTurnPlanner planner = new 
LLMMCPConversationTurnPlanner(instructionFactory);
+        LLMMCPConversationTurnPlanner planner = new 
LLMMCPConversationTurnPlanner(instructionFactory, Set.of());
         List<String> actual = 
planner.createTurnToolNames(createScenario(List.of(MCPInteractionActionNames.READ_RESOURCE,
 "database_gateway_execute_query"),
                 List.of(MCPInteractionActionNames.READ_RESOURCE, 
"database_gateway_execute_query")),
                 List.of(createTraceRecord("database_gateway_plan_mask_rule",
@@ -45,7 +46,7 @@ class LLMMCPConversationTurnPlannerTest {
     @Test
     void assertCreateTurnToolNamesWithImmediateCompletionAction() {
         LLMMCPConversationInstructionFactory instructionFactory = new 
LLMMCPConversationInstructionFactory();
-        LLMMCPConversationTurnPlanner planner = new 
LLMMCPConversationTurnPlanner(instructionFactory);
+        LLMMCPConversationTurnPlanner planner = new 
LLMMCPConversationTurnPlanner(instructionFactory, Set.of());
         List<String> actual = 
planner.createTurnToolNames(createScenario(List.of("database_gateway_plan_mask_rule",
 "database_gateway_execute_query"),
                 List.of("database_gateway_plan_mask_rule", 
"database_gateway_execute_query")),
                 List.of(createTraceRecord("database_gateway_plan_mask_rule",
@@ -56,7 +57,7 @@ class LLMMCPConversationTurnPlannerTest {
     @Test
     void assertCreateTurnToolNamesPrefersReadOnlyAfterSideEffectNextAction() {
         LLMMCPConversationInstructionFactory instructionFactory = new 
LLMMCPConversationInstructionFactory();
-        LLMMCPConversationTurnPlanner planner = new 
LLMMCPConversationTurnPlanner(instructionFactory);
+        LLMMCPConversationTurnPlanner planner = new 
LLMMCPConversationTurnPlanner(instructionFactory, 
Set.of("database_gateway_execute_query"));
         Map<String, Object> nextAction = Map.of("type", "tool_call", 
"tool_name", "database_gateway_execute_update", "arguments", 
Map.of("execution_mode", "execute"));
         List<String> actual = 
planner.createTurnToolNames(createScenario(List.of("database_gateway_execute_update",
 "database_gateway_execute_query"),
                 List.of("database_gateway_execute_update", 
"database_gateway_execute_query")),
@@ -65,16 +66,27 @@ class LLMMCPConversationTurnPlannerTest {
     }
     
     @Test
-    void assertCreateToolChoiceWithMissingCoverage() {
+    void 
assertCreateTurnToolNamesPrefersDescriptorReadOnlyToolAfterSideEffectNextAction()
 {
         LLMMCPConversationInstructionFactory instructionFactory = new 
LLMMCPConversationInstructionFactory();
         LLMMCPConversationTurnPlanner planner = new 
LLMMCPConversationTurnPlanner(instructionFactory);
+        Map<String, Object> nextAction = Map.of("type", "tool_call", 
"tool_name", "database_gateway_execute_update", "arguments", 
Map.of("execution_mode", "execute"));
+        List<String> actual = 
planner.createTurnToolNames(createScenario(List.of("database_gateway_execute_update",
 "database_gateway_validate_runtime_database"),
+                List.of("database_gateway_execute_update", 
"database_gateway_validate_runtime_database")),
+                List.of(createTraceRecord("database_gateway_execute_update", 
Map.of("next_actions", List.of(nextAction)))));
+        assertThat(actual, 
is(List.of("database_gateway_validate_runtime_database")));
+    }
+    
+    @Test
+    void assertCreateToolChoiceWithMissingCoverage() {
+        LLMMCPConversationInstructionFactory instructionFactory = new 
LLMMCPConversationInstructionFactory();
+        LLMMCPConversationTurnPlanner planner = new 
LLMMCPConversationTurnPlanner(instructionFactory, Set.of());
         
assertThat(planner.createToolChoice(createScenario(List.of("database_gateway_execute_query"),
 List.of("database_gateway_execute_query")), List.of(), false), is("required"));
     }
     
     @Test
     void assertCreateToolChoiceWithCoveredRequiredTools() {
         LLMMCPConversationInstructionFactory instructionFactory = new 
LLMMCPConversationInstructionFactory();
-        LLMMCPConversationTurnPlanner planner = new 
LLMMCPConversationTurnPlanner(instructionFactory);
+        LLMMCPConversationTurnPlanner planner = new 
LLMMCPConversationTurnPlanner(instructionFactory, Set.of());
         
assertThat(planner.createToolChoice(createScenario(List.of("database_gateway_execute_query"),
 List.of("database_gateway_execute_query")),
                 List.of(createTraceRecord("database_gateway_execute_query", 
Map.of("row_objects", List.of()))), false), is("auto"));
     }
@@ -82,7 +94,7 @@ class LLMMCPConversationTurnPlannerTest {
     @Test
     void assertCreateToolChoiceWithFinalAnswer() {
         LLMMCPConversationInstructionFactory instructionFactory = new 
LLMMCPConversationInstructionFactory();
-        LLMMCPConversationTurnPlanner planner = new 
LLMMCPConversationTurnPlanner(instructionFactory);
+        LLMMCPConversationTurnPlanner planner = new 
LLMMCPConversationTurnPlanner(instructionFactory, Set.of());
         
assertThat(planner.createToolChoice(createScenario(List.of("database_gateway_execute_query"),
 List.of("database_gateway_execute_query")), List.of(), true), is("none"));
     }
     
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPModelFacingToolResponseFormatter.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPModelFacingToolResponseFormatter.java
index 285fa340f04..54d7768fba1 100644
--- 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPModelFacingToolResponseFormatter.java
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPModelFacingToolResponseFormatter.java
@@ -20,6 +20,8 @@ package 
org.apache.shardingsphere.test.e2e.mcp.llm.conversation;
 import lombok.AccessLevel;
 import lombok.NoArgsConstructor;
 import org.apache.shardingsphere.infra.util.json.JsonUtils;
+import 
org.apache.shardingsphere.mcp.support.protocol.MCPModelFacingPayloadContract;
+import org.apache.shardingsphere.mcp.support.protocol.MCPPayloadFieldNames;
 
 import java.util.Collection;
 import java.util.LinkedHashMap;
@@ -33,17 +35,17 @@ final class LLMMCPModelFacingToolResponseFormatter {
     
     private static final List<String> GENERAL_FIELD_NAMES = List.of(
             "response_mode", "error_code", "message", "recovery_category", 
"result_kind", "status", "statement_type", "normalized_sql", "rows", 
"row_objects",
-            "returned_row_count", "plan_id", "workflow_resource", 
"manual_artifact_summary", "manual_follow_up");
+            "returned_row_count", "plan_id", "workflow_resource");
     
     private static final List<String> POST_ACTION_FIELD_NAMES = List.of(
-            "completion", "count", "has_more", "total_match_count", 
"returned_count", "truncated", "large_result_guidance", "search_context", 
"ambiguity_state");
+            "completion", "count", "has_more", "total_match_count", 
"returned_count", "truncated", "large_result_guidance", "search_context");
     
     static String format(final Map<String, Object> response) {
         Map<String, Object> result = new LinkedHashMap<>(16, 1F);
         copyFields(response, result, GENERAL_FIELD_NAMES);
         copyCompactArtifactList(response, result, "manual_artifacts");
         copyCompactArtifactList(response, result, "exported_artifacts");
-        copyIfPresent(response, result, "resources_to_read");
+        copyModelCriticalFields(response, result);
         copyModelFacingNextActions(response, result);
         copyFields(response, result, POST_ACTION_FIELD_NAMES);
         List<Map<String, Object>> resources = 
LLMMCPJsonValues.castToList(response.get("resources"));
@@ -78,6 +80,14 @@ final class LLMMCPModelFacingToolResponseFormatter {
         }
     }
     
+    private static void copyModelCriticalFields(final Map<String, Object> 
source, final Map<String, Object> target) {
+        for (String each : 
MCPModelFacingPayloadContract.getModelCriticalFieldNames()) {
+            if (!MCPPayloadFieldNames.NEXT_ACTIONS.equals(each) && 
!MCPPayloadFieldNames.RECOVERY.equals(each)) {
+                copyIfPresent(source, target, each);
+            }
+        }
+    }
+    
     private static void copyCompactItems(final Map<String, Object> source, 
final Map<String, Object> target) {
         List<Map<String, Object>> items = 
LLMMCPJsonValues.castToList(source.get("items"));
         if (items.isEmpty()) {
@@ -164,13 +174,13 @@ final class LLMMCPModelFacingToolResponseFormatter {
         copyIfPresent(recovery, compactRecovery, "plan_id");
         copyIfPresent(recovery, compactRecovery, "completion_first");
         copyIfPresent(recovery, compactRecovery, "suggested_arguments");
-        copyIfPresent(recovery, compactRecovery, "resources_to_read");
+        copyModelCriticalFields(recovery, compactRecovery);
         copyModelFacingNextActions(recovery, compactRecovery);
         target.put("recovery", compactRecovery);
     }
     
     private static void copyModelFacingNextActions(final Map<String, Object> 
source, final Map<String, Object> target) {
-        List<Map<String, Object>> nextActions = 
LLMMCPJsonValues.castToList(source.get("next_actions"));
+        List<Map<String, Object>> nextActions = 
LLMMCPJsonValues.castToList(source.get(MCPPayloadFieldNames.NEXT_ACTIONS));
         if (nextActions.isEmpty()) {
             return;
         }
@@ -181,7 +191,7 @@ final class LLMMCPModelFacingToolResponseFormatter {
             }
         }
         if (!result.isEmpty()) {
-            target.put("next_actions", result);
+            target.put(MCPPayloadFieldNames.NEXT_ACTIONS, result);
         }
     }
 }
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPModelFacingToolResponseFormatterTest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPModelFacingToolResponseFormatterTest.java
index 1da9230f688..ce053954a62 100644
--- 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPModelFacingToolResponseFormatterTest.java
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPModelFacingToolResponseFormatterTest.java
@@ -88,6 +88,31 @@ class LLMMCPModelFacingToolResponseFormatterTest {
                 "exported_artifacts", List.of(Map.of("ddl_artifact_count", 
1)))));
     }
     
+    @Test
+    void assertFormatWithModelCriticalFields() {
+        Map<String, Object> actual = format(Map.of(
+                "resources_to_read", List.of(Map.of("uri", 
"shardingsphere://capabilities")),
+                "resource", Map.of("uri", "shardingsphere://databases"),
+                "parent_resource", Map.of("uri", "shardingsphere://databases"),
+                "next_resources", List.of(Map.of("uri", 
"shardingsphere://databases/logic_db/schemas")),
+                "manual_artifact_summary", "Review DistSQL.",
+                "manual_follow_up", "Validate runtime state.",
+                "empty_state", Map.of("state", "no_match"),
+                "recovery_guidance", "Read metadata before retrying.",
+                "remediation", "Fix the mismatch.",
+                "ignored", "value"));
+        assertThat(actual, is(Map.of(
+                "resources_to_read", List.of(Map.of("uri", 
"shardingsphere://capabilities")),
+                "resource", Map.of("uri", "shardingsphere://databases"),
+                "parent_resource", Map.of("uri", "shardingsphere://databases"),
+                "next_resources", List.of(Map.of("uri", 
"shardingsphere://databases/logic_db/schemas")),
+                "manual_artifact_summary", "Review DistSQL.",
+                "manual_follow_up", "Validate runtime state.",
+                "empty_state", Map.of("state", "no_match"),
+                "recovery_guidance", "Read metadata before retrying.",
+                "remediation", "Fix the mismatch.")));
+    }
+    
     @Test
     void assertFormatWithRecoveryAndNextActions() {
         Map<String, Object> actual = format(Map.of(
@@ -102,6 +127,9 @@ class LLMMCPModelFacingToolResponseFormatterTest {
                         "next_actions", List.of(
                                 Map.of("type", "tool_call", "tool_name", 
"database_gateway_execute_update", "arguments", Map.of("execution_mode", 
"execute")),
                                 Map.of("type", "resource_read", 
"resource_uri", "shardingsphere://databases")),
+                        "resources_to_read", List.of(Map.of("uri", 
"shardingsphere://databases")),
+                        "recovery_guidance", "Read metadata.",
+                        "remediation", "Fix the request.",
                         "ignored", "value")));
         assertThat(actual, is(Map.of(
                 "next_actions", List.of(
@@ -110,6 +138,9 @@ class LLMMCPModelFacingToolResponseFormatterTest {
                         "recovery_category", "missing_context",
                         "model_action", "retry",
                         "suggested_arguments", Map.of("database", "logic_db"),
+                        "resources_to_read", List.of(Map.of("uri", 
"shardingsphere://databases")),
+                        "recovery_guidance", "Read metadata.",
+                        "remediation", "Fix the request.",
                         "next_actions", List.of(Map.of("type", 
"resource_read", "resource_uri", "shardingsphere://databases"))))));
     }
     
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/assertion/MCPModelContractAssertions.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/assertion/MCPModelContractAssertions.java
index 010b7875ede..bc596ff5c00 100644
--- 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/assertion/MCPModelContractAssertions.java
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/assertion/MCPModelContractAssertions.java
@@ -17,9 +17,11 @@
 
 package org.apache.shardingsphere.test.e2e.mcp.support.assertion;
 
+import 
org.apache.shardingsphere.mcp.support.protocol.MCPModelFacingPayloadContract;
+import org.apache.shardingsphere.mcp.support.protocol.MCPPayloadFieldNames;
+
 import java.util.Collection;
 import java.util.Map;
-import java.util.Set;
 
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -29,24 +31,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
  */
 public final class MCPModelContractAssertions {
     
-    private static final Set<String> REMOVED_MODEL_FACING_FIELDS = Set.of(
-            "target_tool", "target_resource", "required_arguments", 
"action_kind", "suggested_next_tool", "suggested_next_tools", 
"recommended_next_tool",
-            "recommended_recovery", "suggested_next_action", 
"approved_by_user", "requires_user_approval", "approval_required", 
"user_overrides");
-    
-    private static final Map<String, Set<String>> NEXT_ACTION_REQUIRED_FIELDS 
= Map.of(
-            "resource_read", Set.of("order", "type", "title", "resource_uri"),
-            "tool_call", Set.of("order", "type", "title", "tool_name", 
"arguments"),
-            "completion", Set.of("order", "type", "title", "ref", "argument"),
-            "ask_user", Set.of("order", "type", "title", "question"),
-            "terminal", Set.of("order", "type", "title"));
-    
-    private static final Map<String, Set<String>> NEXT_ACTION_ALLOWED_FIELDS = 
Map.of(
-            "resource_read", Set.of("order", "type", "title", "resource_uri", 
"reason", "depends_on"),
-            "tool_call", Set.of("order", "type", "title", "tool_name", 
"arguments", "reason", "depends_on"),
-            "completion", Set.of("order", "type", "title", "ref", "argument", 
"context", "missing_context_arguments", "resume_ref", "resume_arguments", 
"reason", "depends_on"),
-            "ask_user", Set.of("order", "type", "title", "question", 
"required_inputs", "reason", "depends_on"),
-            "terminal", Set.of("order", "type", "title", "reason", 
"depends_on"));
-    
     private MCPModelContractAssertions() {
     }
     
@@ -67,8 +51,8 @@ public final class MCPModelContractAssertions {
     
     private static void assertCanonicalNextActionListMap(final Map<?, ?> 
value) {
         assertNoRemovedModelFacingFields(value);
-        if (value.containsKey("next_actions") && 
!isNextActionsSchema(value.get("next_actions"))) {
-            assertNextActions(value.get("next_actions"));
+        if (value.containsKey(MCPPayloadFieldNames.NEXT_ACTIONS) && 
!isNextActionsSchema(value.get(MCPPayloadFieldNames.NEXT_ACTIONS))) {
+            assertNextActions(value.get(MCPPayloadFieldNames.NEXT_ACTIONS));
         }
         for (Object each : value.values()) {
             assertCanonicalNextActionLists(each);
@@ -77,7 +61,7 @@ public final class MCPModelContractAssertions {
     
     private static void assertNoRemovedModelFacingFields(final Map<?, ?> 
value) {
         for (Object each : value.keySet()) {
-            
assertFalse(REMOVED_MODEL_FACING_FIELDS.contains(String.valueOf(each)), () -> 
"Removed model-facing field returned: " + each);
+            
assertFalse(MCPModelFacingPayloadContract.isRemovedFieldName(String.valueOf(each)),
 () -> "Removed model-facing field returned: " + each);
         }
     }
     
@@ -95,12 +79,14 @@ public final class MCPModelContractAssertions {
     
     private static void assertNextAction(final Map<?, ?> action) {
         String type = String.valueOf(action.get("type"));
-        assertTrue(NEXT_ACTION_REQUIRED_FIELDS.containsKey(type), () -> 
"Unknown next_actions type: " + type);
-        for (String each : NEXT_ACTION_REQUIRED_FIELDS.get(type)) {
+        Collection<String> requiredFields = 
MCPModelFacingPayloadContract.getNextActionRequiredFields(type);
+        assertFalse(requiredFields.isEmpty(), () -> "Unknown next_actions 
type: " + type);
+        for (String each : requiredFields) {
             assertTrue(action.containsKey(each), () -> 
String.format("next_actions `%s` must contain `%s`.", type, each));
         }
         for (Object each : action.keySet()) {
-            
assertTrue(NEXT_ACTION_ALLOWED_FIELDS.get(type).contains(String.valueOf(each)), 
() -> String.format("next_actions `%s` contains unsupported field `%s`.", type, 
each));
+            
assertTrue(MCPModelFacingPayloadContract.getNextActionAllowedFields(type).contains(String.valueOf(each)),
+                    () -> String.format("next_actions `%s` contains 
unsupported field `%s`.", type, each));
         }
     }
 }

Reply via email to