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,

Reply via email to