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