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 17410a4ef77 Fix MCP Rule DistSQL recovery guidance (#39055)
17410a4ef77 is described below
commit 17410a4ef772221472dcab062e676227d158c4c0
Author: Liang Zhang <[email protected]>
AuthorDate: Wed Jul 8 19:26:25 2026 +0800
Fix MCP Rule DistSQL recovery guidance (#39055)
* Fix MCP Rule DistSQL recovery guidance
Classify Rule DistSQL as rule-metadata side effects, surface workflow-first
preview guidance, and convert Rule DistSQL syntax failures into structured
recovery actions that point models to guidance and database capabilities
before
asking users to rewrite SQL.
* Fix MCP Rule DistSQL recovery guidance
Classify Rule DistSQL as rule-metadata side effects, surface workflow-first
preview guidance, and convert Rule DistSQL syntax failures into structured
recovery actions that point models to guidance and database capabilities
before
asking users to rewrite SQL.
---
.../error/MCPQueryRecoveryPayloadFactory.java | 4 ++
.../protocol/error/MCPRecoveryPayloadFactory.java | 2 +
.../protocol/error/MCPRecoveryPayloadSupport.java | 3 ++
.../error/MCPSQLRecoveryPayloadFactory.java | 46 +++++++++++++++++++++
.../tool/handler/execute/ClassificationResult.java | 47 ++++++++++++++++++++++
.../handler/execute/ExecuteUpdateToolHandler.java | 17 ++++----
.../handler/execute/MCPJdbcStatementExecutor.java | 4 ++
.../execute/RuleDistSQLExecutionException.java | 42 +++++++++++++++++++
.../mcp-descriptors/mcp-descriptor-core.yaml | 5 ++-
.../mcp/core/protocol/MCPErrorConverterTest.java | 23 +++++++++++
.../capability/ServerCapabilitiesHandlerTest.java | 11 ++++-
.../execute/ExecuteUpdateToolHandlerTest.java | 20 +++++++++
.../execute/MCPJdbcStatementExecutorTest.java | 23 +++++++++++
.../execute/RuleDistSQLExecutionExceptionTest.java | 42 +++++++++++++++++++
.../handler/execute/StatementClassifierTest.java | 17 ++++++++
.../mcp-descriptors/mcp-descriptor-broadcast.yaml | 4 +-
.../mcp-descriptors/mcp-descriptor-encrypt.yaml | 4 +-
.../prompts/plan-encrypt-rule.md | 1 +
.../encrypt/EncryptDescriptorContractTest.java | 11 +++++
.../mcp-descriptors/mcp-descriptor-mask.yaml | 4 +-
.../shardingsphere-mcp/prompts/plan-mask-rule.md | 1 +
.../feature/mask/MaskFeatureDefinitionTest.java | 22 ++++++++++
.../mcp-descriptor-readwrite-splitting.yaml | 8 ++--
.../mcp-descriptors/mcp-descriptor-shadow.yaml | 8 ++--
.../mcp-descriptors/mcp-descriptor-sharding.yaml | 26 ++++++------
.../prompts/plan-sharding-table-rule.md | 1 +
.../sharding/ShardingDescriptorContractTest.java | 16 ++++++++
.../descriptor/MCPGuidancePayloadBuilder.java | 13 +++++-
.../descriptor/MCPGuidancePayloadBuilderTest.java | 14 ++++++-
.../scenario/LLMUsabilityScenarioCatalog.java | 8 ++--
.../scenario/LLMUsabilityScenarioCatalogTest.java | 2 +
.../baseline-contract/model-contract/guidance.yaml | 16 +++++++-
32 files changed, 418 insertions(+), 47 deletions(-)
diff --git
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPQueryRecoveryPayloadFactory.java
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPQueryRecoveryPayloadFactory.java
index cc5e7c24d27..001e4151937 100644
---
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPQueryRecoveryPayloadFactory.java
+++
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPQueryRecoveryPayloadFactory.java
@@ -23,6 +23,7 @@ import
org.apache.shardingsphere.mcp.api.protocol.exception.MCPInvalidRequestExc
import
org.apache.shardingsphere.mcp.api.protocol.exception.MCPQueryFailedException;
import
org.apache.shardingsphere.mcp.api.protocol.exception.MCPTimeoutException;
import
org.apache.shardingsphere.mcp.api.protocol.exception.MCPUnsupportedException;
+import
org.apache.shardingsphere.mcp.core.tool.handler.execute.RuleDistSQLExecutionException;
import org.apache.shardingsphere.mcp.support.diagnostic.MCPDiagnosticCategory;
import org.apache.shardingsphere.mcp.support.protocol.MCPNextActionUtils;
import org.apache.shardingsphere.mcp.support.protocol.MCPPayloadFieldNames;
@@ -44,6 +45,9 @@ import java.util.Optional;
final class MCPQueryRecoveryPayloadFactory {
static boolean isQueryFailure(final Throwable cause) {
+ if (cause instanceof RuleDistSQLExecutionException) {
+ return false;
+ }
if (cause instanceof MCPQueryFailedException || cause instanceof
SQLException) {
return true;
}
diff --git
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPRecoveryPayloadFactory.java
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPRecoveryPayloadFactory.java
index d7ee80899a7..84b03f12fbf 100644
---
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPRecoveryPayloadFactory.java
+++
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPRecoveryPayloadFactory.java
@@ -34,6 +34,7 @@ import
org.apache.shardingsphere.mcp.core.protocol.exception.MCPWorkflowStateExc
import
org.apache.shardingsphere.mcp.core.protocol.exception.UnsupportedResourceUriException;
import
org.apache.shardingsphere.mcp.core.protocol.exception.UnsupportedToolException;
import
org.apache.shardingsphere.mcp.core.tool.handler.execute.MetadataIntrospectionSQLStatementException;
+import
org.apache.shardingsphere.mcp.core.tool.handler.execute.RuleDistSQLExecutionException;
import
org.apache.shardingsphere.mcp.core.tool.handler.execute.SQLToolMismatchException;
import
org.apache.shardingsphere.mcp.support.database.metadata.jdbc.RuntimeDatabaseConnectionException;
@@ -49,6 +50,7 @@ final class MCPRecoveryPayloadFactory {
private static final List<RecoveryMapping> RECOVERY_MAPPINGS = List.of(
new RecoveryMapping(SQLToolMismatchException.class, cause ->
MCPSQLRecoveryPayloadFactory.createSQLToolMismatchRecovery((SQLToolMismatchException)
cause)),
+ new RecoveryMapping(RuleDistSQLExecutionException.class, cause ->
MCPSQLRecoveryPayloadFactory.createRuleDistSQLExecutionRecovery((RuleDistSQLExecutionException)
cause)),
new
RecoveryMapping(MetadataIntrospectionSQLStatementException.class,
cause ->
MCPSQLRecoveryPayloadFactory.createMetadataIntrospectionSQLRecovery((MetadataIntrospectionSQLStatementException)
cause)),
new RecoveryMapping(MCPMultipleSQLStatementsException.class, cause
-> MCPSQLRecoveryPayloadFactory.createMultipleStatementsRecovery()),
diff --git
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPRecoveryPayloadSupport.java
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPRecoveryPayloadSupport.java
index daa138c5186..2219d32e720 100644
---
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPRecoveryPayloadSupport.java
+++
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPRecoveryPayloadSupport.java
@@ -68,6 +68,9 @@ final class MCPRecoveryPayloadSupport {
if ("invalid_enum_value".equals(category)) {
return "invalid_enum";
}
+ if ("rule_distsql_execution_failed".equals(category)) {
+ return "rule_metadata_execution";
+ }
if ("unsafe_sql_attempted".equals(category) ||
"multiple_sql_statements".equals(category)) {
return "unsafe_sql";
}
diff --git
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPSQLRecoveryPayloadFactory.java
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPSQLRecoveryPayloadFactory.java
index 956a7c65adc..12031d21ec5 100644
---
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPSQLRecoveryPayloadFactory.java
+++
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPSQLRecoveryPayloadFactory.java
@@ -19,11 +19,14 @@ package org.apache.shardingsphere.mcp.core.protocol.error;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
+import
org.apache.shardingsphere.mcp.core.tool.handler.execute.ClassificationResult;
import
org.apache.shardingsphere.mcp.core.tool.handler.execute.MetadataIntrospectionSQLStatementException;
+import
org.apache.shardingsphere.mcp.core.tool.handler.execute.RuleDistSQLExecutionException;
import
org.apache.shardingsphere.mcp.core.tool.handler.execute.SQLToolMismatchException;
import org.apache.shardingsphere.mcp.support.protocol.MCPNextActionUtils;
import org.apache.shardingsphere.mcp.support.protocol.MCPPayloadFieldNames;
import org.apache.shardingsphere.mcp.support.protocol.MCPResourceHintUtils;
+import org.apache.shardingsphere.mcp.support.resource.MCPUriPathSegmentUtils;
import java.util.List;
import java.util.Locale;
@@ -52,6 +55,49 @@ final class MCPSQLRecoveryPayloadFactory {
return result;
}
+ static Map<String, Object> createRuleDistSQLExecutionRecovery(final
RuleDistSQLExecutionException cause) {
+ Map<String, Object> result =
MCPRecoveryPayloadSupport.createBaseRecovery("rule_distsql_execution_failed",
+ "Do not ask the user to rewrite the SQL yet; read workflow
guidance and verify the runtime database can execute ShardingSphere rule
DistSQL.");
+ ClassificationResult classificationResult =
cause.getClassificationResult();
+ result.put("database", cause.getDatabase());
+ result.put("statement_class",
classificationResult.getStatementClass().name().toLowerCase(Locale.ENGLISH));
+ result.put("statement_type", classificationResult.getStatementType());
+ result.put("side_effect_scope",
List.of(classificationResult.getSideEffectScope()));
+ result.put("secret_safe", true);
+ result.put(MCPPayloadFieldNames.RESOURCES_TO_READ,
createRuleDistSQLExecutionResources(cause.getDatabase()));
+ result.put(MCPPayloadFieldNames.NEXT_ACTIONS,
createRuleDistSQLExecutionNextActions(cause.getDatabase()));
+ result.put("ask_user_when_uncertain", false);
+ return result;
+ }
+
+ private static List<Map<String, Object>>
createRuleDistSQLExecutionResources(final String database) {
+ return List.of(
+ MCPResourceHintUtils.create("shardingsphere://guidance",
"guidance", "read_first",
+ "Read workflow guidance before retrying rule DistSQL
execution.", MCPPayloadFieldNames.RESOURCES_TO_READ),
+ createDatabaseCapabilityResourceHint(database));
+ }
+
+ private static Map<String, Object>
createDatabaseCapabilityResourceHint(final String database) {
+ return
MCPResourceHintUtils.create(createDatabaseCapabilityUri(database),
database.isEmpty() ? "logical-database" : "logical-database-capability",
"read_first",
+ createDatabaseCapabilityReason(database),
MCPPayloadFieldNames.RESOURCES_TO_READ);
+ }
+
+ private static String createDatabaseCapabilityUri(final String database) {
+ return database.isEmpty() ? "shardingsphere://databases" :
String.format("shardingsphere://databases/%s/capabilities",
MCPUriPathSegmentUtils.encodePathSegment(database));
+ }
+
+ private static String createDatabaseCapabilityReason(final String
database) {
+ return database.isEmpty()
+ ? "Choose a configured logical database before retrying rule
DistSQL execution."
+ : "Verify the runtime database capabilities before retrying
rule DistSQL execution.";
+ }
+
+ private static List<Map<String, Object>>
createRuleDistSQLExecutionNextActions(final String database) {
+ return MCPNextActionUtils.ordered(
+ MCPNextActionUtils.readResource("shardingsphere://guidance",
"Read workflow guidance and choose the matching database_gateway_plan_*
workflow tool for rule changes."),
+
MCPNextActionUtils.dependsOn(MCPNextActionUtils.readResource(createDatabaseCapabilityUri(database),
createDatabaseCapabilityReason(database)), 1));
+ }
+
static Map<String, Object> createMetadataIntrospectionSQLRecovery(final
MetadataIntrospectionSQLStatementException cause) {
Map<String, Object> result =
MCPRecoveryPayloadSupport.createBaseRecovery(
"metadata_introspection_sql", "Use logical metadata resources
or database_gateway_search_metadata instead of console-style metadata SQL.");
diff --git
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ClassificationResult.java
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ClassificationResult.java
index 3239a0c952f..f0b64a30350 100644
---
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ClassificationResult.java
+++
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ClassificationResult.java
@@ -22,6 +22,7 @@ import
org.apache.shardingsphere.mcp.support.database.capability.SupportedMCPSta
import java.util.Collection;
import java.util.List;
+import java.util.Locale;
import java.util.Optional;
/**
@@ -30,6 +31,16 @@ import java.util.Optional;
@Getter
public final class ClassificationResult {
+ private static final Collection<String> RULE_DIST_SQL_PREFIXES = List.of(
+ "CREATE SHARDING ", "ALTER SHARDING ", "DROP SHARDING ",
+ "CREATE DEFAULT SHARDING ", "ALTER DEFAULT SHARDING ", "DROP
DEFAULT SHARDING ",
+ "CREATE BROADCAST ", "DROP BROADCAST ",
+ "CREATE ENCRYPT ", "ALTER ENCRYPT ", "DROP ENCRYPT ",
+ "CREATE MASK ", "ALTER MASK ", "DROP MASK ",
+ "CREATE SHADOW ", "ALTER SHADOW ", "DROP SHADOW ",
+ "CREATE DEFAULT SHADOW ", "ALTER DEFAULT SHADOW ", "DROP DEFAULT
SHADOW ",
+ "CREATE READWRITE_SPLITTING ", "ALTER READWRITE_SPLITTING ", "DROP
READWRITE_SPLITTING ");
+
private final SupportedMCPStatement statementClass;
private final String statementType;
@@ -92,6 +103,42 @@ public final class ClassificationResult {
return savepointName.isEmpty() ? Optional.empty() :
Optional.of(savepointName);
}
+ /**
+ * Determine whether this statement mutates ShardingSphere rule metadata
through DistSQL.
+ *
+ * @return true when the statement is a recognized rule DistSQL statement
+ */
+ public boolean isRuleDistSQL() {
+ if (SupportedMCPStatement.DDL != statementClass) {
+ return false;
+ }
+ String upperSql = normalizedSql.toUpperCase(Locale.ENGLISH);
+ for (String each : RULE_DIST_SQL_PREFIXES) {
+ if (upperSql.startsWith(each)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Get model-facing side-effect scope.
+ *
+ * @return side-effect scope
+ */
+ public String getSideEffectScope() {
+ if (isRuleDistSQL()) {
+ return "rule-metadata";
+ }
+ return switch (analyzedStatementClass.orElse(statementClass)) {
+ case DML -> "physical-data";
+ case DDL -> "physical-structure";
+ case DCL -> "privilege-metadata";
+ case TRANSACTION_CONTROL, SAVEPOINT -> "transaction-state";
+ default -> "unknown-side-effect";
+ };
+ }
+
String getTraceStatementMarker() {
return SupportedMCPStatement.TRANSACTION_CONTROL == statementClass ||
SupportedMCPStatement.SAVEPOINT == statementClass ? statementType :
statementClass.name();
}
diff --git
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteUpdateToolHandler.java
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteUpdateToolHandler.java
index eacaa424c14..7880cc97569 100644
---
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteUpdateToolHandler.java
+++
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteUpdateToolHandler.java
@@ -54,6 +54,9 @@ public final class ExecuteUpdateToolHandler implements
MCPToolHandler<MCPDatabas
private static final String PREVIEW_REVIEW_GUIDANCE = "Review
normalized_sql and side_effect_scope before execution. "
+ "This preview is classification-only; it does not guarantee
parsing, rule validation, algorithm initialization, affected rows, or runtime
success.";
+ private static final String RULE_DIST_SQL_PREVIEW_REVIEW_GUIDANCE =
PREVIEW_REVIEW_GUIDANCE
+ + " For natural-language rule changes, prefer the matching
database_gateway_plan_* workflow tool before raw execution.";
+
private static final String PREVIEW_CONFIRMATION_REASON = "Confirm that
normalized_sql and side_effect_scope still match the intended side effect
before execution.";
private static final String PREVIEW_EXECUTION_REASON = "Execute only after
reviewing normalized_sql and side_effect_scope; preview did not validate
runtime executability.";
@@ -118,7 +121,7 @@ public final class ExecuteUpdateToolHandler implements
MCPToolHandler<MCPDatabas
result.put("side_effect_scope",
createSideEffectScope(classificationResult));
classificationResult.getTargetObjectName().ifPresent(optional ->
result.put("target_object", optional));
classificationResult.getSavepointName().ifPresent(optional ->
result.put("savepoint", optional));
- result.put("review_guidance", PREVIEW_REVIEW_GUIDANCE);
+ result.put("review_guidance",
createReviewGuidance(classificationResult));
String reviewSummary = createReviewSummary(classificationResult);
result.put(MCPPayloadFieldNames.SUMMARY, reviewSummary);
result.put("review_summary", reviewSummary);
@@ -141,14 +144,12 @@ public final class ExecuteUpdateToolHandler implements
MCPToolHandler<MCPDatabas
String.join(", ",
createSideEffectScope(classificationResult)));
}
+ private String createReviewGuidance(final ClassificationResult
classificationResult) {
+ return classificationResult.isRuleDistSQL() ?
RULE_DIST_SQL_PREVIEW_REVIEW_GUIDANCE : PREVIEW_REVIEW_GUIDANCE;
+ }
+
private List<String> createSideEffectScope(final ClassificationResult
classificationResult) {
- return switch
(classificationResult.getAnalyzedStatementClass().orElse(classificationResult.getStatementClass()))
{
- case DML -> List.of("physical-data");
- case DDL -> List.of("physical-structure");
- case DCL -> List.of("privilege-metadata");
- case TRANSACTION_CONTROL, SAVEPOINT ->
List.of("transaction-state");
- default -> List.of("unknown-side-effect");
- };
+ return List.of(classificationResult.getSideEffectScope());
}
private Map<String, Object> createSuggestedArguments(final
MCPToolArguments toolArguments, final ClassificationResult
classificationResult) {
diff --git
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/MCPJdbcStatementExecutor.java
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/MCPJdbcStatementExecutor.java
index 8c31c94b484..3c45261e16b 100644
---
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/MCPJdbcStatementExecutor.java
+++
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/MCPJdbcStatementExecutor.java
@@ -68,6 +68,7 @@ public final class MCPJdbcStatementExecutor {
* @throws MCPTimeoutException when the JDBC execution times out
* @throws MCPUnsupportedException when the JDBC driver or statement class
is unsupported
* @throws MCPInvalidRequestException when the SQL is invalid for the
target database
+ * @throws RuleDistSQLExecutionException when rule DistSQL needs
workflow-aware recovery
* @throws MCPQueryFailedException when query execution fails
* @throws MCPUnavailableException when the runtime database configuration
is unavailable
*/
@@ -94,6 +95,9 @@ public final class MCPJdbcStatementExecutor {
} catch (final SQLFeatureNotSupportedException ex) {
throw new MCPUnsupportedException(ex.getMessage(), ex);
} catch (final SQLSyntaxErrorException ex) {
+ if (classificationResult.isRuleDistSQL()) {
+ throw new
RuleDistSQLExecutionException(executionRequest.getDatabase(),
classificationResult, ex);
+ }
throw new MCPInvalidRequestException(ex.getMessage(), ex);
} catch (final SQLException ex) {
throw new MCPQueryFailedException(ex.getMessage(), ex);
diff --git
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/RuleDistSQLExecutionException.java
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/RuleDistSQLExecutionException.java
new file mode 100644
index 00000000000..dc2976637da
--- /dev/null
+++
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/RuleDistSQLExecutionException.java
@@ -0,0 +1,42 @@
+/*
+ * 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.core.tool.handler.execute;
+
+import lombok.Getter;
+import
org.apache.shardingsphere.mcp.api.protocol.exception.MCPInvalidRequestException;
+
+import java.sql.SQLSyntaxErrorException;
+
+/**
+ * Exception for rule DistSQL execution failures that need workflow-aware
recovery.
+ */
+@Getter
+public final class RuleDistSQLExecutionException extends
MCPInvalidRequestException {
+
+ private static final long serialVersionUID = -5256057044313402728L;
+
+ private final String database;
+
+ private final ClassificationResult classificationResult;
+
+ public RuleDistSQLExecutionException(final String database, final
ClassificationResult classificationResult, final SQLSyntaxErrorException cause)
{
+ super(String.format("Rule DistSQL execution failed for database `%s`;
check MCP runtime capability and workflow guidance before asking for corrected
SQL.", database), cause);
+ this.database = database;
+ this.classificationResult = classificationResult;
+ }
+}
diff --git
a/mcp/core/src/main/resources/META-INF/shardingsphere-mcp/mcp-descriptors/mcp-descriptor-core.yaml
b/mcp/core/src/main/resources/META-INF/shardingsphere-mcp/mcp-descriptors/mcp-descriptor-core.yaml
index f22ce5313df..b8e575fe86d 100644
---
a/mcp/core/src/main/resources/META-INF/shardingsphere-mcp/mcp-descriptors/mcp-descriptor-core.yaml
+++
b/mcp/core/src/main/resources/META-INF/shardingsphere-mcp/mcp-descriptors/mcp-descriptor-core.yaml
@@ -1387,7 +1387,7 @@ tools:
description: >-
Execute or preview exactly one supported SQL statement that may mutate
data, metadata, rules, or transaction state.
Use execution_mode=preview first when the side-effect scope has not been
reviewed. Preview is classification-only,
- not a database dry run.
+ not a database dry run. For natural-language rule changes, prefer the
matching database_gateway_plan_* workflow tool.
inputSchema:
type: object
properties:
@@ -1505,7 +1505,7 @@ tools:
description: "False for preview because preview does not estimate
affected row counts."
side_effect_scope:
type: array
- description: "Preview-only side-effect categories such as
physical-data, physical-structure, privilege-metadata, or transaction-state."
+ description: "Preview-only side-effect categories such as
physical-data, physical-structure, privilege-metadata, rule-metadata, or
transaction-state."
target_object:
type: string
description: "Preview-only target object name when the classifier
can identify one."
@@ -1611,6 +1611,7 @@ tools:
- physical-data
- physical-structure
- privilege-metadata
+ - rule-metadata
- transaction-state
meta:
org.apache.shardingsphere/related-resource-uris:
diff --git
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/protocol/MCPErrorConverterTest.java
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/protocol/MCPErrorConverterTest.java
index 2d9f5525615..83564cbb27a 100644
---
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/protocol/MCPErrorConverterTest.java
+++
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/protocol/MCPErrorConverterTest.java
@@ -41,6 +41,7 @@ import
org.apache.shardingsphere.mcp.core.protocol.error.MCPErrorConverter;
import org.apache.shardingsphere.mcp.core.protocol.response.MCPErrorResponse;
import
org.apache.shardingsphere.mcp.core.tool.handler.execute.ClassificationResult;
import
org.apache.shardingsphere.mcp.core.tool.handler.execute.MetadataIntrospectionSQLStatementException;
+import
org.apache.shardingsphere.mcp.core.tool.handler.execute.RuleDistSQLExecutionException;
import
org.apache.shardingsphere.mcp.core.tool.handler.execute.SQLToolMismatchException;
import
org.apache.shardingsphere.mcp.support.database.capability.SupportedMCPStatement;
import
org.apache.shardingsphere.mcp.support.database.exception.DatabaseCapabilityNotFoundException;
@@ -298,6 +299,28 @@ class MCPErrorConverterTest {
assertThat(actualRecovery.get("suggested_arguments"),
is(suggestedArguments));
}
+ @Test
+ void assertConvertRuleDistSQLExecutionWithRecovery() {
+ ClassificationResult classificationResult = new
ClassificationResult(SupportedMCPStatement.DDL, "CREATE",
+ "CREATE SHARDING TABLE RULE
t_order(DATANODES('ds_${0..1}.t_order_${0..1}'))", "", "");
+ Map<String, Object> actual = MCPErrorConverter.convert(new
RuleDistSQLExecutionException(
+ "sharding_db", classificationResult, new
SQLSyntaxErrorException("syntax error"))).toPayload();
+ assertThat(actual.get("message"),
+ is("Rule DistSQL execution failed for database `sharding_db`;
check MCP runtime capability and workflow guidance before asking for corrected
SQL."));
+ Map<?, ?> actualRecovery = (Map<?, ?>) actual.get("recovery");
+ assertThat(actualRecovery.get("category"),
is("rule_distsql_execution_failed"));
+ assertThat(actualRecovery.get("recovery_category"),
is("rule_metadata_execution"));
+ assertThat(actualRecovery.get("database"), is("sharding_db"));
+ assertThat(actualRecovery.get("side_effect_scope"),
is(List.of("rule-metadata")));
+ assertTrue((Boolean) actualRecovery.get("secret_safe"));
+ assertFalse((Boolean) actualRecovery.get("ask_user_when_uncertain"));
+ assertThat(getResourceToRead(actualRecovery, 1).get("uri"),
is("shardingsphere://databases/sharding_db/capabilities"));
+ List<?> actualNextActions = (List<?>)
actualRecovery.get("next_actions");
+ assertThat(((Map<?, ?>)
actualNextActions.getFirst()).get("resource_uri"),
is("shardingsphere://guidance"));
+ assertThat(((Map<?, ?>) actualNextActions.get(1)).get("type"),
is("resource_read"));
+ assertFalse(String.valueOf(actual).contains("syntax error"));
+ }
+
@Test
void assertConvertMetadataIntrospectionSQLWithRecovery() {
Map<String, Object> actual = MCPErrorConverter.convert(new
MetadataIntrospectionSQLStatementException("SHOW")).toPayload();
diff --git
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/capability/ServerCapabilitiesHandlerTest.java
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/capability/ServerCapabilitiesHandlerTest.java
index 48d40c8bd39..6844bcce2f9 100644
---
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/capability/ServerCapabilitiesHandlerTest.java
+++
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/capability/ServerCapabilitiesHandlerTest.java
@@ -124,6 +124,8 @@ class ServerCapabilitiesHandlerTest {
assertFalse(actual.containsKey("safe_first_resource"));
Map<?, ?> firstRoute = findByKey((List<?>)
actual.get("first_call_routes"), "intent", "inspect_metadata");
assertThat(firstRoute.get("first_action"), is("read_resource
shardingsphere://databases"));
+ Map<?, ?> ruleWorkflowRoute = findByKey((List<?>)
actual.get("first_call_routes"), "intent", "rule_workflow");
+ assertThat(ruleWorkflowRoute.get("first_action"), is("call_tool
matching database_gateway_plan_* workflow tool without plan_id for a new
plan"));
Map<?, ?> recoveryRoute = findByKey((List<?>)
actual.get("first_call_routes"), "intent", "recover_error");
assertThat(recoveryRoute.get("first_action"), is("follow top-level
next_actions"));
Map<?, ?> metadataRule = (Map<?, ?>) actual.get("metadata_rule");
@@ -133,8 +135,14 @@ class ServerCapabilitiesHandlerTest {
Map<?, ?> sqlToolSelection = (Map<?, ?>)
actual.get("sql_tool_selection");
assertThat(((Map<?, ?>)
sqlToolSelection.get("read_only")).get("tool"),
is("database_gateway_execute_query"));
assertThat(((Map<?, ?>)
sqlToolSelection.get("side_effecting")).get("first_mode"), is("preview"));
+ assertThat(((Map<?, ?>)
sqlToolSelection.get("side_effecting")).get("rule_change_preference"),
+ is("For natural-language rule changes, use the matching
database_gateway_plan_* workflow tool before raw SQL execution; "
+ + "omit plan_id for a new plan."));
assertTrue(String.valueOf(actual.get("side_effect_rule")).contains("requested
side effect is still intended"));
Map<?, ?> workflowRule = (Map<?, ?>) actual.get("workflow_rule");
+ assertThat(workflowRule.get("selection_rule"),
+ is("For natural-language rule changes, use the matching
database_gateway_plan_* workflow tool before raw side-effect SQL; "
+ + "omit plan_id for a new plan and reuse only returned
plan_id values."));
assertTrue(workflowRule.containsKey("planning_tools"));
assertThat(((Map<?, ?>) workflowRule.get("preview_tool")).get("tool"),
is("database_gateway_apply_workflow"));
assertThat(((Map<?, ?>)
workflowRule.get("execute_tool")).get("execute_requires"),
@@ -153,7 +161,8 @@ class ServerCapabilitiesHandlerTest {
assertFalse(actual.containsKey("safe_first_resource"));
assertThat(actual.get("metadata_first_resource"),
is("shardingsphere://databases"));
assertTrue(String.valueOf(actual.get("preflight_rule")).contains("database_gateway_validate_runtime_database"));
- assertTrue(((Map<?, ?>)
actual.get("sql_tool_selection")).containsKey("side_effecting"));
+ Map<?, ?> sqlToolSelection = (Map<?, ?>)
actual.get("sql_tool_selection");
+ assertTrue(sqlToolSelection.containsKey("side_effecting"));
assertTrue(actual.containsKey("workflow_session_rule"));
assertTrue(actual.containsKey("next_action_rule"));
assertTrue(actual.containsKey("recovery_rule"));
diff --git
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteUpdateToolHandlerTest.java
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteUpdateToolHandlerTest.java
index 64abba2e0b5..9de02cb18d3 100644
---
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteUpdateToolHandlerTest.java
+++
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteUpdateToolHandlerTest.java
@@ -141,6 +141,26 @@ class ExecuteUpdateToolHandlerTest {
verifyNoInteractions(executionFacade);
}
+ @Test
+ void assertPreviewRuleDistSQLStatementWithoutExecuting() {
+ MCPFeatureExecutionFacade executionFacade =
mock(MCPFeatureExecutionFacade.class);
+ MCPDatabaseHandlerContext databaseContext =
mock(MCPDatabaseHandlerContext.class);
+ when(databaseContext.getExecutionFacade()).thenReturn(executionFacade);
+ MCPResponse actual = new
ExecuteUpdateToolHandler().handle(databaseContext, new MCPToolCall("session-1",
+ Map.of("database", "sharding_db", "sql",
+ "CREATE SHARDING TABLE RULE
t_order(DATANODES('ds_${0..1}.t_order_${0..1}'),
KEY_GENERATE_STRATEGY(COLUMN=id, TYPE(NAME='snowflake')))",
+ "execution_mode", "preview")));
+ assertThat(actual.toPayload().get("response_mode"), is("preview"));
+ assertThat(actual.toPayload().get("statement_class"), is("ddl"));
+ assertThat(actual.toPayload().get("side_effect_scope"),
is(List.of("rule-metadata")));
+ assertThat(actual.toPayload().get("summary"), is("Previewed CREATE
statement with side-effect scope rule-metadata. It has not been executed."));
+ assertThat(actual.toPayload().get("review_guidance"),
+ is("Review normalized_sql and side_effect_scope before
execution. "
+ + "This preview is classification-only; it does not
guarantee parsing, rule validation, algorithm initialization, affected rows, or
runtime success."
+ + " For natural-language rule changes, prefer the
matching database_gateway_plan_* workflow tool before raw execution."));
+ verifyNoInteractions(executionFacade);
+ }
+
@Test
void assertRejectPreviewWithInvalidTimeout() {
MCPFeatureExecutionFacade executionFacade =
mock(MCPFeatureExecutionFacade.class);
diff --git
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/MCPJdbcStatementExecutorTest.java
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/MCPJdbcStatementExecutorTest.java
index 135f6522c1c..c037d0740a3 100644
---
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/MCPJdbcStatementExecutorTest.java
+++
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/MCPJdbcStatementExecutorTest.java
@@ -278,6 +278,29 @@ class MCPJdbcStatementExecutorTest {
assertThat(actual.getMessage(), is("Statement class is not
supported."));
}
+ @Test
+ void assertExecuteRuleDistSQLSyntaxError() throws SQLException {
+ SQLSyntaxErrorException cause = new SQLSyntaxErrorException("syntax
error");
+ Statement statement = mock(Statement.class);
+ when(statement.execute(anyString())).thenThrow(cause);
+ MCPJdbcTransactionResourceManager transactionResourceManager =
mock(MCPJdbcTransactionResourceManager.class);
+ when(transactionResourceManager.findTransactionConnection(anyString(),
anyString())).thenReturn(Optional.empty());
+ Connection connection = createStatementConnection(statement);
+ RuntimeDatabaseConfiguration databaseConfig =
mock(RuntimeDatabaseConfiguration.class);
+
when(databaseConfig.openConnection(anyString())).thenReturn(connection);
+ MCPJdbcStatementExecutor statementExecutor = new
MCPJdbcStatementExecutor(Map.of("sharding_db", databaseConfig),
transactionResourceManager);
+ ClassificationResult classificationResult = new
ClassificationResult(SupportedMCPStatement.DDL, "CREATE",
+ "CREATE SHARDING TABLE RULE
t_order(DATANODES('ds_${0..1}.t_order_${0..1}'))", "", "");
+ RuleDistSQLExecutionException actual =
assertThrows(RuleDistSQLExecutionException.class, () ->
statementExecutor.execute(new SQLExecutionRequest("session-1",
+ "sharding_db", "public", "CREATE SHARDING TABLE RULE
t_order(DATANODES('ds_${0..1}.t_order_${0..1}'))", 10, 1000),
+ classificationResult,
createDatabaseCapability(SchemaExecutionSemantics.FIXED_TO_DATABASE)));
+ assertThat(actual.getDatabase(), is("sharding_db"));
+ assertThat(actual.getClassificationResult(), is(classificationResult));
+ assertThat(actual.getCause(), is(cause));
+ assertThat(actual.getMessage(),
+ is("Rule DistSQL execution failed for database `sharding_db`;
check MCP runtime capability and workflow guidance before asking for corrected
SQL."));
+ }
+
@ParameterizedTest(name = "{0}")
@MethodSource("assertExecuteWithSQLExceptionCases")
void assertExecuteWithSQLException(final String name, final SQLException
sqlException, final Class<? extends ShardingSphereMCPException>
expectedExceptionClass,
diff --git
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/RuleDistSQLExecutionExceptionTest.java
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/RuleDistSQLExecutionExceptionTest.java
new file mode 100644
index 00000000000..e3970ac37d4
--- /dev/null
+++
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/RuleDistSQLExecutionExceptionTest.java
@@ -0,0 +1,42 @@
+/*
+ * 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.core.tool.handler.execute;
+
+import
org.apache.shardingsphere.mcp.support.database.capability.SupportedMCPStatement;
+import org.junit.jupiter.api.Test;
+
+import java.sql.SQLSyntaxErrorException;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+class RuleDistSQLExecutionExceptionTest {
+
+ @Test
+ void assertRuleDistSQLExecutionException() {
+ ClassificationResult classificationResult = new
ClassificationResult(SupportedMCPStatement.DDL, "CREATE",
+ "CREATE SHARDING TABLE RULE
t_order(DATANODES('ds_${0..1}.t_order_${0..1}'))", "", "");
+ SQLSyntaxErrorException cause = new SQLSyntaxErrorException("syntax
error");
+ RuleDistSQLExecutionException actual = new
RuleDistSQLExecutionException("sharding_db", classificationResult, cause);
+ assertThat(actual.getMessage(),
+ is("Rule DistSQL execution failed for database `sharding_db`;
check MCP runtime capability and workflow guidance before asking for corrected
SQL."));
+ assertThat(actual.getDatabase(), is("sharding_db"));
+ assertThat(actual.getClassificationResult(), is(classificationResult));
+ assertThat(actual.getCause(), is(cause));
+ }
+}
diff --git
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/StatementClassifierTest.java
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/StatementClassifierTest.java
index 94364730b76..5c204c6a292 100644
---
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/StatementClassifierTest.java
+++
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/StatementClassifierTest.java
@@ -32,7 +32,9 @@ import java.util.stream.Stream;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
class StatementClassifierTest {
@@ -85,6 +87,21 @@ class StatementClassifierTest {
assertThat(actualResult.getReferencedObjectNames(),
contains("logic_db.foo_orders", "other_db.foo_order_items"));
}
+ @Test
+ void assertClassifyRuleDistSQLSideEffectScope() {
+ ClassificationResult actualResult = statementClassifier.classify(
+ "CREATE SHARDING TABLE RULE
t_order(DATANODES('ds_${0..1}.t_order_${0..1}'),
KEY_GENERATE_STRATEGY(COLUMN=id, TYPE(NAME='snowflake')))");
+ assertTrue(actualResult.isRuleDistSQL());
+ assertThat(actualResult.getSideEffectScope(), is("rule-metadata"));
+ }
+
+ @Test
+ void assertClassifyPhysicalDDLSideEffectScope() {
+ ClassificationResult actualResult =
statementClassifier.classify("CREATE TABLE foo_orders(order_id BIGINT)");
+ assertFalse(actualResult.isRuleDistSQL());
+ assertThat(actualResult.getSideEffectScope(),
is("physical-structure"));
+ }
+
@Test
void assertClassifySubqueryReferencedObjectNames() {
ClassificationResult actualResult = statementClassifier.classify(
diff --git
a/mcp/features/broadcast/src/main/resources/META-INF/shardingsphere-mcp/mcp-descriptors/mcp-descriptor-broadcast.yaml
b/mcp/features/broadcast/src/main/resources/META-INF/shardingsphere-mcp/mcp-descriptors/mcp-descriptor-broadcast.yaml
index 9134e875ab5..04d2bbf8f64 100644
---
a/mcp/features/broadcast/src/main/resources/META-INF/shardingsphere-mcp/mcp-descriptors/mcp-descriptor-broadcast.yaml
+++
b/mcp/features/broadcast/src/main/resources/META-INF/shardingsphere-mcp/mcp-descriptors/mcp-descriptor-broadcast.yaml
@@ -96,7 +96,7 @@ prompts:
required: false
- name: plan_id
title: Workflow Plan ID
- description: "Optional current-session workflow plan identifier used
to continue a broadcast plan."
+ description: "Existing current-session workflow plan identifier used
only to continue a broadcast plan. Omit when starting a new plan; never use
placeholder values such as `plan_id`."
required: false
meta:
org.apache.shardingsphere/related-tools:
@@ -172,7 +172,7 @@ tools:
properties:
plan_id:
type: string
- description: "Optional existing workflow plan identifier used to
continue or revise a previous broadcast plan."
+ description: "Existing workflow plan identifier used only to
continue or revise a previous broadcast plan. Omit when starting a new plan;
never use placeholder values such as `plan_id`."
database:
type: string
description: "ShardingSphere logical database that owns the
broadcast rule."
diff --git
a/mcp/features/encrypt/src/main/resources/META-INF/shardingsphere-mcp/mcp-descriptors/mcp-descriptor-encrypt.yaml
b/mcp/features/encrypt/src/main/resources/META-INF/shardingsphere-mcp/mcp-descriptors/mcp-descriptor-encrypt.yaml
index 76462bfd7f4..2b0b4ac2551 100644
---
a/mcp/features/encrypt/src/main/resources/META-INF/shardingsphere-mcp/mcp-descriptors/mcp-descriptor-encrypt.yaml
+++
b/mcp/features/encrypt/src/main/resources/META-INF/shardingsphere-mcp/mcp-descriptors/mcp-descriptor-encrypt.yaml
@@ -106,7 +106,7 @@ prompts:
required: false
- name: plan_id
title: Workflow Plan ID
- description: "Optional current-session workflow plan identifier used
to continue an encrypt plan."
+ description: "Existing current-session workflow plan identifier used
only to continue an encrypt plan. Omit when starting a new plan; never use
placeholder values such as `plan_id`."
required: false
meta:
org.apache.shardingsphere/related-tools:
@@ -185,7 +185,7 @@ tools:
properties:
plan_id:
type: string
- description: "Optional existing workflow plan identifier used to
continue or revise a previous encrypt plan."
+ description: "Existing workflow plan identifier used only to
continue or revise a previous encrypt plan. Omit when starting a new plan;
never use placeholder values such as `plan_id`."
database:
type: string
description: "ShardingSphere logical database that owns the target
table."
diff --git
a/mcp/features/encrypt/src/main/resources/META-INF/shardingsphere-mcp/prompts/plan-encrypt-rule.md
b/mcp/features/encrypt/src/main/resources/META-INF/shardingsphere-mcp/prompts/plan-encrypt-rule.md
index c0858f25e08..5961553ffd8 100644
---
a/mcp/features/encrypt/src/main/resources/META-INF/shardingsphere-mcp/prompts/plan-encrypt-rule.md
+++
b/mcp/features/encrypt/src/main/resources/META-INF/shardingsphere-mcp/prompts/plan-encrypt-rule.md
@@ -39,6 +39,7 @@ Model path:
2. Read shardingsphere://features/encrypt/algorithms before choosing
algorithm_type.
3. Read existing encrypt rules for the database or table when database and
table are known.
4. Call database_gateway_plan_encrypt_rule with gathered logical names,
explicit rule column names, and reviewed algorithm choices.
+ Omit plan_id for a new plan; pass plan_id only when continuing an actual
current-session plan returned by a previous planning response.
5. Use database_gateway_apply_workflow with execution_mode=preview before
applying generated encrypt rule DistSQL.
Call review-then-execute only after the user confirms explicit
approved_steps from preview_artifacts.
6. Before choosing uncertain database, schema, table, column, algorithm, or
plan_id values, ask the user or read feature algorithm/rule resources.
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
index f9c7c72fc3c..1627bb9ec3e 100644
---
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
@@ -21,6 +21,7 @@ import
org.apache.shardingsphere.mcp.api.tool.descriptor.MCPToolDescriptor;
import
org.apache.shardingsphere.mcp.support.descriptor.MCPCompletionTargetDescriptor;
import org.apache.shardingsphere.mcp.support.descriptor.MCPDescriptorCatalog;
import
org.apache.shardingsphere.mcp.support.descriptor.MCPDescriptorCatalogLoader;
+import
org.apache.shardingsphere.mcp.support.descriptor.MCPToolDescriptorValidationUtils;
import org.junit.jupiter.api.Test;
import java.util.Collection;
@@ -53,6 +54,16 @@ class EncryptDescriptorContractTest {
"like_query_algorithm_type", "plan_id");
}
+ @Test
+ void assertPlanIdInputGuidesNewPlanOmission() {
+ MCPToolDescriptor actualDescriptor = findToolDescriptor();
+ Map<?, ?> actualPlanIdInput =
MCPToolDescriptorValidationUtils.findToolInputProperty(actualDescriptor,
"plan_id").orElseThrow();
+ String actualDescription =
actualPlanIdInput.get("description").toString();
+
assertFalse(MCPToolDescriptorValidationUtils.isRequiredToolInput(actualDescriptor,
"plan_id"));
+ assertTrue(actualDescription.contains("Omit when starting a new
plan"));
+ assertTrue(actualDescription.contains("never use placeholder values"));
+ }
+
private MCPToolDescriptor findToolDescriptor() {
MCPDescriptorCatalog catalog = MCPDescriptorCatalogLoader.load();
return catalog.getProtocolDescriptors().getToolDescriptors().stream()
diff --git
a/mcp/features/mask/src/main/resources/META-INF/shardingsphere-mcp/mcp-descriptors/mcp-descriptor-mask.yaml
b/mcp/features/mask/src/main/resources/META-INF/shardingsphere-mcp/mcp-descriptors/mcp-descriptor-mask.yaml
index dcf2ac10904..9765ccf6aee 100644
---
a/mcp/features/mask/src/main/resources/META-INF/shardingsphere-mcp/mcp-descriptors/mcp-descriptor-mask.yaml
+++
b/mcp/features/mask/src/main/resources/META-INF/shardingsphere-mcp/mcp-descriptors/mcp-descriptor-mask.yaml
@@ -98,7 +98,7 @@ prompts:
required: false
- name: plan_id
title: Workflow Plan ID
- description: "Optional current-session workflow plan identifier used
to continue a mask plan."
+ description: "Existing current-session workflow plan identifier used
only to continue a mask plan. Omit when starting a new plan; never use
placeholder values such as `plan_id`."
required: false
meta:
org.apache.shardingsphere/related-tools:
@@ -175,7 +175,7 @@ tools:
properties:
plan_id:
type: string
- description: "Optional existing workflow plan identifier used to
continue or revise a previous mask plan."
+ description: "Existing workflow plan identifier used only to
continue or revise a previous mask plan. Omit when starting a new plan; never
use placeholder values such as `plan_id`."
database:
type: string
description: "ShardingSphere logical database that owns the target
table."
diff --git
a/mcp/features/mask/src/main/resources/META-INF/shardingsphere-mcp/prompts/plan-mask-rule.md
b/mcp/features/mask/src/main/resources/META-INF/shardingsphere-mcp/prompts/plan-mask-rule.md
index b5c7976b440..f3fcbcad581 100644
---
a/mcp/features/mask/src/main/resources/META-INF/shardingsphere-mcp/prompts/plan-mask-rule.md
+++
b/mcp/features/mask/src/main/resources/META-INF/shardingsphere-mcp/prompts/plan-mask-rule.md
@@ -31,6 +31,7 @@ Model path:
2. Read shardingsphere://features/mask/algorithms before choosing
algorithm_type.
3. Read existing mask rules for the database or table when database and table
are known.
4. Call database_gateway_plan_mask_rule with gathered logical names and any
reviewed algorithm choice.
+ Omit plan_id for a new plan; pass plan_id only when continuing an actual
current-session plan returned by a previous planning response.
5. Use database_gateway_apply_workflow with execution_mode=preview before
applying generated mask rule DistSQL.
Call review-then-execute only after the user confirms explicit
approved_steps from preview_artifacts.
6. Before choosing uncertain database, schema, table, column, algorithm, or
plan_id values, ask the user or read feature algorithm/rule resources.
diff --git
a/mcp/features/mask/src/test/java/org/apache/shardingsphere/mcp/feature/mask/MaskFeatureDefinitionTest.java
b/mcp/features/mask/src/test/java/org/apache/shardingsphere/mcp/feature/mask/MaskFeatureDefinitionTest.java
index 1698de9b40e..16b135f611c 100644
---
a/mcp/features/mask/src/test/java/org/apache/shardingsphere/mcp/feature/mask/MaskFeatureDefinitionTest.java
+++
b/mcp/features/mask/src/test/java/org/apache/shardingsphere/mcp/feature/mask/MaskFeatureDefinitionTest.java
@@ -17,14 +17,20 @@
package org.apache.shardingsphere.mcp.feature.mask;
+import org.apache.shardingsphere.mcp.api.tool.descriptor.MCPToolDescriptor;
import
org.apache.shardingsphere.mcp.support.descriptor.MCPCompletionTargetDescriptor;
+import org.apache.shardingsphere.mcp.support.descriptor.MCPDescriptorCatalog;
import
org.apache.shardingsphere.mcp.support.descriptor.MCPDescriptorCatalogLoader;
+import
org.apache.shardingsphere.mcp.support.descriptor.MCPToolDescriptorValidationUtils;
import org.junit.jupiter.api.Test;
import java.util.List;
+import java.util.Map;
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 MaskFeatureDefinitionTest {
@@ -44,4 +50,20 @@ class MaskFeatureDefinitionTest {
.filter(each -> "prompt".equals(each.getReferenceType()) &&
MaskFeatureDefinition.PLAN_PROMPT_NAME.equals(each.getReference())).findFirst().orElseThrow();
assertThat(actual.getArguments(), is(List.of("database", "schema",
"table", "column", "algorithm_type", "plan_id")));
}
+
+ @Test
+ void assertPlanIdInputGuidesNewPlanOmission() {
+ MCPToolDescriptor actualDescriptor = findToolDescriptor();
+ Map<?, ?> actualPlanIdInput =
MCPToolDescriptorValidationUtils.findToolInputProperty(actualDescriptor,
"plan_id").orElseThrow();
+ String actualDescription =
actualPlanIdInput.get("description").toString();
+
assertFalse(MCPToolDescriptorValidationUtils.isRequiredToolInput(actualDescriptor,
"plan_id"));
+ assertTrue(actualDescription.contains("Omit when starting a new
plan"));
+ assertTrue(actualDescription.contains("never use placeholder values"));
+ }
+
+ private MCPToolDescriptor findToolDescriptor() {
+ MCPDescriptorCatalog catalog = MCPDescriptorCatalogLoader.load();
+ return catalog.getProtocolDescriptors().getToolDescriptors().stream()
+ .filter(each ->
MaskFeatureDefinition.PLAN_TOOL_NAME.equals(each.getName())).findFirst().orElseThrow();
+ }
}
diff --git
a/mcp/features/readwrite-splitting/src/main/resources/META-INF/shardingsphere-mcp/mcp-descriptors/mcp-descriptor-readwrite-splitting.yaml
b/mcp/features/readwrite-splitting/src/main/resources/META-INF/shardingsphere-mcp/mcp-descriptors/mcp-descriptor-readwrite-splitting.yaml
index 8cb059e6478..50767535a38 100644
---
a/mcp/features/readwrite-splitting/src/main/resources/META-INF/shardingsphere-mcp/mcp-descriptors/mcp-descriptor-readwrite-splitting.yaml
+++
b/mcp/features/readwrite-splitting/src/main/resources/META-INF/shardingsphere-mcp/mcp-descriptors/mcp-descriptor-readwrite-splitting.yaml
@@ -160,7 +160,7 @@ prompts:
required: false
- name: plan_id
title: Workflow Plan ID
- description: "Optional current-session workflow plan identifier used
to continue a readwrite-splitting rule plan."
+ description: "Existing current-session workflow plan identifier used
only to continue a readwrite-splitting rule plan. Omit when starting a new
plan; never use placeholder values such as `plan_id`."
required: false
meta:
org.apache.shardingsphere/related-tools:
@@ -200,7 +200,7 @@ prompts:
required: false
- name: plan_id
title: Workflow Plan ID
- description: "Optional current-session workflow plan identifier used
to continue a readwrite-splitting status plan."
+ description: "Existing current-session workflow plan identifier used
only to continue a readwrite-splitting status plan. Omit when starting a new
plan; never use placeholder values such as `plan_id`."
required: false
meta:
org.apache.shardingsphere/related-tools:
@@ -310,7 +310,7 @@ tools:
properties:
plan_id:
type: string
- description: "Optional existing workflow plan identifier used to
continue or revise a previous readwrite-splitting plan."
+ description: "Existing workflow plan identifier used only to
continue or revise a previous readwrite-splitting plan. Omit when starting a
new plan; never use placeholder values such as `plan_id`."
database:
type: string
description: "ShardingSphere logical database that owns the
readwrite-splitting rule."
@@ -565,7 +565,7 @@ tools:
properties:
plan_id:
type: string
- description: "Optional existing workflow plan identifier used to
continue or revise a previous readwrite-splitting status plan."
+ description: "Existing workflow plan identifier used only to
continue or revise a previous readwrite-splitting status plan. Omit when
starting a new plan; never use placeholder values such as `plan_id`."
database:
type: string
description: "ShardingSphere logical database that owns the
readwrite-splitting rule."
diff --git
a/mcp/features/shadow/src/main/resources/META-INF/shardingsphere-mcp/mcp-descriptors/mcp-descriptor-shadow.yaml
b/mcp/features/shadow/src/main/resources/META-INF/shardingsphere-mcp/mcp-descriptors/mcp-descriptor-shadow.yaml
index 5afbda9a43a..a995150fb99 100644
---
a/mcp/features/shadow/src/main/resources/META-INF/shardingsphere-mcp/mcp-descriptors/mcp-descriptor-shadow.yaml
+++
b/mcp/features/shadow/src/main/resources/META-INF/shardingsphere-mcp/mcp-descriptors/mcp-descriptor-shadow.yaml
@@ -199,7 +199,7 @@ prompts:
required: false
- name: plan_id
title: Workflow Plan ID
- description: "Optional current-session workflow plan identifier used
to continue a shadow rule plan."
+ description: "Existing current-session workflow plan identifier used
only to continue a shadow rule plan. Omit when starting a new plan; never use
placeholder values such as `plan_id`."
required: false
meta:
org.apache.shardingsphere/related-tools:
@@ -236,7 +236,7 @@ prompts:
required: false
- name: plan_id
title: Workflow Plan ID
- description: "Optional current-session workflow plan identifier used
to continue a default shadow algorithm plan."
+ description: "Existing current-session workflow plan identifier used
only to continue a default shadow algorithm plan. Omit when starting a new
plan; never use placeholder values such as `plan_id`."
required: false
meta:
org.apache.shardingsphere/related-tools:
@@ -268,7 +268,7 @@ prompts:
required: false
- name: plan_id
title: Workflow Plan ID
- description: "Optional current-session workflow plan identifier used
to continue a shadow algorithm cleanup plan."
+ description: "Existing current-session workflow plan identifier used
only to continue a shadow algorithm cleanup plan. Omit when starting a new
plan; never use placeholder values such as `plan_id`."
required: false
meta:
org.apache.shardingsphere/related-tools:
@@ -403,7 +403,7 @@ tools:
properties:
plan_id:
type: string
- description: "Optional existing workflow plan identifier used to
continue or revise a previous shadow plan."
+ description: "Existing workflow plan identifier used only to
continue or revise a previous shadow plan. Omit when starting a new plan; never
use placeholder values such as `plan_id`."
database:
type: string
description: "ShardingSphere logical database that owns the shadow
rule."
diff --git
a/mcp/features/sharding/src/main/resources/META-INF/shardingsphere-mcp/mcp-descriptors/mcp-descriptor-sharding.yaml
b/mcp/features/sharding/src/main/resources/META-INF/shardingsphere-mcp/mcp-descriptors/mcp-descriptor-sharding.yaml
index b6ba5ecd493..87be57e3a0b 100644
---
a/mcp/features/sharding/src/main/resources/META-INF/shardingsphere-mcp/mcp-descriptors/mcp-descriptor-sharding.yaml
+++
b/mcp/features/sharding/src/main/resources/META-INF/shardingsphere-mcp/mcp-descriptors/mcp-descriptor-sharding.yaml
@@ -319,7 +319,7 @@ resourceTemplates:
prompts:
- name: plan_sharding_table_rule
title: Plan Sharding Table Rule
- description: "Guide the model to plan sharding table rule DistSQL."
+ description: "Guide the model to plan sharding table rule DistSQL,
including database/table sharding and key generation."
binding:
templateResource:
META-INF/shardingsphere-mcp/prompts/plan-sharding-table-rule.md
arguments:
@@ -337,7 +337,7 @@ prompts:
required: false
- name: plan_id
title: Workflow Plan ID
- description: "Optional current-session workflow plan identifier used
to continue a sharding table rule plan."
+ description: "Existing current-session workflow plan identifier used
only to continue a sharding table rule plan. Omit when starting a new plan;
never use placeholder values such as `plan_id`."
required: false
meta:
org.apache.shardingsphere/related-tools:
@@ -372,7 +372,7 @@ prompts:
required: false
- name: plan_id
title: Workflow Plan ID
- description: "Optional current-session workflow plan identifier used
to continue this sharding plan."
+ description: "Existing current-session workflow plan identifier used
only to continue this sharding plan. Omit when starting a new plan; never use
placeholder values such as `plan_id`."
required: false
meta:
org.apache.shardingsphere/related-tools:
@@ -408,7 +408,7 @@ prompts:
required: false
- name: plan_id
title: Workflow Plan ID
- description: "Optional current-session workflow plan identifier used
to continue a default sharding strategy plan."
+ description: "Existing current-session workflow plan identifier used
only to continue a default sharding strategy plan. Omit when starting a new
plan; never use placeholder values such as `plan_id`."
required: false
meta:
org.apache.shardingsphere/related-tools:
@@ -445,7 +445,7 @@ prompts:
required: false
- name: plan_id
title: Workflow Plan ID
- description: "Optional current-session workflow plan identifier used
to continue a sharding key generator plan."
+ description: "Existing current-session workflow plan identifier used
only to continue a sharding key generator plan. Omit when starting a new plan;
never use placeholder values such as `plan_id`."
required: false
meta:
org.apache.shardingsphere/related-tools:
@@ -482,7 +482,7 @@ prompts:
required: false
- name: plan_id
title: Workflow Plan ID
- description: "Optional current-session workflow plan identifier used
to continue a sharding key generate strategy plan."
+ description: "Existing current-session workflow plan identifier used
only to continue a sharding key generate strategy plan. Omit when starting a
new plan; never use placeholder values such as `plan_id`."
required: false
meta:
org.apache.shardingsphere/related-tools:
@@ -778,11 +778,11 @@ resourceNavigation:
tools:
- name: database_gateway_plan_sharding_table_rule
title: Plan Sharding Table Rule
- description: "Plan reviewable sharding table rule DistSQL without
executing it."
+ description: "Plan reviewable sharding table rule DistSQL without
executing it. Use this before raw SQL for natural-language requests to create,
alter, or drop database/table sharding rules."
inputSchema:
type: object
properties:
- plan_id: {type: string, description: "Optional workflow plan id."}
+ plan_id: {type: string, description: "Existing workflow plan id for
continuing a previous plan. Omit for a new plan; never use placeholder values
such as `plan_id`."}
database: {type: string, description: "Logical database."}
table: {type: string, description: "Logical table."}
column: {type: string, description: "Standard or auto-table sharding
column."}
@@ -893,7 +893,7 @@ tools:
inputSchema:
type: object
properties:
- plan_id: {type: string, description: "Optional workflow plan id."}
+ plan_id: {type: string, description: "Existing workflow plan id for
continuing a previous plan. Omit for a new plan; never use placeholder values
such as `plan_id`."}
database: {type: string, description: "Logical database."}
rule: {type: string, description: "Table reference rule name."}
operation_type: {type: string, description: "create, alter, or drop.",
enum: [create, alter, drop]}
@@ -930,7 +930,7 @@ tools:
inputSchema:
type: object
properties:
- plan_id: {type: string, description: "Optional workflow plan id."}
+ plan_id: {type: string, description: "Existing workflow plan id for
continuing a previous plan. Omit for a new plan; never use placeholder values
such as `plan_id`."}
database: {type: string, description: "Logical database."}
operation_type: {type: string, description: "create, alter, or drop.",
enum: [create, alter, drop]}
default_strategy_type: {type: string, description: "DATABASE or TABLE
default strategy type.", enum: [DATABASE, TABLE]}
@@ -972,7 +972,7 @@ tools:
inputSchema:
type: object
properties:
- plan_id: {type: string, description: "Optional workflow plan id."}
+ plan_id: {type: string, description: "Existing workflow plan id for
continuing a previous plan. Omit for a new plan; never use placeholder values
such as `plan_id`."}
database: {type: string, description: "Logical database."}
key_generator: {type: string, description: "Key generator name."}
operation_type: {type: string, description: "create, alter, or drop.",
enum: [create, alter, drop]}
@@ -1011,7 +1011,7 @@ tools:
inputSchema:
type: object
properties:
- plan_id: {type: string, description: "Optional workflow plan id."}
+ plan_id: {type: string, description: "Existing workflow plan id for
continuing a previous plan. Omit for a new plan; never use placeholder values
such as `plan_id`."}
database: {type: string, description: "Logical database."}
key_generate_strategy: {type: string, description: "Key generate
strategy name."}
operation_type: {type: string, description: "create, alter, or drop.",
enum: [create, alter, drop]}
@@ -1055,7 +1055,7 @@ tools:
inputSchema:
type: object
properties:
- plan_id: {type: string, description: "Optional workflow plan id."}
+ plan_id: {type: string, description: "Existing workflow plan id for
continuing a previous plan. Omit for a new plan; never use placeholder values
such as `plan_id`."}
database: {type: string, description: "Logical database."}
operation_type: {type: string, description: "Cleanup is drop-only.",
enum: [drop]}
component_type: {type: string, description: "algorithm, key-generator,
or auditor.", enum: [algorithm, key-generator, auditor]}
diff --git
a/mcp/features/sharding/src/main/resources/META-INF/shardingsphere-mcp/prompts/plan-sharding-table-rule.md
b/mcp/features/sharding/src/main/resources/META-INF/shardingsphere-mcp/prompts/plan-sharding-table-rule.md
index 407e877b89c..b9bb4b231d7 100644
---
a/mcp/features/sharding/src/main/resources/META-INF/shardingsphere-mcp/prompts/plan-sharding-table-rule.md
+++
b/mcp/features/sharding/src/main/resources/META-INF/shardingsphere-mcp/prompts/plan-sharding-table-rule.md
@@ -7,6 +7,7 @@ Inputs:
- plan_id: {{plan_id}}
Plan only ShardingSphere sharding table rule DistSQL.
+Use this workflow before raw side-effect SQL when the user asks in natural
language to create, alter, or drop database/table sharding rules, including
generated key rules such as Snowflake.
Before planning:
- Read existing table rules, algorithm plugins, and key-generate algorithm
plugins for the logical database.
diff --git
a/mcp/features/sharding/src/test/java/org/apache/shardingsphere/mcp/feature/sharding/ShardingDescriptorContractTest.java
b/mcp/features/sharding/src/test/java/org/apache/shardingsphere/mcp/feature/sharding/ShardingDescriptorContractTest.java
index ae882e5d3d2..805fc1588e6 100644
---
a/mcp/features/sharding/src/test/java/org/apache/shardingsphere/mcp/feature/sharding/ShardingDescriptorContractTest.java
+++
b/mcp/features/sharding/src/test/java/org/apache/shardingsphere/mcp/feature/sharding/ShardingDescriptorContractTest.java
@@ -17,6 +17,7 @@
package org.apache.shardingsphere.mcp.feature.sharding;
+import org.apache.shardingsphere.mcp.api.prompt.descriptor.MCPPromptDescriptor;
import org.apache.shardingsphere.mcp.api.tool.descriptor.MCPToolDescriptor;
import
org.apache.shardingsphere.mcp.support.descriptor.MCPCompletionTargetDescriptor;
import org.apache.shardingsphere.mcp.support.descriptor.MCPDescriptorCatalog;
@@ -57,6 +58,21 @@ class ShardingDescriptorContractTest {
assertCompletionTargetArguments(catalog,
ShardingFeatureDefinition.PLAN_COMPONENT_CLEANUP_PROMPT_NAME, "database",
"plan_id");
}
+ @Test
+ void assertPlanTableRuleToolGuidesNaturalLanguageRuleChanges() {
+ MCPDescriptorCatalog catalog = MCPDescriptorCatalogLoader.load();
+ MCPPromptDescriptor actualPrompt = findPrompt(catalog,
ShardingFeatureDefinition.PLAN_TABLE_RULE_PROMPT_NAME);
+ assertThat(actualPrompt.getDescription(), is("Guide the model to plan
sharding table rule DistSQL, including database/table sharding and key
generation."));
+ MCPToolDescriptor actual = findTool(catalog,
ShardingFeatureDefinition.PLAN_TABLE_RULE_TOOL_NAME);
+ assertThat(actual.getDescription(),
+ is("Plan reviewable sharding table rule DistSQL without
executing it. "
+ + "Use this before raw SQL for natural-language
requests to create, alter, or drop database/table sharding rules."));
+ }
+
+ private MCPPromptDescriptor findPrompt(final MCPDescriptorCatalog catalog,
final String promptName) {
+ return
catalog.getProtocolDescriptors().getPromptDescriptors().stream().filter(each ->
promptName.equals(each.getName())).findFirst().orElseThrow();
+ }
+
private MCPToolDescriptor findTool(final MCPDescriptorCatalog catalog,
final String toolName) {
return
catalog.getProtocolDescriptors().getToolDescriptors().stream().filter(each ->
toolName.equals(each.getName())).findFirst().orElseThrow();
}
diff --git
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPGuidancePayloadBuilder.java
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPGuidancePayloadBuilder.java
index 1261b405d28..36a541bd326 100644
---
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPGuidancePayloadBuilder.java
+++
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPGuidancePayloadBuilder.java
@@ -90,6 +90,9 @@ final class MCPGuidancePayloadBuilder {
"Follow top-level next_actions when validation
fails."),
createFirstCallRoute("read_only_sql", "read_resource
shardingsphere://databases/{database}/capabilities", "call_tool
database_gateway_execute_query",
"Stop after reporting the result rows."),
+ createFirstCallRoute("rule_workflow", "call_tool matching
database_gateway_plan_* workflow tool without plan_id for a new plan",
+ "call_tool database_gateway_apply_workflow
execution_mode=preview with the returned plan_id",
+ "Use workflow apply and validation for
natural-language rule changes before considering raw side-effect SQL."),
createFirstCallRoute("side_effect_sql", "call_tool
database_gateway_execute_update execution_mode=preview", "call_tool
database_gateway_execute_update execution_mode=execute",
"Execute only after preview review confirms the
intended side effect."),
createFirstCallRoute("complete_uncertain_argument", "call
completion/complete for one uncertain argument",
@@ -249,16 +252,22 @@ final class MCPGuidancePayloadBuilder {
readOnly.put("tool", "database_gateway_execute_query");
readOnly.put("statement_rule", "Use for one SELECT or EXPLAIN ANALYZE
statement.");
result.put("read_only", readOnly);
- Map<String, Object> sideEffecting = new LinkedHashMap<>(3, 1F);
+ Map<String, Object> sideEffecting = new LinkedHashMap<>(4, 1F);
sideEffecting.put("tool", "database_gateway_execute_update");
sideEffecting.put("first_mode", "preview");
sideEffecting.put("execute_requires", "execution_mode=execute");
+ sideEffecting.put("rule_change_preference",
+ "For natural-language rule changes, use the matching
database_gateway_plan_* workflow tool before raw SQL execution; "
+ + "omit plan_id for a new plan.");
result.put("side_effecting", sideEffecting);
return result;
}
private Map<String, Object> createWorkflowRule() {
- Map<String, Object> result = new LinkedHashMap<>(4, 1F);
+ Map<String, Object> result = new LinkedHashMap<>(5, 1F);
+ result.put("selection_rule",
+ "For natural-language rule changes, use the matching
database_gateway_plan_* workflow tool before raw side-effect SQL; "
+ + "omit plan_id for a new plan and reuse only returned
plan_id values.");
result.put("planning_tools",
catalog.getProtocolDescriptors().getToolDescriptors().stream()
.map(MCPToolDescriptor::getName).filter(each ->
each.startsWith(PLANNING_TOOL_NAME_PREFIX)).toList());
Map<String, Object> previewTool = new LinkedHashMap<>(2, 1F);
diff --git
a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/descriptor/MCPGuidancePayloadBuilderTest.java
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/descriptor/MCPGuidancePayloadBuilderTest.java
index bbf10e2a823..51eb890c596 100644
---
a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/descriptor/MCPGuidancePayloadBuilderTest.java
+++
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/descriptor/MCPGuidancePayloadBuilderTest.java
@@ -55,11 +55,20 @@ class MCPGuidancePayloadBuilderTest {
assertThat(actualMetadataRoute.get("first_action"), is("read_resource
shardingsphere://databases"));
Map<?, ?> actualCompletionRoute =
findByKey(castToRouteList(actual.get("first_call_routes")), "intent",
"complete_uncertain_argument");
assertThat(actualCompletionRoute.get("first_action"), is("call
completion/complete for one uncertain argument"));
+ Map<?, ?> actualRuleWorkflowRoute =
findByKey(castToRouteList(actual.get("first_call_routes")), "intent",
"rule_workflow");
+ assertThat(actualRuleWorkflowRoute.get("first_action"), is("call_tool
matching database_gateway_plan_* workflow tool without plan_id for a new
plan"));
Map<?, ?> actualRecoveryRoute =
findByKey(castToRouteList(actual.get("first_call_routes")), "intent",
"recover_error");
assertThat(actualRecoveryRoute.get("first_action"), is("follow
top-level next_actions"));
assertThat(((Map<?, ?>) actual.get("preflight_rule")).get("tool"),
is("database_gateway_validate_runtime_database"));
-
assertThat(castToMap(castToMap(actual.get("sql_tool_selection")).get("side_effecting")).get("execute_requires"),
is("execution_mode=execute"));
+ Map<?, ?> actualSideEffectingSelection =
castToMap(castToMap(actual.get("sql_tool_selection")).get("side_effecting"));
+ assertThat(actualSideEffectingSelection.get("execute_requires"),
is("execution_mode=execute"));
+ assertThat(actualSideEffectingSelection.get("rule_change_preference"),
+ is("For natural-language rule changes, use the matching
database_gateway_plan_* workflow tool before raw SQL execution; "
+ + "omit plan_id for a new plan."));
Map<?, ?> actualWorkflowRule = castToMap(actual.get("workflow_rule"));
+ assertThat(actualWorkflowRule.get("selection_rule"),
+ is("For natural-language rule changes, use the matching
database_gateway_plan_* workflow tool before raw side-effect SQL; "
+ + "omit plan_id for a new plan and reuse only returned
plan_id values."));
assertThat(actualWorkflowRule.get("planning_tools"),
is(List.of("database_gateway_plan_encrypt_rule")));
assertThat(castToMap(actualWorkflowRule.get("preview_tool")).get("execution_mode"),
is("preview"));
assertThat(castToMap(actualWorkflowRule.get("execute_tool")).get("execute_requires"),
@@ -76,7 +85,8 @@ class MCPGuidancePayloadBuilderTest {
assertThat(actual.get("guidance_resource"),
is("shardingsphere://guidance"));
assertThat(actual.get("metadata_first_resource"),
is("shardingsphere://databases"));
assertTrue(String.valueOf(actual.get("preflight_rule")).contains("database_gateway_validate_runtime_database"));
-
assertThat(castToMap(actual.get("sql_tool_selection")).keySet().stream().toList(),
is(List.of("read_only", "side_effecting")));
+ Map<?, ?> actualSqlToolSelection =
castToMap(actual.get("sql_tool_selection"));
+ assertThat(actualSqlToolSelection.keySet().stream().toList(),
is(List.of("read_only", "side_effecting")));
assertThat(actual.get("side_effect_rule"), is("Preview before side
effects and continue only when the requested side effect is still intended."));
assertThat(actual.get("completion_rule"),
is("Use completion/complete for one uncertain argument at a
time; when completion reports missing context, follow meta.next_actions before
guessing."));
diff --git
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/suite/usability/scenario/LLMUsabilityScenarioCatalog.java
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/suite/usability/scenario/LLMUsabilityScenarioCatalog.java
index a5d35659f38..8aaeccb6dc4 100644
---
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/suite/usability/scenario/LLMUsabilityScenarioCatalog.java
+++
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/suite/usability/scenario/LLMUsabilityScenarioCatalog.java
@@ -95,8 +95,8 @@ public final class LLMUsabilityScenarioCatalog {
List.of(LLMUsabilityScenario.NATURAL_TASK_TAG, "natural",
"workflow", "mask"),
new LLME2EScenario("natural-mask-rule-md5-" + runtimeKind,
SYSTEM_PROMPT,
"A user asks you to create a mask rule for logical
database `" + databaseName + "`, schema `" + schemaName + "`, table `" +
tableName
- + "`, and column `status` with the MD5 mask
algorithm. Use the planning response plan_id for follow-up workflow calls, keep
execution manual-only, "
- + "validate the workflow with the returned
plan_id, and finish by verifying `" + query + "`.",
+ + "`, and column `status` with the MD5 mask
algorithm. Do not send plan_id to the planning tool; use the returned plan_id
for follow-up workflow calls, "
+ + "keep execution manual-only, validate the
workflow with the returned plan_id, and finish by verifying `" + query + "`.",
createAnswer(databaseName, schemaName, tableName,
query, totalOrders),
List.of(MCPInteractionActionNames.READ_RESOURCE,
"database_gateway_plan_mask_rule", "database_gateway_apply_workflow",
"database_gateway_validate_workflow",
"database_gateway_execute_query"),
@@ -107,8 +107,8 @@ public final class LLMUsabilityScenarioCatalog {
new LLME2EScenario("natural-encrypt-rule-md5-" + runtimeKind,
SYSTEM_PROMPT,
"A user asks you to identify and create an encrypt
rule now for logical database `" + databaseName + "`, schema `" + schemaName +
"`, table `" + tableName
+ "`, and column `status` using the MD5
encrypt algorithm with cipher column `status_cipher`. The user wants
irreversible hashing, no equality, and no like. "
- + "Use the planning response plan_id for
follow-up workflow calls, keep execution manual-only, validate the workflow
with the returned plan_id, "
- + "and finish by verifying `" + query + "`.",
+ + "Do not send plan_id to the planning tool;
use the returned plan_id for follow-up workflow calls, keep execution
manual-only, "
+ + "validate the workflow with the returned
plan_id, and finish by verifying `" + query + "`.",
createAnswer(databaseName, schemaName, tableName,
query, totalOrders),
List.of(MCPInteractionActionNames.READ_RESOURCE,
"database_gateway_plan_encrypt_rule", "database_gateway_apply_workflow",
"database_gateway_validate_workflow",
"database_gateway_execute_query"),
diff --git
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/suite/usability/scenario/LLMUsabilityScenarioCatalogTest.java
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/suite/usability/scenario/LLMUsabilityScenarioCatalogTest.java
index 1a8d49b669e..77969f7445d 100644
---
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/suite/usability/scenario/LLMUsabilityScenarioCatalogTest.java
+++
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/suite/usability/scenario/LLMUsabilityScenarioCatalogTest.java
@@ -53,6 +53,7 @@ class LLMUsabilityScenarioCatalogTest {
assertThat(actualScenarios.get("natural-mask-rule-md5-mysql").getLlmScenario().getUserPrompt(),
containsString("create a mask rule"));
assertThat(actualScenarios.get("natural-mask-rule-md5-mysql").getLlmScenario().getUserPrompt(),
containsString("table `orders`"));
assertThat(actualScenarios.get("natural-mask-rule-md5-mysql").getLlmScenario().getUserPrompt(),
containsString("MD5 mask algorithm"));
+
assertThat(actualScenarios.get("natural-mask-rule-md5-mysql").getLlmScenario().getUserPrompt(),
containsString("Do not send plan_id to the planning tool"));
assertThat(actualScenarios.get("natural-mask-rule-md5-mysql").getLlmScenario().getUserPrompt(),
containsString("manual-only"));
assertThat(actualScenarios.get("natural-mask-rule-md5-mysql").getLlmScenario().getUserPrompt(),
containsString("validate the workflow"));
assertThat(actualScenarios.get("natural-encrypt-rule-md5-mysql").getLlmScenario().getRequiredToolNames(),
@@ -62,6 +63,7 @@ class LLMUsabilityScenarioCatalogTest {
assertThat(actualScenarios.get("natural-encrypt-rule-md5-mysql").getLlmScenario().getUserPrompt(),
containsString("MD5 encrypt algorithm"));
assertThat(actualScenarios.get("natural-encrypt-rule-md5-mysql").getLlmScenario().getUserPrompt(),
containsString("cipher column `status_cipher`"));
assertThat(actualScenarios.get("natural-encrypt-rule-md5-mysql").getLlmScenario().getUserPrompt(),
containsString("irreversible hashing, no equality, and no like"));
+
assertThat(actualScenarios.get("natural-encrypt-rule-md5-mysql").getLlmScenario().getUserPrompt(),
containsString("Do not send plan_id to the planning tool"));
assertThat(actualScenarios.get("natural-encrypt-rule-md5-mysql").getLlmScenario().getUserPrompt(),
containsString("manual-only"));
assertThat(actualScenarios.get("natural-encrypt-rule-md5-mysql").getLlmScenario().getUserPrompt(),
containsString("validate the workflow"));
assertTrue(actual.stream().allMatch(each ->
each.getTags().contains("natural")));
diff --git
a/test/e2e/mcp/src/test/resources/baseline-contract/model-contract/guidance.yaml
b/test/e2e/mcp/src/test/resources/baseline-contract/model-contract/guidance.yaml
index 0e6127b7a99..ca27d47b403 100644
---
a/test/e2e/mcp/src/test/resources/baseline-contract/model-contract/guidance.yaml
+++
b/test/e2e/mcp/src/test/resources/baseline-contract/model-contract/guidance.yaml
@@ -34,6 +34,11 @@ model_first_summary:
- {intent: read_only_sql, first_action: 'read_resource
shardingsphere://databases/{database}/capabilities',
next_step: call_tool database_gateway_execute_query, stop_rule: Stop after
reporting
the result rows.}
+ - {intent: rule_workflow, first_action: call_tool matching
database_gateway_plan_*
+ workflow tool without plan_id for a new plan, next_step: call_tool
database_gateway_apply_workflow
+ execution_mode=preview with the returned plan_id, stop_rule: Use
workflow apply
+ and validation for natural-language rule changes before considering raw
side-effect
+ SQL.}
- {intent: side_effect_sql, first_action: call_tool
database_gateway_execute_update
execution_mode=preview, next_step: call_tool
database_gateway_execute_update
execution_mode=execute, stop_rule: Execute only after preview review
confirms
@@ -54,10 +59,19 @@ model_first_summary:
sql_tool_selection:
read_only: {tool: database_gateway_execute_query, statement_rule: Use for
one
SELECT or EXPLAIN ANALYZE statement.}
- side_effecting: {tool: database_gateway_execute_update, first_mode:
preview, execute_requires: execution_mode=execute}
+ side_effecting:
+ tool: database_gateway_execute_update
+ first_mode: preview
+ execute_requires: execution_mode=execute
+ rule_change_preference: For natural-language rule changes, use the
matching
+ database_gateway_plan_* workflow tool before raw SQL execution; omit
plan_id
+ for a new plan.
side_effect_rule: Preview side effects first; execute only when the
requested side
effect is still intended.
workflow_rule:
+ selection_rule: For natural-language rule changes, use the matching
database_gateway_plan_*
+ workflow tool before raw side-effect SQL; omit plan_id for a new plan
and reuse
+ only returned plan_id values.
planning_tools: [database_gateway_plan_broadcast_rule,
database_gateway_plan_encrypt_rule,
database_gateway_plan_mask_rule,
database_gateway_plan_readwrite_splitting_rule,
database_gateway_plan_readwrite_splitting_status,
database_gateway_plan_shadow_rule,