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 9e60e6a3c4f Refactor MCP workflow support duplication (#38974)
9e60e6a3c4f is described below

commit 9e60e6a3c4f80a53e6fa60384e8cfbfb83da04e1
Author: Liang Zhang <[email protected]>
AuthorDate: Wed Jul 1 20:00:19 2026 +0800

    Refactor MCP workflow support duplication (#38974)
    
    * Fix documentation accuracy and consistency
    
    - align DistSQL syntax docs, return columns, and examples with current 
source
    - normalize SQL and server error code documentation
    - fix MCP resources, management metadata docs, links, versions, and typos
    
    * Fix documentation accuracy and consistency
    
    - align DistSQL syntax docs, return columns, and examples with current 
source
    - normalize SQL and server error code documentation
    - fix MCP resources, management metadata docs, links, versions, and typos
    
    * Refactor MCP workflow support duplication
    
    Centralize repeated workflow validation, identifier checks, DistSQL rule 
query fallback, and MCP error/recovery mapping while preserving existing 
behavior. Reuse shared helpers across MCP feature planning/validation services 
and simplify duplicated MCP E2E test setup helpers.
---
 .../mcp/core/protocol/error/MCPErrorConverter.java |  85 +++----
 .../protocol/error/MCPRecoveryPayloadFactory.java  |  96 ++++----
 .../service/BroadcastWorkflowPlanningService.java  |  23 +-
 .../BroadcastWorkflowValidationService.java        |   8 +-
 .../tool/service/EncryptRuleInspectionService.java |  17 +-
 .../service/EncryptWorkflowValidationService.java  |   8 +-
 .../tool/service/MaskRuleInspectionService.java    |  15 +-
 .../service/MaskWorkflowValidationService.java     |   8 +-
 ...dwriteSplittingRuleWorkflowPlanningService.java |  26 +--
 ...riteSplittingRuleWorkflowValidationService.java |   8 +-
 ...riteSplittingStatusWorkflowPlanningService.java |  15 +-
 ...teSplittingStatusWorkflowValidationService.java |   8 +-
 .../service/ShadowWorkflowPlanningService.java     |  46 ++--
 .../service/ShadowWorkflowValidationService.java   |   8 +-
 .../service/ShardingWorkflowValidationService.java |   8 +-
 .../service/WorkflowDistSQLQueryUtils.java         |  22 ++
 .../WorkflowGuidanceResourceHintProvider.java      | 259 ++++++++++-----------
 .../service/WorkflowIntentResolverSupport.java     |  64 ++---
 .../workflow/service/WorkflowPlanningSupport.java  |  71 +++++-
 .../service/WorkflowValidationSupport.java         |  21 ++
 .../service/WorkflowDistSQLQueryUtilsTest.java     |  33 +++
 .../service/WorkflowPlanningSupportTest.java       |  18 ++
 .../service/WorkflowValidationSupportTest.java     |  32 +++
 .../AbstractHttpProgrammaticRuntimeE2ETest.java    |  15 ++
 .../HttpTransportApprovalSafetyE2ETest.java        |  15 --
 .../HttpTransportCompletionE2ETest.java            |  14 --
 .../support/runtime/MySQLRuntimeTestSupport.java   |  29 ++-
 27 files changed, 493 insertions(+), 479 deletions(-)

diff --git 
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPErrorConverter.java
 
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPErrorConverter.java
index 8c7fad6d73c..4aa11fb52b9 100644
--- 
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPErrorConverter.java
+++ 
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPErrorConverter.java
@@ -36,6 +36,7 @@ import java.sql.SQLException;
 import java.sql.SQLFeatureNotSupportedException;
 import java.sql.SQLSyntaxErrorException;
 import java.sql.SQLTimeoutException;
+import java.util.List;
 import java.util.Objects;
 
 /**
@@ -44,6 +45,26 @@ import java.util.Objects;
 @NoArgsConstructor(access = AccessLevel.PRIVATE)
 public final class MCPErrorConverter {
     
+    private static final List<ErrorMapping> ERROR_MAPPINGS = List.of(
+            new ErrorMapping(UnsupportedToolException.class, "Unsupported 
tool."),
+            new ErrorMapping(UnsupportedResourceUriException.class, 
"Unsupported resource URI."),
+            new ErrorMapping(MCPInvalidRequestException.class, "Invalid 
request."),
+            new ErrorMapping(MCPNotFoundException.class, "MCP operation not 
found."),
+            new ErrorMapping(MCPUnsupportedException.class, "Unsupported MCP 
operation."),
+            new ErrorMapping(MCPTimeoutException.class, "MCP operation 
timeout."),
+            new ErrorMapping(MCPTransactionStateException.class, "MCP 
transaction operation failed."),
+            new ErrorMapping(MCPQueryFailedException.class, "MCP query 
failed."),
+            new ErrorMapping(MCPToolCallLimitExceededException.class, "MCP 
tool call limit exceeded."),
+            new ErrorMapping(MCPUnavailableException.class, "Service is 
temporarily unavailable."),
+            new ErrorMapping(RuntimeDatabaseConnectionException.class, 
"Runtime database connection failed."),
+            new ErrorMapping(SQLSyntaxErrorException.class, "Invalid 
request."),
+            new ErrorMapping(SQLTimeoutException.class, "MCP operation 
timeout."),
+            new ErrorMapping(SQLFeatureNotSupportedException.class, 
"Unsupported MCP operation."),
+            new ErrorMapping(UnsupportedOperationException.class, "Unsupported 
MCP operation."),
+            new ErrorMapping(SQLException.class, "MCP query failed."),
+            new ErrorMapping(IllegalArgumentException.class, "Invalid 
request."),
+            new ErrorMapping(IllegalStateException.class, "MCP operation 
failed."));
+    
     /**
      * Convert throwable to MCP error.
      *
@@ -51,59 +72,10 @@ public final class MCPErrorConverter {
      * @return MCP error
      */
     public static MCPErrorResponse convert(final Throwable cause) {
-        if (cause instanceof UnsupportedToolException) {
-            return createError(cause, "Unsupported tool.");
-        }
-        if (cause instanceof UnsupportedResourceUriException) {
-            return createError(cause, "Unsupported resource URI.");
-        }
-        if (cause instanceof MCPInvalidRequestException) {
-            return createError(cause, "Invalid request.");
-        }
-        if (cause instanceof MCPNotFoundException) {
-            return createError(cause, "MCP operation not found.");
-        }
-        if (cause instanceof MCPUnsupportedException) {
-            return createError(cause, "Unsupported MCP operation.");
-        }
-        if (cause instanceof MCPTimeoutException) {
-            return createError(cause, "MCP operation timeout.");
-        }
-        if (cause instanceof MCPTransactionStateException) {
-            return createError(cause, "MCP transaction operation failed.");
-        }
-        if (cause instanceof MCPQueryFailedException) {
-            return createError(cause, "MCP query failed.");
-        }
-        if (cause instanceof MCPToolCallLimitExceededException) {
-            return createError(cause, "MCP tool call limit exceeded.");
-        }
-        if (cause instanceof MCPUnavailableException) {
-            return createError(cause, "Service is temporarily unavailable.");
-        }
-        if (cause instanceof RuntimeDatabaseConnectionException) {
-            return createError(cause, "Runtime database connection failed.");
-        }
-        if (cause instanceof SQLSyntaxErrorException) {
-            return createError(cause, "Invalid request.");
-        }
-        if (cause instanceof SQLTimeoutException) {
-            return createError(cause, "MCP operation timeout.");
-        }
-        if (cause instanceof SQLFeatureNotSupportedException) {
-            return createError(cause, "Unsupported MCP operation.");
-        }
-        if (cause instanceof UnsupportedOperationException) {
-            return createError(cause, "Unsupported MCP operation.");
-        }
-        if (cause instanceof SQLException) {
-            return createError(cause, "MCP query failed.");
-        }
-        if (cause instanceof IllegalArgumentException) {
-            return createError(cause, "Invalid request.");
-        }
-        if (cause instanceof IllegalStateException) {
-            return createError(cause, "MCP operation failed.");
+        for (ErrorMapping each : ERROR_MAPPINGS) {
+            if (each.matches(cause)) {
+                return createError(cause, each.defaultMessage());
+            }
         }
         return createError(cause, "Service is temporarily unavailable.");
     }
@@ -112,4 +84,11 @@ public final class MCPErrorConverter {
         String message = MCPQueryRecoveryPayloadFactory.isQueryFailure(cause) 
? defaultMessage : Objects.toString(cause.getMessage(), defaultMessage).trim();
         return new MCPErrorResponse(message, 
MCPRecoveryPayloadFactory.create(cause));
     }
+    
+    private record ErrorMapping(Class<? extends Throwable> causeType, String 
defaultMessage) {
+        
+        private boolean matches(final Throwable cause) {
+            return causeType.isInstance(cause);
+        }
+    }
 }
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 42f969eec99..d7ee80899a7 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
@@ -37,7 +37,9 @@ import 
org.apache.shardingsphere.mcp.core.tool.handler.execute.MetadataIntrospec
 import 
org.apache.shardingsphere.mcp.core.tool.handler.execute.SQLToolMismatchException;
 import 
org.apache.shardingsphere.mcp.support.database.metadata.jdbc.RuntimeDatabaseConnectionException;
 
+import java.util.List;
 import java.util.Map;
+import java.util.function.Function;
 
 /**
  * MCP recovery payload factory.
@@ -45,61 +47,55 @@ import java.util.Map;
 @NoArgsConstructor(access = AccessLevel.PRIVATE)
 final class MCPRecoveryPayloadFactory {
     
+    private static final List<RecoveryMapping> RECOVERY_MAPPINGS = List.of(
+            new RecoveryMapping(SQLToolMismatchException.class, cause -> 
MCPSQLRecoveryPayloadFactory.createSQLToolMismatchRecovery((SQLToolMismatchException)
 cause)),
+            new 
RecoveryMapping(MetadataIntrospectionSQLStatementException.class,
+                    cause -> 
MCPSQLRecoveryPayloadFactory.createMetadataIntrospectionSQLRecovery((MetadataIntrospectionSQLStatementException)
 cause)),
+            new RecoveryMapping(MCPMultipleSQLStatementsException.class, cause 
-> MCPSQLRecoveryPayloadFactory.createMultipleStatementsRecovery()),
+            new RecoveryMapping(MCPUnsupportedSQLStatementException.class, 
cause -> MCPSQLRecoveryPayloadFactory.createUnsupportedStatementRecovery()),
+            new RecoveryMapping(MCPBannedSQLStatementException.class, cause -> 
MCPSQLRecoveryPayloadFactory.createBannedStatementRecovery()),
+            new RecoveryMapping(MCPExecutionModeRequiredException.class,
+                    cause -> 
MCPWorkflowRecoveryPayloadFactory.createMissingExecutionModeRecovery((MCPExecutionModeRequiredException)
 cause)),
+            new RecoveryMapping(MCPInvalidExecutionModeException.class,
+                    cause -> 
MCPWorkflowRecoveryPayloadFactory.createInvalidExecutionModeRecovery((MCPInvalidExecutionModeException)
 cause)),
+            new RecoveryMapping(MCPInvalidApprovedStepsException.class,
+                    cause -> 
MCPWorkflowRecoveryPayloadFactory.createInvalidApprovedStepsRecovery((MCPInvalidApprovedStepsException)
 cause)),
+            new RecoveryMapping(MCPWorkflowStateException.class, cause -> 
MCPWorkflowRecoveryPayloadFactory.createWorkflowStateRecovery((MCPWorkflowStateException)
 cause)),
+            new RecoveryMapping(UnsupportedToolException.class, cause -> 
MCPBasicRecoveryPayloadFactory.createUnsupportedToolRecovery(((UnsupportedToolException)
 cause).getToolName())),
+            new RecoveryMapping(UnsupportedResourceUriException.class,
+                    cause -> 
MCPBasicRecoveryPayloadFactory.createUnsupportedResourceRecovery(((UnsupportedResourceUriException)
 cause).getResourceUri())),
+            new RecoveryMapping(RuntimeDatabaseConnectionException.class,
+                    cause -> 
MCPBasicRecoveryPayloadFactory.createRuntimeDatabaseConnectionRecovery((RuntimeDatabaseConnectionException)
 cause)),
+            new RecoveryMapping(MCPToolCallLimitExceededException.class,
+                    cause -> 
MCPBasicRecoveryPayloadFactory.createToolCallLimitRecovery((MCPToolCallLimitExceededException)
 cause)),
+            new RecoveryMapping(MCPInvalidToolArgumentException.class, cause 
-> 
MCPBasicRecoveryPayloadFactory.createInvalidToolArgumentRecovery((MCPInvalidToolArgumentException)
 cause)),
+            new 
RecoveryMapping(MCPToolArgumentContractViolationException.class,
+                    cause -> 
MCPBasicRecoveryPayloadFactory.createToolArgumentContractViolationRecovery((MCPToolArgumentContractViolationException)
 cause)),
+            new RecoveryMapping(MCPMissingToolArgumentException.class,
+                    cause -> 
MCPBasicRecoveryPayloadFactory.createMissingArgumentRecovery(((MCPMissingToolArgumentException)
 cause).getArgumentName())),
+            new RecoveryMapping(MCPInvalidMetadataObjectTypesException.class,
+                    cause -> 
MCPBasicRecoveryPayloadFactory.createInvalidObjectTypesRecovery((MCPInvalidMetadataObjectTypesException)
 cause)));
+    
     static Map<String, Object> create(final Throwable cause) {
-        if (cause instanceof SQLToolMismatchException) {
-            return 
MCPSQLRecoveryPayloadFactory.createSQLToolMismatchRecovery((SQLToolMismatchException)
 cause);
-        }
-        if (cause instanceof MetadataIntrospectionSQLStatementException) {
-            return 
MCPSQLRecoveryPayloadFactory.createMetadataIntrospectionSQLRecovery((MetadataIntrospectionSQLStatementException)
 cause);
-        }
-        if (cause instanceof MCPMultipleSQLStatementsException) {
-            return 
MCPSQLRecoveryPayloadFactory.createMultipleStatementsRecovery();
-        }
-        if (cause instanceof MCPUnsupportedSQLStatementException) {
-            return 
MCPSQLRecoveryPayloadFactory.createUnsupportedStatementRecovery();
-        }
-        if (cause instanceof MCPBannedSQLStatementException) {
-            return 
MCPSQLRecoveryPayloadFactory.createBannedStatementRecovery();
-        }
-        if (cause instanceof MCPExecutionModeRequiredException) {
-            return 
MCPWorkflowRecoveryPayloadFactory.createMissingExecutionModeRecovery((MCPExecutionModeRequiredException)
 cause);
-        }
-        if (cause instanceof MCPInvalidExecutionModeException) {
-            return 
MCPWorkflowRecoveryPayloadFactory.createInvalidExecutionModeRecovery((MCPInvalidExecutionModeException)
 cause);
-        }
-        if (cause instanceof MCPInvalidApprovedStepsException) {
-            return 
MCPWorkflowRecoveryPayloadFactory.createInvalidApprovedStepsRecovery((MCPInvalidApprovedStepsException)
 cause);
-        }
-        if (cause instanceof MCPWorkflowStateException) {
-            return 
MCPWorkflowRecoveryPayloadFactory.createWorkflowStateRecovery((MCPWorkflowStateException)
 cause);
-        }
-        if (cause instanceof UnsupportedToolException) {
-            return 
MCPBasicRecoveryPayloadFactory.createUnsupportedToolRecovery(((UnsupportedToolException)
 cause).getToolName());
-        }
-        if (cause instanceof UnsupportedResourceUriException) {
-            return 
MCPBasicRecoveryPayloadFactory.createUnsupportedResourceRecovery(((UnsupportedResourceUriException)
 cause).getResourceUri());
-        }
-        if (cause instanceof RuntimeDatabaseConnectionException) {
-            return 
MCPBasicRecoveryPayloadFactory.createRuntimeDatabaseConnectionRecovery((RuntimeDatabaseConnectionException)
 cause);
-        }
-        if (cause instanceof MCPToolCallLimitExceededException) {
-            return 
MCPBasicRecoveryPayloadFactory.createToolCallLimitRecovery((MCPToolCallLimitExceededException)
 cause);
-        }
-        if (cause instanceof MCPInvalidToolArgumentException) {
-            return 
MCPBasicRecoveryPayloadFactory.createInvalidToolArgumentRecovery((MCPInvalidToolArgumentException)
 cause);
-        }
-        if (cause instanceof MCPToolArgumentContractViolationException) {
-            return 
MCPBasicRecoveryPayloadFactory.createToolArgumentContractViolationRecovery((MCPToolArgumentContractViolationException)
 cause);
-        }
-        if (cause instanceof MCPMissingToolArgumentException) {
-            return 
MCPBasicRecoveryPayloadFactory.createMissingArgumentRecovery(((MCPMissingToolArgumentException)
 cause).getArgumentName());
-        }
-        if (cause instanceof MCPInvalidMetadataObjectTypesException) {
-            return 
MCPBasicRecoveryPayloadFactory.createInvalidObjectTypesRecovery((MCPInvalidMetadataObjectTypesException)
 cause);
+        for (RecoveryMapping each : RECOVERY_MAPPINGS) {
+            if (each.matches(cause)) {
+                return each.create(cause);
+            }
         }
         if (MCPQueryRecoveryPayloadFactory.isQueryFailure(cause)) {
             return MCPQueryRecoveryPayloadFactory.create(cause);
         }
         return Map.of();
     }
+    
+    private record RecoveryMapping(Class<? extends Throwable> causeType, 
Function<Throwable, Map<String, Object>> payloadFactory) {
+        
+        private boolean matches(final Throwable cause) {
+            return causeType.isInstance(cause);
+        }
+        
+        private Map<String, Object> create(final Throwable cause) {
+            return payloadFactory.apply(cause);
+        }
+    }
 }
diff --git 
a/mcp/features/broadcast/src/main/java/org/apache/shardingsphere/mcp/feature/broadcast/tool/service/BroadcastWorkflowPlanningService.java
 
b/mcp/features/broadcast/src/main/java/org/apache/shardingsphere/mcp/feature/broadcast/tool/service/BroadcastWorkflowPlanningService.java
index 2b5e09b8941..0586b2d20d8 100644
--- 
a/mcp/features/broadcast/src/main/java/org/apache/shardingsphere/mcp/feature/broadcast/tool/service/BroadcastWorkflowPlanningService.java
+++ 
b/mcp/features/broadcast/src/main/java/org/apache/shardingsphere/mcp/feature/broadcast/tool/service/BroadcastWorkflowPlanningService.java
@@ -30,7 +30,6 @@ import 
org.apache.shardingsphere.mcp.support.workflow.service.WorkflowPlanningSu
 import 
org.apache.shardingsphere.mcp.support.workflow.service.WorkflowRuleValueUtils;
 import org.apache.shardingsphere.mcp.support.workflow.service.WorkflowSQLUtils;
 
-import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 
@@ -113,7 +112,8 @@ public final class BroadcastWorkflowPlanningService {
             snapshot.setStatus(WorkflowLifecycle.STATUS_CLARIFYING);
             return false;
         }
-        if (!ensureSupportedIdentifier("database", request.getDatabase(), 
snapshot) || !ensureSupportedIdentifiers("tables", request.getTargetTables(), 
snapshot)) {
+        if (!planningSupport.ensureSupportedIdentifiers("database", 
List.of(request.getDatabase()), snapshot, "discovering")
+                || !planningSupport.ensureSupportedIdentifiers("tables", 
request.getTargetTables(), snapshot, "discovering")) {
             snapshot.setStatus(WorkflowLifecycle.STATUS_FAILED);
             return false;
         }
@@ -128,25 +128,6 @@ public final class BroadcastWorkflowPlanningService {
         return true;
     }
     
-    private boolean ensureSupportedIdentifiers(final String fieldName, final 
Collection<String> identifiers, final WorkflowContextSnapshot snapshot) {
-        for (String each : identifiers) {
-            if (!ensureSupportedIdentifier(fieldName, each, snapshot)) {
-                return false;
-            }
-        }
-        return true;
-    }
-    
-    private boolean ensureSupportedIdentifier(final String fieldName, final 
String identifier, final WorkflowContextSnapshot snapshot) {
-        if (WorkflowSQLUtils.isSupportedIdentifier(identifier)) {
-            return true;
-        }
-        snapshot.getIssues().add(new 
WorkflowIssue(WorkflowIssueCode.UNSUPPORTED_IDENTIFIER, "error", "discovering",
-                String.format("%s identifier `%s` contains unsupported 
characters.", fieldName, identifier),
-                "Use a reviewable logical identifier without NUL or line 
terminators.", false, Map.of("field", fieldName, "identifier", identifier)));
-        return false;
-    }
-    
     private boolean ensureLifecycleState(final ClarifiedIntent 
clarifiedIntent, final BroadcastWorkflowRequest request, final List<Map<String, 
Object>> broadcastRules,
                                          final WorkflowContextSnapshot 
snapshot, final String databaseType) {
         boolean dropWorkflow = 
WorkflowLifecycle.OPERATION_DROP.equalsIgnoreCase(clarifiedIntent.getOperationType());
diff --git 
a/mcp/features/broadcast/src/main/java/org/apache/shardingsphere/mcp/feature/broadcast/tool/service/BroadcastWorkflowValidationService.java
 
b/mcp/features/broadcast/src/main/java/org/apache/shardingsphere/mcp/feature/broadcast/tool/service/BroadcastWorkflowValidationService.java
index 630dd312dc9..d74ce8be30c 100644
--- 
a/mcp/features/broadcast/src/main/java/org/apache/shardingsphere/mcp/feature/broadcast/tool/service/BroadcastWorkflowValidationService.java
+++ 
b/mcp/features/broadcast/src/main/java/org/apache/shardingsphere/mcp/feature/broadcast/tool/service/BroadcastWorkflowValidationService.java
@@ -62,13 +62,7 @@ public final class BroadcastWorkflowValidationService 
implements MCPWorkflowRunt
     public Map<String, Object> validate(final WorkflowSessionContext 
workflowSessionContext, final MCPMetadataQueryFacade metadataQueryFacade,
                                         final MCPFeatureQueryFacade 
queryFacade, final MCPFeatureExecutionFacade executionFacade, final String 
sessionId,
                                         final WorkflowContextSnapshot 
snapshot) {
-        Map<String, Object> rejectedResponse = 
validationSupport.checkValidatePreconditions(sessionId, snapshot);
-        if (!rejectedResponse.isEmpty()) {
-            return rejectedResponse;
-        }
-        ValidationReport validationReport = createValidationReport(snapshot, 
queryFacade);
-        snapshot.setValidationReport(validationReport);
-        return validationSupport.finalizeValidation(workflowSessionContext, 
snapshot, validationReport);
+        return validationSupport.validateAndFinalize(workflowSessionContext, 
sessionId, snapshot, () -> createValidationReport(snapshot, queryFacade));
     }
     
     @Override
diff --git 
a/mcp/features/encrypt/src/main/java/org/apache/shardingsphere/mcp/feature/encrypt/tool/service/EncryptRuleInspectionService.java
 
b/mcp/features/encrypt/src/main/java/org/apache/shardingsphere/mcp/feature/encrypt/tool/service/EncryptRuleInspectionService.java
index 9be1b45ba74..e58cd310301 100644
--- 
a/mcp/features/encrypt/src/main/java/org/apache/shardingsphere/mcp/feature/encrypt/tool/service/EncryptRuleInspectionService.java
+++ 
b/mcp/features/encrypt/src/main/java/org/apache/shardingsphere/mcp/feature/encrypt/tool/service/EncryptRuleInspectionService.java
@@ -17,8 +17,8 @@
 
 package org.apache.shardingsphere.mcp.feature.encrypt.tool.service;
 
-import 
org.apache.shardingsphere.mcp.api.protocol.exception.MCPQueryFailedException;
 import org.apache.shardingsphere.mcp.feature.encrypt.EncryptFeatureDefinition;
+import 
org.apache.shardingsphere.mcp.api.protocol.exception.MCPQueryFailedException;
 import 
org.apache.shardingsphere.mcp.support.database.spi.MCPFeatureQueryFacade;
 import 
org.apache.shardingsphere.mcp.support.workflow.model.AlgorithmPropertyRequirement;
 import 
org.apache.shardingsphere.mcp.support.workflow.service.WorkflowDistSQLQueryUtils;
@@ -44,7 +44,7 @@ public final class EncryptRuleInspectionService {
      * @return encrypt rules
      */
     public List<Map<String, Object>> queryEncryptRules(final 
MCPFeatureQueryFacade queryFacade, final String databaseName) {
-        return queryRuleRows(queryFacade, databaseName, String.format("SHOW 
ENCRYPT RULES FROM %s", 
WorkflowSQLUtils.formatDistSQLIdentifier(databaseName)));
+        return WorkflowDistSQLQueryUtils.queryRuleRows(queryFacade, 
databaseName, String.format("SHOW ENCRYPT RULES FROM %s", 
WorkflowSQLUtils.formatDistSQLIdentifier(databaseName)));
     }
     
     /**
@@ -56,7 +56,7 @@ public final class EncryptRuleInspectionService {
      * @return encrypt rules
      */
     public List<Map<String, Object>> queryEncryptRules(final 
MCPFeatureQueryFacade queryFacade, final String databaseName, final String 
tableName) {
-        return queryRuleRows(
+        return WorkflowDistSQLQueryUtils.queryRuleRows(
                 queryFacade, databaseName,
                 String.format("SHOW ENCRYPT TABLE RULE %s FROM %s", 
WorkflowSQLUtils.formatDistSQLIdentifier(tableName), 
WorkflowSQLUtils.formatDistSQLIdentifier(databaseName)));
     }
@@ -87,17 +87,6 @@ public final class EncryptRuleInspectionService {
         return result;
     }
     
-    private List<Map<String, Object>> queryRuleRows(final 
MCPFeatureQueryFacade queryFacade, final String databaseName, final String sql) 
{
-        try {
-            return queryFacade.query(databaseName, "", sql);
-        } catch (final MCPQueryFailedException ex) {
-            if 
(WorkflowDistSQLQueryUtils.isUnsupportedDistSQLQueryFailure(ex)) {
-                return List.of();
-            }
-            throw ex;
-        }
-    }
-    
     private List<Map<String, Object>> queryAlgorithmRows(final 
MCPFeatureQueryFacade queryFacade) {
         try {
             return queryFacade.queryWithAnyDatabase("SHOW ENCRYPT ALGORITHM 
PLUGINS");
diff --git 
a/mcp/features/encrypt/src/main/java/org/apache/shardingsphere/mcp/feature/encrypt/tool/service/EncryptWorkflowValidationService.java
 
b/mcp/features/encrypt/src/main/java/org/apache/shardingsphere/mcp/feature/encrypt/tool/service/EncryptWorkflowValidationService.java
index c7417257467..3ecc86e065c 100644
--- 
a/mcp/features/encrypt/src/main/java/org/apache/shardingsphere/mcp/feature/encrypt/tool/service/EncryptWorkflowValidationService.java
+++ 
b/mcp/features/encrypt/src/main/java/org/apache/shardingsphere/mcp/feature/encrypt/tool/service/EncryptWorkflowValidationService.java
@@ -72,13 +72,7 @@ public final class EncryptWorkflowValidationService 
implements MCPWorkflowRuntim
     public Map<String, Object> validate(final WorkflowSessionContext 
workflowSessionContext, final MCPMetadataQueryFacade metadataQueryFacade,
                                         final MCPFeatureQueryFacade 
queryFacade, final MCPFeatureExecutionFacade executionFacade, final String 
sessionId,
                                         final WorkflowContextSnapshot 
snapshot) {
-        Map<String, Object> rejectedResponse = 
validationSupport.checkValidatePreconditions(sessionId, snapshot);
-        if (!rejectedResponse.isEmpty()) {
-            return rejectedResponse;
-        }
-        ValidationReport validationReport = createValidationReport(snapshot, 
queryFacade);
-        snapshot.setValidationReport(validationReport);
-        return validationSupport.finalizeValidation(workflowSessionContext, 
snapshot, validationReport);
+        return validationSupport.validateAndFinalize(workflowSessionContext, 
sessionId, snapshot, () -> createValidationReport(snapshot, queryFacade));
     }
     
     @Override
diff --git 
a/mcp/features/mask/src/main/java/org/apache/shardingsphere/mcp/feature/mask/tool/service/MaskRuleInspectionService.java
 
b/mcp/features/mask/src/main/java/org/apache/shardingsphere/mcp/feature/mask/tool/service/MaskRuleInspectionService.java
index 7de3d5926db..e924daa449e 100644
--- 
a/mcp/features/mask/src/main/java/org/apache/shardingsphere/mcp/feature/mask/tool/service/MaskRuleInspectionService.java
+++ 
b/mcp/features/mask/src/main/java/org/apache/shardingsphere/mcp/feature/mask/tool/service/MaskRuleInspectionService.java
@@ -45,7 +45,7 @@ public final class MaskRuleInspectionService {
      * @return mask rules
      */
     public List<Map<String, Object>> queryMaskRules(final 
MCPFeatureQueryFacade queryFacade, final String databaseName) {
-        return queryRuleRows(queryFacade, databaseName, String.format("SHOW 
MASK RULES FROM %s", WorkflowSQLUtils.formatDistSQLIdentifier(databaseName)));
+        return WorkflowDistSQLQueryUtils.queryRuleRows(queryFacade, 
databaseName, String.format("SHOW MASK RULES FROM %s", 
WorkflowSQLUtils.formatDistSQLIdentifier(databaseName)));
     }
     
     /**
@@ -57,7 +57,7 @@ public final class MaskRuleInspectionService {
      * @return mask rules
      */
     public List<Map<String, Object>> queryMaskRules(final 
MCPFeatureQueryFacade queryFacade, final String databaseName, final String 
tableName) {
-        return queryRuleRows(
+        return WorkflowDistSQLQueryUtils.queryRuleRows(
                 queryFacade, databaseName, String.format("SHOW MASK RULE %s 
FROM %s", WorkflowSQLUtils.formatDistSQLIdentifier(tableName), 
WorkflowSQLUtils.formatDistSQLIdentifier(databaseName)));
     }
     
@@ -71,17 +71,6 @@ public final class MaskRuleInspectionService {
         return decorateMaskAlgorithms(queryAlgorithmRows(queryFacade));
     }
     
-    private List<Map<String, Object>> queryRuleRows(final 
MCPFeatureQueryFacade queryFacade, final String databaseName, final String sql) 
{
-        try {
-            return queryFacade.query(databaseName, "", sql);
-        } catch (final MCPQueryFailedException ex) {
-            if 
(WorkflowDistSQLQueryUtils.isUnsupportedDistSQLQueryFailure(ex)) {
-                return List.of();
-            }
-            throw ex;
-        }
-    }
-    
     private List<Map<String, Object>> queryAlgorithmRows(final 
MCPFeatureQueryFacade queryFacade) {
         try {
             return queryFacade.queryWithAnyDatabase("SHOW MASK ALGORITHM 
PLUGINS");
diff --git 
a/mcp/features/mask/src/main/java/org/apache/shardingsphere/mcp/feature/mask/tool/service/MaskWorkflowValidationService.java
 
b/mcp/features/mask/src/main/java/org/apache/shardingsphere/mcp/feature/mask/tool/service/MaskWorkflowValidationService.java
index ed1953d3904..8e01b46687d 100644
--- 
a/mcp/features/mask/src/main/java/org/apache/shardingsphere/mcp/feature/mask/tool/service/MaskWorkflowValidationService.java
+++ 
b/mcp/features/mask/src/main/java/org/apache/shardingsphere/mcp/feature/mask/tool/service/MaskWorkflowValidationService.java
@@ -70,13 +70,7 @@ public final class MaskWorkflowValidationService implements 
MCPWorkflowRuntimeHa
     public Map<String, Object> validate(final WorkflowSessionContext 
workflowSessionContext, final MCPMetadataQueryFacade metadataQueryFacade,
                                         final MCPFeatureQueryFacade 
queryFacade, final MCPFeatureExecutionFacade executionFacade, final String 
sessionId,
                                         final WorkflowContextSnapshot 
snapshot) {
-        Map<String, Object> rejectedResponse = 
validationSupport.checkValidatePreconditions(sessionId, snapshot);
-        if (!rejectedResponse.isEmpty()) {
-            return rejectedResponse;
-        }
-        ValidationReport validationReport = createValidationReport(snapshot, 
queryFacade);
-        snapshot.setValidationReport(validationReport);
-        return validationSupport.finalizeValidation(workflowSessionContext, 
snapshot, validationReport);
+        return validationSupport.validateAndFinalize(workflowSessionContext, 
sessionId, snapshot, () -> createValidationReport(snapshot, queryFacade));
     }
     
     @Override
diff --git 
a/mcp/features/readwrite-splitting/src/main/java/org/apache/shardingsphere/mcp/feature/readwritesplitting/tool/service/ReadwriteSplittingRuleWorkflowPlanningService.java
 
b/mcp/features/readwrite-splitting/src/main/java/org/apache/shardingsphere/mcp/feature/readwritesplitting/tool/service/ReadwriteSplittingRuleWorkflowPlanningService.java
index d71b0f52a84..7fc058b4759 100644
--- 
a/mcp/features/readwrite-splitting/src/main/java/org/apache/shardingsphere/mcp/feature/readwritesplitting/tool/service/ReadwriteSplittingRuleWorkflowPlanningService.java
+++ 
b/mcp/features/readwrite-splitting/src/main/java/org/apache/shardingsphere/mcp/feature/readwritesplitting/tool/service/ReadwriteSplittingRuleWorkflowPlanningService.java
@@ -128,9 +128,10 @@ public final class 
ReadwriteSplittingRuleWorkflowPlanningService {
             snapshot.setStatus(WorkflowLifecycle.STATUS_CLARIFYING);
             return false;
         }
-        if (!ensureSupportedIdentifier("database", request.getDatabase(), 
snapshot) || 
!ensureSupportedIdentifier(ReadwriteSplittingFeatureDefinition.RULE_FIELD, 
request.getRuleName(), snapshot)
-                || 
!ensureSupportedIdentifier(ReadwriteSplittingFeatureDefinition.WRITE_STORAGE_UNIT_FIELD,
 request.getWriteStorageUnit(), snapshot)
-                || 
!ensureSupportedIdentifiers(ReadwriteSplittingFeatureDefinition.READ_STORAGE_UNITS_FIELD,
 request.getReadStorageUnits(), snapshot)) {
+        if (!planningSupport.ensureOptionalSupportedIdentifiers("database", 
List.of(request.getDatabase()), snapshot, "intaking")
+                || 
!planningSupport.ensureOptionalSupportedIdentifiers(ReadwriteSplittingFeatureDefinition.RULE_FIELD,
 List.of(request.getRuleName()), snapshot, "intaking")
+                || 
!planningSupport.ensureOptionalSupportedIdentifiers(ReadwriteSplittingFeatureDefinition.WRITE_STORAGE_UNIT_FIELD,
 List.of(request.getWriteStorageUnit()), snapshot, "intaking")
+                || 
!planningSupport.ensureOptionalSupportedIdentifiers(ReadwriteSplittingFeatureDefinition.READ_STORAGE_UNITS_FIELD,
 request.getReadStorageUnits(), snapshot, "intaking")) {
             snapshot.setStatus(WorkflowLifecycle.STATUS_FAILED);
             return false;
         }
@@ -167,25 +168,6 @@ public final class 
ReadwriteSplittingRuleWorkflowPlanningService {
         }
     }
     
-    private boolean ensureSupportedIdentifiers(final String fieldName, final 
Collection<String> identifiers, final WorkflowContextSnapshot snapshot) {
-        for (String each : identifiers) {
-            if (!ensureSupportedIdentifier(fieldName, each, snapshot)) {
-                return false;
-            }
-        }
-        return true;
-    }
-    
-    private boolean ensureSupportedIdentifier(final String fieldName, final 
String identifier, final WorkflowContextSnapshot snapshot) {
-        if (identifier.isEmpty() || 
WorkflowSQLUtils.isSupportedIdentifier(identifier)) {
-            return true;
-        }
-        snapshot.getIssues().add(new 
WorkflowIssue(WorkflowIssueCode.UNSUPPORTED_IDENTIFIER, "error", "intaking",
-                String.format("%s identifier `%s` contains unsupported 
characters.", fieldName, identifier),
-                "Use a reviewable logical identifier without NUL or line 
terminators.", false, Map.of("field", fieldName, "identifier", identifier)));
-        return false;
-    }
-    
     private boolean ensureLifecycleState(final ClarifiedIntent 
clarifiedIntent, final ReadwriteSplittingRuleWorkflowRequest request, final 
List<Map<String, Object>> rules,
                                          final WorkflowContextSnapshot 
snapshot, final String databaseType) {
         boolean ruleExists = containsRule(rules, databaseType, 
request.getRuleName());
diff --git 
a/mcp/features/readwrite-splitting/src/main/java/org/apache/shardingsphere/mcp/feature/readwritesplitting/tool/service/ReadwriteSplittingRuleWorkflowValidationService.java
 
b/mcp/features/readwrite-splitting/src/main/java/org/apache/shardingsphere/mcp/feature/readwritesplitting/tool/service/ReadwriteSplittingRuleWorkflowValidationService.java
index 114b490a422..565013675ae 100644
--- 
a/mcp/features/readwrite-splitting/src/main/java/org/apache/shardingsphere/mcp/feature/readwritesplitting/tool/service/ReadwriteSplittingRuleWorkflowValidationService.java
+++ 
b/mcp/features/readwrite-splitting/src/main/java/org/apache/shardingsphere/mcp/feature/readwritesplitting/tool/service/ReadwriteSplittingRuleWorkflowValidationService.java
@@ -66,13 +66,7 @@ public final class 
ReadwriteSplittingRuleWorkflowValidationService implements MC
     public Map<String, Object> validate(final WorkflowSessionContext 
workflowSessionContext, final MCPMetadataQueryFacade metadataQueryFacade,
                                         final MCPFeatureQueryFacade 
queryFacade, final MCPFeatureExecutionFacade executionFacade, final String 
sessionId,
                                         final WorkflowContextSnapshot 
snapshot) {
-        Map<String, Object> rejectedResponse = 
validationSupport.checkValidatePreconditions(sessionId, snapshot);
-        if (!rejectedResponse.isEmpty()) {
-            return rejectedResponse;
-        }
-        ValidationReport validationReport = createValidationReport(snapshot, 
queryFacade);
-        snapshot.setValidationReport(validationReport);
-        return validationSupport.finalizeValidation(workflowSessionContext, 
snapshot, validationReport);
+        return validationSupport.validateAndFinalize(workflowSessionContext, 
sessionId, snapshot, () -> createValidationReport(snapshot, queryFacade));
     }
     
     @Override
diff --git 
a/mcp/features/readwrite-splitting/src/main/java/org/apache/shardingsphere/mcp/feature/readwritesplitting/tool/service/ReadwriteSplittingStatusWorkflowPlanningService.java
 
b/mcp/features/readwrite-splitting/src/main/java/org/apache/shardingsphere/mcp/feature/readwritesplitting/tool/service/ReadwriteSplittingStatusWorkflowPlanningService.java
index 47efe6a6772..4b7816cbbbc 100644
--- 
a/mcp/features/readwrite-splitting/src/main/java/org/apache/shardingsphere/mcp/feature/readwritesplitting/tool/service/ReadwriteSplittingStatusWorkflowPlanningService.java
+++ 
b/mcp/features/readwrite-splitting/src/main/java/org/apache/shardingsphere/mcp/feature/readwritesplitting/tool/service/ReadwriteSplittingStatusWorkflowPlanningService.java
@@ -117,8 +117,9 @@ public final class 
ReadwriteSplittingStatusWorkflowPlanningService {
             snapshot.setStatus(WorkflowLifecycle.STATUS_CLARIFYING);
             return false;
         }
-        if (!ensureSupportedIdentifier("database", request.getDatabase(), 
snapshot) || 
!ensureSupportedIdentifier(ReadwriteSplittingFeatureDefinition.RULE_FIELD, 
request.getRuleName(), snapshot)
-                || 
!ensureSupportedIdentifier(ReadwriteSplittingFeatureDefinition.STORAGE_UNIT_FIELD,
 request.getStorageUnit(), snapshot)) {
+        if (!planningSupport.ensureOptionalSupportedIdentifiers("database", 
List.of(request.getDatabase()), snapshot, "intaking")
+                || 
!planningSupport.ensureOptionalSupportedIdentifiers(ReadwriteSplittingFeatureDefinition.RULE_FIELD,
 List.of(request.getRuleName()), snapshot, "intaking")
+                || 
!planningSupport.ensureOptionalSupportedIdentifiers(ReadwriteSplittingFeatureDefinition.STORAGE_UNIT_FIELD,
 List.of(request.getStorageUnit()), snapshot, "intaking")) {
             snapshot.setStatus(WorkflowLifecycle.STATUS_FAILED);
             return false;
         }
@@ -143,16 +144,6 @@ public final class 
ReadwriteSplittingStatusWorkflowPlanningService {
         }
     }
     
-    private boolean ensureSupportedIdentifier(final String fieldName, final 
String identifier, final WorkflowContextSnapshot snapshot) {
-        if (identifier.isEmpty() || 
WorkflowSQLUtils.isSupportedIdentifier(identifier)) {
-            return true;
-        }
-        snapshot.getIssues().add(new 
WorkflowIssue(WorkflowIssueCode.UNSUPPORTED_IDENTIFIER, "error", "intaking",
-                String.format("%s identifier `%s` contains unsupported 
characters.", fieldName, identifier),
-                "Use a reviewable logical identifier without NUL or line 
terminators.", false, Map.of("field", fieldName, "identifier", identifier)));
-        return false;
-    }
-    
     private boolean ensureTargetStatusRow(final 
ReadwriteSplittingStatusWorkflowRequest request, final List<Map<String, 
Object>> statuses,
                                           final WorkflowContextSnapshot 
snapshot, final String databaseType) {
         if (statuses.stream().anyMatch(each -> matchesStatusTarget(request, 
each, databaseType))) {
diff --git 
a/mcp/features/readwrite-splitting/src/main/java/org/apache/shardingsphere/mcp/feature/readwritesplitting/tool/service/ReadwriteSplittingStatusWorkflowValidationService.java
 
b/mcp/features/readwrite-splitting/src/main/java/org/apache/shardingsphere/mcp/feature/readwritesplitting/tool/service/ReadwriteSplittingStatusWorkflowValidationService.java
index 70ea8adcbb4..42f7b8a3724 100644
--- 
a/mcp/features/readwrite-splitting/src/main/java/org/apache/shardingsphere/mcp/feature/readwritesplitting/tool/service/ReadwriteSplittingStatusWorkflowValidationService.java
+++ 
b/mcp/features/readwrite-splitting/src/main/java/org/apache/shardingsphere/mcp/feature/readwritesplitting/tool/service/ReadwriteSplittingStatusWorkflowValidationService.java
@@ -59,13 +59,7 @@ public final class 
ReadwriteSplittingStatusWorkflowValidationService implements
     public Map<String, Object> validate(final WorkflowSessionContext 
workflowSessionContext, final MCPMetadataQueryFacade metadataQueryFacade,
                                         final MCPFeatureQueryFacade 
queryFacade, final MCPFeatureExecutionFacade executionFacade, final String 
sessionId,
                                         final WorkflowContextSnapshot 
snapshot) {
-        Map<String, Object> rejectedResponse = 
validationSupport.checkValidatePreconditions(sessionId, snapshot);
-        if (!rejectedResponse.isEmpty()) {
-            return rejectedResponse;
-        }
-        ValidationReport validationReport = createValidationReport(snapshot, 
queryFacade);
-        snapshot.setValidationReport(validationReport);
-        return validationSupport.finalizeValidation(workflowSessionContext, 
snapshot, validationReport);
+        return validationSupport.validateAndFinalize(workflowSessionContext, 
sessionId, snapshot, () -> createValidationReport(snapshot, queryFacade));
     }
     
     @Override
diff --git 
a/mcp/features/shadow/src/main/java/org/apache/shardingsphere/mcp/feature/shadow/tool/service/ShadowWorkflowPlanningService.java
 
b/mcp/features/shadow/src/main/java/org/apache/shardingsphere/mcp/feature/shadow/tool/service/ShadowWorkflowPlanningService.java
index 3eca34c9806..48dbc6c3810 100644
--- 
a/mcp/features/shadow/src/main/java/org/apache/shardingsphere/mcp/feature/shadow/tool/service/ShadowWorkflowPlanningService.java
+++ 
b/mcp/features/shadow/src/main/java/org/apache/shardingsphere/mcp/feature/shadow/tool/service/ShadowWorkflowPlanningService.java
@@ -73,6 +73,8 @@ public final class ShadowWorkflowPlanningService {
     
     private static final List<String> VALIDATION_LAYERS = List.of("rules", 
"algorithms");
     
+    private static final String ALGORITHM_ARTIFACT_READY_MESSAGE = "Please use 
a shadow algorithm visible in the current Proxy and provide required 
properties.";
+    
     private final WorkflowPlanningSupport planningSupport = new 
WorkflowPlanningSupport();
     
     private final ShadowInspectionService inspectionService;
@@ -112,15 +114,11 @@ public final class ShadowWorkflowPlanningService {
         ShadowRuleWorkflowRequest mergedRequest = prepareSnapshot(result, 
request, ShadowFeatureDefinition.RULE_WORKFLOW_KIND,
                 resolveIntent(request, "create"), "Shadow rule workflow 
plan.", RULE_INTERACTION_STEPS);
         planningSupport.applyResolvedIntent(mergedRequest, 
result.getClarifiedIntent());
-        if 
(!WorkflowLifecycle.OPERATION_DROP.equalsIgnoreCase(result.getClarifiedIntent().getOperationType())
 && !mergedRequest.getDatabase().isEmpty()) {
-            planAlgorithms(queryFacade, mergedRequest, result);
-        }
+        planAlgorithmsIfRequired(queryFacade, mergedRequest, result);
         if (!ensureRulePlanningContext(mergedRequest, 
result.getClarifiedIntent(), result)) {
             return workflowSessionContext.persist(result, 
WorkflowLifecycle.STEP_CLARIFYING, result.getStatus());
         }
-        if 
(!WorkflowLifecycle.OPERATION_DROP.equalsIgnoreCase(result.getClarifiedIntent().getOperationType())
-                && !planningSupport.isReadyForArtifactPlanning(mergedRequest, 
result.getClarifiedIntent(), result, findPropertyRequirements(mergedRequest),
-                        "Please use a shadow algorithm visible in the current 
Proxy and provide required properties.")) {
+        if (!isReadyForAlgorithmArtifactPlanning(mergedRequest, result)) {
             return workflowSessionContext.persist(result, 
WorkflowLifecycle.STEP_CLARIFYING, WorkflowLifecycle.STATUS_CLARIFYING);
         }
         String databaseType = 
queryFacade.getDatabaseType(mergedRequest.getDatabase());
@@ -146,15 +144,11 @@ public final class ShadowWorkflowPlanningService {
         ShadowDefaultAlgorithmWorkflowRequest mergedRequest = 
prepareSnapshot(result, request, 
ShadowFeatureDefinition.DEFAULT_ALGORITHM_WORKFLOW_KIND,
                 resolveIntent(request, "create"), "Default shadow algorithm 
workflow plan.", DEFAULT_ALGORITHM_INTERACTION_STEPS);
         planningSupport.applyResolvedIntent(mergedRequest, 
result.getClarifiedIntent());
-        if 
(!WorkflowLifecycle.OPERATION_DROP.equalsIgnoreCase(result.getClarifiedIntent().getOperationType())
 && !mergedRequest.getDatabase().isEmpty()) {
-            planAlgorithms(queryFacade, mergedRequest, result);
-        }
+        planAlgorithmsIfRequired(queryFacade, mergedRequest, result);
         if (!ensureDefaultAlgorithmPlanningContext(mergedRequest, 
result.getClarifiedIntent(), result)) {
             return workflowSessionContext.persist(result, 
WorkflowLifecycle.STEP_CLARIFYING, result.getStatus());
         }
-        if 
(!WorkflowLifecycle.OPERATION_DROP.equalsIgnoreCase(result.getClarifiedIntent().getOperationType())
-                && !planningSupport.isReadyForArtifactPlanning(mergedRequest, 
result.getClarifiedIntent(), result, findPropertyRequirements(mergedRequest),
-                        "Please use a shadow algorithm visible in the current 
Proxy and provide required properties.")) {
+        if (!isReadyForAlgorithmArtifactPlanning(mergedRequest, result)) {
             return workflowSessionContext.persist(result, 
WorkflowLifecycle.STEP_CLARIFYING, WorkflowLifecycle.STATUS_CLARIFYING);
         }
         boolean exists = !inspectionService.queryDefaultAlgorithm(queryFacade, 
mergedRequest.getDatabase()).isEmpty();
@@ -270,16 +264,26 @@ public final class ShadowWorkflowPlanningService {
     }
     
     private boolean ensureSupportedIdentifiers(final WorkflowContextSnapshot 
snapshot, final String... identifiers) {
-        for (String each : identifiers) {
-            if (!each.isEmpty() && 
!WorkflowSQLUtils.isSupportedIdentifier(each)) {
-                snapshot.getIssues().add(new 
WorkflowIssue(WorkflowIssueCode.UNSUPPORTED_IDENTIFIER, "error", "intaking",
-                        String.format("Identifier `%s` contains unsupported 
characters.", each),
-                        "Use reviewable logical identifiers without NUL or 
line terminators.", false, Map.of("identifier", each)));
-                snapshot.setStatus(WorkflowLifecycle.STATUS_FAILED);
-                return false;
-            }
+        if (planningSupport.ensureOptionalSupportedIdentifiers("", 
List.of(identifiers), snapshot, "intaking")) {
+            return true;
         }
-        return true;
+        snapshot.setStatus(WorkflowLifecycle.STATUS_FAILED);
+        return false;
+    }
+    
+    private void planAlgorithmsIfRequired(final MCPFeatureQueryFacade 
queryFacade, final WorkflowRequest request, final WorkflowContextSnapshot 
snapshot) {
+        if (!isDropWorkflow(snapshot.getClarifiedIntent()) && 
!request.getDatabase().isEmpty()) {
+            planAlgorithms(queryFacade, request, snapshot);
+        }
+    }
+    
+    private boolean isReadyForAlgorithmArtifactPlanning(final WorkflowRequest 
request, final WorkflowContextSnapshot snapshot) {
+        return isDropWorkflow(snapshot.getClarifiedIntent())
+                || planningSupport.isReadyForArtifactPlanning(request, 
snapshot.getClarifiedIntent(), snapshot, findPropertyRequirements(request), 
ALGORITHM_ARTIFACT_READY_MESSAGE);
+    }
+    
+    private boolean isDropWorkflow(final ClarifiedIntent clarifiedIntent) {
+        return 
WorkflowLifecycle.OPERATION_DROP.equalsIgnoreCase(clarifiedIntent.getOperationType());
     }
     
     private void addMissingInput(final Collection<String> missingInputs, final 
String value, final String fieldName) {
diff --git 
a/mcp/features/shadow/src/main/java/org/apache/shardingsphere/mcp/feature/shadow/tool/service/ShadowWorkflowValidationService.java
 
b/mcp/features/shadow/src/main/java/org/apache/shardingsphere/mcp/feature/shadow/tool/service/ShadowWorkflowValidationService.java
index 21f392c44da..59da9445489 100644
--- 
a/mcp/features/shadow/src/main/java/org/apache/shardingsphere/mcp/feature/shadow/tool/service/ShadowWorkflowValidationService.java
+++ 
b/mcp/features/shadow/src/main/java/org/apache/shardingsphere/mcp/feature/shadow/tool/service/ShadowWorkflowValidationService.java
@@ -67,13 +67,7 @@ public final class ShadowWorkflowValidationService 
implements MCPWorkflowRuntime
     public Map<String, Object> validate(final WorkflowSessionContext 
workflowSessionContext, final MCPMetadataQueryFacade metadataQueryFacade,
                                         final MCPFeatureQueryFacade 
queryFacade, final MCPFeatureExecutionFacade executionFacade, final String 
sessionId,
                                         final WorkflowContextSnapshot 
snapshot) {
-        Map<String, Object> rejectedResponse = 
validationSupport.checkValidatePreconditions(sessionId, snapshot);
-        if (!rejectedResponse.isEmpty()) {
-            return rejectedResponse;
-        }
-        ValidationReport validationReport = createValidationReport(snapshot, 
queryFacade);
-        snapshot.setValidationReport(validationReport);
-        return validationSupport.finalizeValidation(workflowSessionContext, 
snapshot, validationReport);
+        return validationSupport.validateAndFinalize(workflowSessionContext, 
sessionId, snapshot, () -> createValidationReport(snapshot, queryFacade));
     }
     
     @Override
diff --git 
a/mcp/features/sharding/src/main/java/org/apache/shardingsphere/mcp/feature/sharding/tool/service/ShardingWorkflowValidationService.java
 
b/mcp/features/sharding/src/main/java/org/apache/shardingsphere/mcp/feature/sharding/tool/service/ShardingWorkflowValidationService.java
index 0425f2441d0..80134654573 100644
--- 
a/mcp/features/sharding/src/main/java/org/apache/shardingsphere/mcp/feature/sharding/tool/service/ShardingWorkflowValidationService.java
+++ 
b/mcp/features/sharding/src/main/java/org/apache/shardingsphere/mcp/feature/sharding/tool/service/ShardingWorkflowValidationService.java
@@ -68,13 +68,7 @@ public final class ShardingWorkflowValidationService 
implements MCPWorkflowRunti
     public Map<String, Object> validate(final WorkflowSessionContext 
workflowSessionContext, final MCPMetadataQueryFacade metadataQueryFacade,
                                         final MCPFeatureQueryFacade 
queryFacade, final MCPFeatureExecutionFacade executionFacade, final String 
sessionId,
                                         final WorkflowContextSnapshot 
snapshot) {
-        Map<String, Object> rejectedResponse = 
validationSupport.checkValidatePreconditions(sessionId, snapshot);
-        if (!rejectedResponse.isEmpty()) {
-            return rejectedResponse;
-        }
-        ValidationReport validationReport = createValidationReport(snapshot, 
queryFacade);
-        snapshot.setValidationReport(validationReport);
-        return validationSupport.finalizeValidation(workflowSessionContext, 
snapshot, validationReport);
+        return validationSupport.validateAndFinalize(workflowSessionContext, 
sessionId, snapshot, () -> createValidationReport(snapshot, queryFacade));
     }
     
     @Override
diff --git 
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowDistSQLQueryUtils.java
 
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowDistSQLQueryUtils.java
index dfb970321da..d0e6aceac32 100644
--- 
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowDistSQLQueryUtils.java
+++ 
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowDistSQLQueryUtils.java
@@ -18,9 +18,12 @@
 package org.apache.shardingsphere.mcp.support.workflow.service;
 
 import 
org.apache.shardingsphere.mcp.api.protocol.exception.MCPQueryFailedException;
+import 
org.apache.shardingsphere.mcp.support.database.spi.MCPFeatureQueryFacade;
 
 import java.sql.SQLSyntaxErrorException;
+import java.util.List;
 import java.util.Locale;
+import java.util.Map;
 import java.util.Objects;
 
 /**
@@ -41,6 +44,25 @@ public final class WorkflowDistSQLQueryUtils {
         return hasSyntaxErrorCause(ex) || hasUnsupportedDistSQLMessage(ex);
     }
     
+    /**
+     * Query DistSQL rule rows, returning an empty list when the current 
backend does not support the rule DistSQL syntax.
+     *
+     * @param queryFacade query facade
+     * @param databaseName database name
+     * @param sql DistSQL to execute
+     * @return queried rows
+     */
+    public static List<Map<String, Object>> queryRuleRows(final 
MCPFeatureQueryFacade queryFacade, final String databaseName, final String sql) 
{
+        try {
+            return queryFacade.query(databaseName, "", sql);
+        } catch (final MCPQueryFailedException ex) {
+            if (isUnsupportedDistSQLQueryFailure(ex)) {
+                return List.of();
+            }
+            throw ex;
+        }
+    }
+    
     private static boolean hasSyntaxErrorCause(final Throwable throwable) {
         Throwable current = throwable;
         while (null != current) {
diff --git 
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowGuidanceResourceHintProvider.java
 
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowGuidanceResourceHintProvider.java
index be9bea56ff5..3e3541bce56 100644
--- 
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowGuidanceResourceHintProvider.java
+++ 
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowGuidanceResourceHintProvider.java
@@ -40,6 +40,104 @@ import java.util.Optional;
  */
 public final class WorkflowGuidanceResourceHintProvider {
     
+    private static final Map<String, List<ResourceHintTemplate>> 
FEATURE_RESOURCE_HINTS = Map.ofEntries(
+            Map.entry(WorkflowKindDescriptors.ENCRYPT_RULE, List.of(new 
ResourceHintTemplate("shardingsphere://features/encrypt/algorithms", 
"algorithm", "read_first",
+                    "Read encrypt algorithm metadata before choosing algorithm 
arguments."))),
+            Map.entry(WorkflowKindDescriptors.MASK_RULE, List.of(new 
ResourceHintTemplate("shardingsphere://features/mask/algorithms", "algorithm", 
"read_first",
+                    "Read mask algorithm metadata before choosing algorithm 
arguments."))),
+            Map.entry(WorkflowKindDescriptors.READWRITE_RULE,
+                    List.of(new 
ResourceHintTemplate("shardingsphere://features/readwrite-splitting/load-balance-algorithm-plugins",
 "algorithm", "read_first",
+                            "Read load-balance algorithm plugin metadata 
before choosing algorithm arguments."))),
+            Map.entry(WorkflowKindDescriptors.SHADOW_RULE, List.of(new 
ResourceHintTemplate("shardingsphere://features/shadow/algorithm-plugins", 
"algorithm", "read_first",
+                    "Read shadow algorithm plugin metadata before choosing 
algorithm arguments."))),
+            Map.entry(WorkflowKindDescriptors.SHADOW_DEFAULT_ALGORITHM,
+                    List.of(new 
ResourceHintTemplate("shardingsphere://features/shadow/algorithm-plugins", 
"algorithm", "read_first",
+                            "Read shadow algorithm plugin metadata before 
choosing algorithm arguments."))),
+            Map.entry(WorkflowKindDescriptors.SHARDING_TABLE_RULE,
+                    List.of(new 
ResourceHintTemplate("shardingsphere://features/sharding/algorithm-plugins", 
"algorithm", "read_first",
+                            "Read sharding algorithm plugin metadata before 
choosing algorithm arguments."))),
+            Map.entry(WorkflowKindDescriptors.SHARDING_DEFAULT_STRATEGY,
+                    List.of(new 
ResourceHintTemplate("shardingsphere://features/sharding/algorithm-plugins", 
"algorithm", "read_first",
+                            "Read sharding algorithm plugin metadata before 
choosing algorithm arguments."))),
+            Map.entry(WorkflowKindDescriptors.SHARDING_KEY_GENERATOR,
+                    List.of(new 
ResourceHintTemplate("shardingsphere://features/sharding/key-generate-algorithm-plugins",
 "algorithm", "read_first",
+                            "Read key-generate algorithm plugin metadata 
before choosing generator arguments."))),
+            Map.entry(WorkflowKindDescriptors.SHARDING_KEY_GENERATE_STRATEGY,
+                    List.of(new 
ResourceHintTemplate("shardingsphere://features/sharding/key-generate-algorithm-plugins",
 "algorithm", "read_first",
+                            "Read key-generate algorithm plugin metadata 
before choosing generator arguments."))));
+    
+    private static final Collection<String> STORAGE_UNIT_WORKFLOW_KINDS = 
List.of(
+            WorkflowKindDescriptors.READWRITE_RULE, 
WorkflowKindDescriptors.READWRITE_STATUS, WorkflowKindDescriptors.SHADOW_RULE, 
WorkflowKindDescriptors.SHARDING_TABLE_RULE);
+    
+    private static final Collection<String> SINGLE_TABLE_WORKFLOW_KINDS = 
List.of(WorkflowKindDescriptors.SHADOW_RULE, 
WorkflowKindDescriptors.SHARDING_TABLE_RULE);
+    
+    private static final Map<String, List<TableResourceHintTemplate>> 
FEATURE_TABLE_RULE_HINTS = Map.of(
+            WorkflowKindDescriptors.ENCRYPT_RULE, List.of(new 
TableResourceHintTemplate("shardingsphere://features/encrypt/databases/%s/tables/%s/rules",
+                    "Inspect current encrypt table rule DistSQL state before 
planning changes.")),
+            WorkflowKindDescriptors.MASK_RULE, List.of(new 
TableResourceHintTemplate("shardingsphere://features/mask/databases/%s/tables/%s/rules",
+                    "Inspect current mask table rule DistSQL state before 
planning changes.")),
+            WorkflowKindDescriptors.SHADOW_RULE, List.of(new 
TableResourceHintTemplate("shardingsphere://features/shadow/databases/%s/tables/%s/rules",
+                    "Inspect current shadow table rule DistSQL state before 
planning changes.")),
+            WorkflowKindDescriptors.SHARDING_TABLE_RULE, List.of(
+                    new 
TableResourceHintTemplate("shardingsphere://features/sharding/databases/%s/tables/%s/table-rule",
+                            "Inspect current sharding table rule DistSQL state 
before planning changes."),
+                    new 
TableResourceHintTemplate("shardingsphere://features/sharding/databases/%s/tables/%s/nodes",
+                            "Inspect current sharding table nodes before 
planning changes.")));
+    
+    private static final Map<String, List<DatabaseResourceHintTemplate>> 
RULE_RESOURCE_HINTS = Map.ofEntries(
+            Map.entry(WorkflowKindDescriptors.ENCRYPT_RULE, List.of(new 
DatabaseResourceHintTemplate("shardingsphere://features/encrypt/databases/%s/rules",
+                    "Inspect current encrypt rules before planning 
changes."))),
+            Map.entry(WorkflowKindDescriptors.MASK_RULE, List.of(new 
DatabaseResourceHintTemplate("shardingsphere://features/mask/databases/%s/rules",
+                    "Inspect current mask rules before planning changes."))),
+            Map.entry(WorkflowKindDescriptors.BROADCAST_RULE, List.of(new 
DatabaseResourceHintTemplate("shardingsphere://features/broadcast/databases/%s/rules",
+                    "Inspect current broadcast rules before planning 
changes."))),
+            Map.entry(WorkflowKindDescriptors.READWRITE_RULE,
+                    List.of(new 
DatabaseResourceHintTemplate("shardingsphere://features/readwrite-splitting/databases/%s/rules",
+                            "Inspect current readwrite-splitting rules before 
planning changes."))),
+            Map.entry(WorkflowKindDescriptors.READWRITE_STATUS,
+                    List.of(new 
DatabaseResourceHintTemplate("shardingsphere://features/readwrite-splitting/databases/%s/status",
+                            "Inspect current readwrite-splitting status before 
planning changes."))),
+            Map.entry(WorkflowKindDescriptors.SHADOW_RULE, List.of(new 
DatabaseResourceHintTemplate("shardingsphere://features/shadow/databases/%s/rules",
+                    "Inspect current shadow rules before planning changes."))),
+            Map.entry(WorkflowKindDescriptors.SHADOW_DEFAULT_ALGORITHM,
+                    List.of(new 
DatabaseResourceHintTemplate("shardingsphere://features/shadow/databases/%s/default-algorithm",
+                            "Inspect current default shadow algorithm before 
planning changes."))),
+            Map.entry(WorkflowKindDescriptors.SHADOW_ALGORITHM_CLEANUP, 
List.of(
+                    new 
DatabaseResourceHintTemplate("shardingsphere://features/shadow/databases/%s/algorithms",
 "Inspect configured shadow algorithms before planning cleanup."),
+                    new 
DatabaseResourceHintTemplate("shardingsphere://features/shadow/databases/%s/table-rules",
 "Inspect shadow table rule references before planning cleanup."),
+                    new 
DatabaseResourceHintTemplate("shardingsphere://features/shadow/databases/%s/default-algorithm",
+                            "Inspect default shadow algorithm references 
before planning cleanup."))),
+            Map.entry(WorkflowKindDescriptors.SHARDING_TABLE_RULE, List.of(
+                    new 
DatabaseResourceHintTemplate("shardingsphere://features/sharding/databases/%s/table-rules",
 "Inspect current sharding table rules before planning changes."),
+                    new 
DatabaseResourceHintTemplate("shardingsphere://features/sharding/databases/%s/table-nodes",
 "Inspect current sharding table nodes before planning changes."))),
+            Map.entry(WorkflowKindDescriptors.SHARDING_TABLE_REFERENCE,
+                    List.of(new 
DatabaseResourceHintTemplate("shardingsphere://features/sharding/databases/%s/table-reference-rules",
+                            "Inspect current sharding table reference rules 
before planning changes."))),
+            Map.entry(WorkflowKindDescriptors.SHARDING_DEFAULT_STRATEGY, 
List.of(
+                    new 
DatabaseResourceHintTemplate("shardingsphere://features/sharding/databases/%s/default-strategy",
+                            "Inspect current default sharding strategy before 
planning changes."),
+                    new 
DatabaseResourceHintTemplate("shardingsphere://features/sharding/databases/%s/algorithms",
+                            "Inspect configured sharding algorithms before 
planning default strategy changes."))),
+            Map.entry(WorkflowKindDescriptors.SHARDING_KEY_GENERATOR,
+                    List.of(new 
DatabaseResourceHintTemplate("shardingsphere://features/sharding/databases/%s/key-generators",
+                            "Inspect current sharding key generators before 
planning changes."))),
+            Map.entry(WorkflowKindDescriptors.SHARDING_KEY_GENERATE_STRATEGY, 
List.of(
+                    new 
DatabaseResourceHintTemplate("shardingsphere://features/sharding/databases/%s/key-generate-strategies",
+                            "Inspect current sharding key generate strategies 
before planning changes."),
+                    new 
DatabaseResourceHintTemplate("shardingsphere://features/sharding/databases/%s/key-generators",
+                            "Inspect current sharding key generators before 
planning key generate strategy changes."))),
+            Map.entry(WorkflowKindDescriptors.SHARDING_COMPONENT_CLEANUP, 
List.of(
+                    new 
DatabaseResourceHintTemplate("shardingsphere://features/sharding/databases/%s/algorithms",
 "Inspect configured sharding algorithms before planning cleanup."),
+                    new 
DatabaseResourceHintTemplate("shardingsphere://features/sharding/databases/%s/key-generators",
+                            "Inspect configured sharding key generators before 
planning cleanup."),
+                    new 
DatabaseResourceHintTemplate("shardingsphere://features/sharding/databases/%s/auditors",
 "Inspect configured sharding auditors before planning cleanup."),
+                    new 
DatabaseResourceHintTemplate("shardingsphere://features/sharding/databases/%s/unused-algorithms",
+                            "Inspect unused sharding algorithms before 
planning cleanup."),
+                    new 
DatabaseResourceHintTemplate("shardingsphere://features/sharding/databases/%s/unused-key-generators",
+                            "Inspect unused sharding key generators before 
planning cleanup."),
+                    new 
DatabaseResourceHintTemplate("shardingsphere://features/sharding/databases/%s/unused-auditors",
+                            "Inspect unused sharding auditors before planning 
cleanup."))));
+    
     /**
      * Create resource hints for a workflow planning response.
      *
@@ -64,123 +162,33 @@ public final class WorkflowGuidanceResourceHintProvider {
     }
     
     private void addFeatureResources(final Collection<Map<String, Object>> 
resourcesToRead, final WorkflowContextSnapshot snapshot) {
-        switch (resolveWorkflowKind(snapshot)) {
-            case WorkflowKindDescriptors.ENCRYPT_RULE:
-                addResourceHint(resourcesToRead, 
"shardingsphere://features/encrypt/algorithms", "algorithm", "read_first",
-                        "Read encrypt algorithm metadata before choosing 
algorithm arguments.");
-                break;
-            case WorkflowKindDescriptors.MASK_RULE:
-                addResourceHint(resourcesToRead, 
"shardingsphere://features/mask/algorithms", "algorithm", "read_first",
-                        "Read mask algorithm metadata before choosing 
algorithm arguments.");
-                break;
-            case WorkflowKindDescriptors.READWRITE_RULE:
-                addResourceHint(resourcesToRead, 
"shardingsphere://features/readwrite-splitting/load-balance-algorithm-plugins", 
"algorithm", "read_first",
-                        "Read load-balance algorithm plugin metadata before 
choosing algorithm arguments.");
-                break;
-            case WorkflowKindDescriptors.SHADOW_RULE:
-            case WorkflowKindDescriptors.SHADOW_DEFAULT_ALGORITHM:
-                addResourceHint(resourcesToRead, 
"shardingsphere://features/shadow/algorithm-plugins", "algorithm", "read_first",
-                        "Read shadow algorithm plugin metadata before choosing 
algorithm arguments.");
-                break;
-            case WorkflowKindDescriptors.SHARDING_TABLE_RULE:
-            case WorkflowKindDescriptors.SHARDING_DEFAULT_STRATEGY:
-                addResourceHint(resourcesToRead, 
"shardingsphere://features/sharding/algorithm-plugins", "algorithm", 
"read_first",
-                        "Read sharding algorithm plugin metadata before 
choosing algorithm arguments.");
-                break;
-            case WorkflowKindDescriptors.SHARDING_KEY_GENERATOR:
-            case WorkflowKindDescriptors.SHARDING_KEY_GENERATE_STRATEGY:
-                addResourceHint(resourcesToRead, 
"shardingsphere://features/sharding/key-generate-algorithm-plugins", 
"algorithm", "read_first",
-                        "Read key-generate algorithm plugin metadata before 
choosing generator arguments.");
-                break;
-            default:
-                break;
+        for (ResourceHintTemplate each : 
FEATURE_RESOURCE_HINTS.getOrDefault(resolveWorkflowKind(snapshot), List.of())) {
+            addResourceHint(resourcesToRead, each.uri(), each.resourceKind(), 
each.action(), each.reason());
         }
     }
     
     private void addGovernanceMetadataResources(final Collection<Map<String, 
Object>> resourcesToRead, final WorkflowContextSnapshot snapshot, final 
WorkflowRequest request) {
         String workflowKind = resolveWorkflowKind(snapshot);
-        switch (workflowKind) {
-            case WorkflowKindDescriptors.READWRITE_RULE:
-            case WorkflowKindDescriptors.READWRITE_STATUS:
-            case WorkflowKindDescriptors.SHADOW_RULE:
-            case WorkflowKindDescriptors.SHARDING_TABLE_RULE:
-                addStorageUnitsResourceHint(resourcesToRead, request);
-                break;
-            default:
-                break;
+        if (STORAGE_UNIT_WORKFLOW_KINDS.contains(workflowKind)) {
+            addStorageUnitsResourceHint(resourcesToRead, request);
         }
-        switch (workflowKind) {
-            case WorkflowKindDescriptors.SHADOW_RULE:
-            case WorkflowKindDescriptors.SHARDING_TABLE_RULE:
-                addSingleTablesResourceHint(resourcesToRead, request);
-                if (!request.getTable().isEmpty()) {
-                    addSingleTableResourceHint(resourcesToRead, request);
-                }
-                break;
-            default:
-                break;
+        if (SINGLE_TABLE_WORKFLOW_KINDS.contains(workflowKind)) {
+            addSingleTablesResourceHint(resourcesToRead, request);
+            if (!request.getTable().isEmpty()) {
+                addSingleTableResourceHint(resourcesToRead, request);
+            }
         }
     }
     
     private void addFeatureTableRuleResources(final Collection<Map<String, 
Object>> resourcesToRead, final WorkflowContextSnapshot snapshot, final 
WorkflowRequest request) {
-        switch (resolveWorkflowKind(snapshot)) {
-            case WorkflowKindDescriptors.ENCRYPT_RULE:
-                addTableResourceHint(resourcesToRead, request, 
"shardingsphere://features/encrypt/databases/%s/tables/%s/rules",
-                        "Inspect current encrypt table rule DistSQL state 
before planning changes.");
-                break;
-            case WorkflowKindDescriptors.MASK_RULE:
-                addTableResourceHint(resourcesToRead, request, 
"shardingsphere://features/mask/databases/%s/tables/%s/rules",
-                        "Inspect current mask table rule DistSQL state before 
planning changes.");
-                break;
-            case WorkflowKindDescriptors.SHADOW_RULE:
-                addTableResourceHint(resourcesToRead, request, 
"shardingsphere://features/shadow/databases/%s/tables/%s/rules",
-                        "Inspect current shadow table rule DistSQL state 
before planning changes.");
-                break;
-            case WorkflowKindDescriptors.SHARDING_TABLE_RULE:
-                addTableResourceHint(resourcesToRead, request, 
"shardingsphere://features/sharding/databases/%s/tables/%s/table-rule",
-                        "Inspect current sharding table rule DistSQL state 
before planning changes.");
-                addTableResourceHint(resourcesToRead, request, 
"shardingsphere://features/sharding/databases/%s/tables/%s/nodes",
-                        "Inspect current sharding table nodes before planning 
changes.");
-                break;
-            default:
-                break;
+        for (TableResourceHintTemplate each : 
FEATURE_TABLE_RULE_HINTS.getOrDefault(resolveWorkflowKind(snapshot), 
List.of())) {
+            addTableResourceHint(resourcesToRead, request, each.uriTemplate(), 
each.reason());
         }
     }
     
     private void addRuleResources(final Collection<Map<String, Object>> 
resourcesToRead, final WorkflowContextSnapshot snapshot, final WorkflowRequest 
request) {
-        switch (resolveWorkflowKind(snapshot)) {
-            case WorkflowKindDescriptors.ENCRYPT_RULE -> 
addDatabaseResourceHint(resourcesToRead, request, 
"shardingsphere://features/encrypt/databases/%s/rules",
-                    "Inspect current encrypt rules before planning changes.");
-            case WorkflowKindDescriptors.MASK_RULE -> 
addDatabaseResourceHint(resourcesToRead, request, 
"shardingsphere://features/mask/databases/%s/rules",
-                    "Inspect current mask rules before planning changes.");
-            case WorkflowKindDescriptors.BROADCAST_RULE -> 
addDatabaseResourceHint(resourcesToRead, request, 
"shardingsphere://features/broadcast/databases/%s/rules",
-                    "Inspect current broadcast rules before planning 
changes.");
-            case WorkflowKindDescriptors.READWRITE_RULE -> 
addDatabaseResourceHint(resourcesToRead, request, 
"shardingsphere://features/readwrite-splitting/databases/%s/rules",
-                    "Inspect current readwrite-splitting rules before planning 
changes.");
-            case WorkflowKindDescriptors.READWRITE_STATUS -> 
addDatabaseResourceHint(resourcesToRead, request, 
"shardingsphere://features/readwrite-splitting/databases/%s/status",
-                    "Inspect current readwrite-splitting status before 
planning changes.");
-            case WorkflowKindDescriptors.SHADOW_RULE -> 
addDatabaseResourceHint(resourcesToRead, request, 
"shardingsphere://features/shadow/databases/%s/rules",
-                    "Inspect current shadow rules before planning changes.");
-            case WorkflowKindDescriptors.SHADOW_DEFAULT_ALGORITHM -> 
addDatabaseResourceHint(resourcesToRead, request, 
"shardingsphere://features/shadow/databases/%s/default-algorithm",
-                    "Inspect current default shadow algorithm before planning 
changes.");
-            case WorkflowKindDescriptors.SHADOW_ALGORITHM_CLEANUP -> {
-                addDatabaseResourceHint(resourcesToRead, request, 
"shardingsphere://features/shadow/databases/%s/algorithms",
-                        "Inspect configured shadow algorithms before planning 
cleanup.");
-                addDatabaseResourceHint(resourcesToRead, request, 
"shardingsphere://features/shadow/databases/%s/table-rules",
-                        "Inspect shadow table rule references before planning 
cleanup.");
-                addDatabaseResourceHint(resourcesToRead, request, 
"shardingsphere://features/shadow/databases/%s/default-algorithm",
-                        "Inspect default shadow algorithm references before 
planning cleanup.");
-            }
-            case WorkflowKindDescriptors.SHARDING_TABLE_RULE -> 
addShardingTableRuleResources(resourcesToRead, request);
-            case WorkflowKindDescriptors.SHARDING_TABLE_REFERENCE -> 
addDatabaseResourceHint(resourcesToRead, request, 
"shardingsphere://features/sharding/databases/%s/table-reference-rules",
-                    "Inspect current sharding table reference rules before 
planning changes.");
-            case WorkflowKindDescriptors.SHARDING_DEFAULT_STRATEGY -> 
addShardingDefaultStrategyResources(resourcesToRead, request);
-            case WorkflowKindDescriptors.SHARDING_KEY_GENERATOR -> 
addDatabaseResourceHint(resourcesToRead, request, 
"shardingsphere://features/sharding/databases/%s/key-generators",
-                    "Inspect current sharding key generators before planning 
changes.");
-            case WorkflowKindDescriptors.SHARDING_KEY_GENERATE_STRATEGY -> 
addShardingKeyGenerateStrategyResources(resourcesToRead, request);
-            case WorkflowKindDescriptors.SHARDING_COMPONENT_CLEANUP -> 
addShardingComponentCleanupResources(resourcesToRead, request);
-            default -> {
-            }
+        for (DatabaseResourceHintTemplate each : 
RULE_RESOURCE_HINTS.getOrDefault(resolveWorkflowKind(snapshot), List.of())) {
+            addDatabaseResourceHint(resourcesToRead, request, 
each.uriTemplate(), each.reason());
         }
     }
     
@@ -260,42 +268,6 @@ public final class WorkflowGuidanceResourceHintProvider {
         resourcesToRead.add(MCPResourceHintUtils.create(uri, resourceKind, 
action, reason, MCPPayloadFieldNames.RESOURCES_TO_READ));
     }
     
-    private void addShardingTableRuleResources(final Collection<Map<String, 
Object>> resourcesToRead, final WorkflowRequest request) {
-        addDatabaseResourceHint(resourcesToRead, request, 
"shardingsphere://features/sharding/databases/%s/table-rules",
-                "Inspect current sharding table rules before planning 
changes.");
-        addDatabaseResourceHint(resourcesToRead, request, 
"shardingsphere://features/sharding/databases/%s/table-nodes",
-                "Inspect current sharding table nodes before planning 
changes.");
-    }
-    
-    private void addShardingDefaultStrategyResources(final 
Collection<Map<String, Object>> resourcesToRead, final WorkflowRequest request) 
{
-        addDatabaseResourceHint(resourcesToRead, request, 
"shardingsphere://features/sharding/databases/%s/default-strategy",
-                "Inspect current default sharding strategy before planning 
changes.");
-        addDatabaseResourceHint(resourcesToRead, request, 
"shardingsphere://features/sharding/databases/%s/algorithms",
-                "Inspect configured sharding algorithms before planning 
default strategy changes.");
-    }
-    
-    private void addShardingKeyGenerateStrategyResources(final 
Collection<Map<String, Object>> resourcesToRead, final WorkflowRequest request) 
{
-        addDatabaseResourceHint(resourcesToRead, request, 
"shardingsphere://features/sharding/databases/%s/key-generate-strategies",
-                "Inspect current sharding key generate strategies before 
planning changes.");
-        addDatabaseResourceHint(resourcesToRead, request, 
"shardingsphere://features/sharding/databases/%s/key-generators",
-                "Inspect current sharding key generators before planning key 
generate strategy changes.");
-    }
-    
-    private void addShardingComponentCleanupResources(final 
Collection<Map<String, Object>> resourcesToRead, final WorkflowRequest request) 
{
-        addDatabaseResourceHint(resourcesToRead, request, 
"shardingsphere://features/sharding/databases/%s/algorithms",
-                "Inspect configured sharding algorithms before planning 
cleanup.");
-        addDatabaseResourceHint(resourcesToRead, request, 
"shardingsphere://features/sharding/databases/%s/key-generators",
-                "Inspect configured sharding key generators before planning 
cleanup.");
-        addDatabaseResourceHint(resourcesToRead, request, 
"shardingsphere://features/sharding/databases/%s/auditors",
-                "Inspect configured sharding auditors before planning 
cleanup.");
-        addDatabaseResourceHint(resourcesToRead, request, 
"shardingsphere://features/sharding/databases/%s/unused-algorithms",
-                "Inspect unused sharding algorithms before planning cleanup.");
-        addDatabaseResourceHint(resourcesToRead, request, 
"shardingsphere://features/sharding/databases/%s/unused-key-generators",
-                "Inspect unused sharding key generators before planning 
cleanup.");
-        addDatabaseResourceHint(resourcesToRead, request, 
"shardingsphere://features/sharding/databases/%s/unused-auditors",
-                "Inspect unused sharding auditors before planning cleanup.");
-    }
-    
     private void addTableResources(final Collection<Map<String, Object>> 
resourcesToRead, final WorkflowRequest request) {
         
resourcesToRead.add(MCPResourceHintUtils.create(String.format("shardingsphere://databases/%s/schemas/%s/tables/%s/columns",
 MCPUriPathSegmentUtils.encodePathSegment(request.getDatabase()),
                 MCPUriPathSegmentUtils.encodePathSegment(request.getSchema()), 
MCPUriPathSegmentUtils.encodePathSegment(request.getTable())),
@@ -306,4 +278,13 @@ public final class WorkflowGuidanceResourceHintProvider {
         WorkflowKind workflowKind = snapshot.getWorkflowKind();
         return null == workflowKind ? "" : workflowKind.getValue();
     }
+    
+    private record ResourceHintTemplate(String uri, String resourceKind, 
String action, String reason) {
+    }
+    
+    private record DatabaseResourceHintTemplate(String uriTemplate, String 
reason) {
+    }
+    
+    private record TableResourceHintTemplate(String uriTemplate, String 
reason) {
+    }
 }
diff --git 
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowIntentResolverSupport.java
 
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowIntentResolverSupport.java
index fa80cbd2f8a..e29ea134928 100644
--- 
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowIntentResolverSupport.java
+++ 
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowIntentResolverSupport.java
@@ -24,6 +24,8 @@ import 
org.apache.shardingsphere.mcp.support.workflow.model.WorkflowFieldNames;
 import org.apache.shardingsphere.mcp.support.workflow.model.WorkflowLifecycle;
 import org.apache.shardingsphere.mcp.support.workflow.model.WorkflowRequest;
 
+import java.util.Collection;
+import java.util.List;
 import java.util.Locale;
 
 /**
@@ -34,6 +36,17 @@ public final class WorkflowIntentResolverSupport {
     
     private static final String EXECUTION_MODE_MANUAL_ONLY = "manual-only";
     
+    private static final List<FieldSemanticRule> FIELD_SEMANTIC_RULES = 
List.of(
+            new FieldSemanticRule("phone", List.of("phone number", "phone", 
"mobile", "tel", "手机号", "手机", "电话号码", "电话"), List.of("phone", "mobile", "tel")),
+            new FieldSemanticRule("id_card", List.of("identity card", "id 
card", "身份证", "证件"), List.of("id_card")),
+            new FieldSemanticRule("email", List.of("email", "邮箱", "邮件"), 
List.of("email")),
+            new FieldSemanticRule("bank_card", List.of("bank card", "银行卡", 
"银行卡号", "卡号"), List.of("bank_card", "card_no", "card_number")),
+            new FieldSemanticRule("passport", List.of("passport", "护照"), 
List.of("passport")),
+            new FieldSemanticRule("license_plate", List.of("license plate", 
"车牌", "车牌号"), List.of("license_plate", "plate_no", "plate_number")),
+            new FieldSemanticRule("birth_date", List.of("birth date", 
"birthday", "出生日期", "生日"), List.of("birth_date", "birthday", "date_of_birth")),
+            new FieldSemanticRule("address", List.of("address", "地址", "住址"), 
List.of("address", "addr")),
+            new FieldSemanticRule("name", List.of("real name", "full name", 
"姓名", "名字"), List.of("real_name", "full_name", "name")));
+    
     /**
      * Resolve workflow operation type from explicit fields and heuristics.
      *
@@ -90,37 +103,10 @@ public final class WorkflowIntentResolverSupport {
         }
         String naturalLanguageIntent = getNaturalLanguageIntent(request);
         String columnName = request.getColumn().toLowerCase(Locale.ENGLISH);
-        if (containsAny(naturalLanguageIntent, "phone number", "phone", 
"mobile", "tel", "手机号", "手机", "电话号码", "电话")
-                || containsAny(columnName, "phone", "mobile", "tel")) {
-            return recordInferredValue(clarifiedIntent, 
WorkflowFieldNames.FIELD_SEMANTICS, "phone");
-        }
-        if (containsAny(naturalLanguageIntent, "identity card", "id card", 
"身份证", "证件") || columnName.contains("id_card")) {
-            return recordInferredValue(clarifiedIntent, 
WorkflowFieldNames.FIELD_SEMANTICS, "id_card");
-        }
-        if (containsAny(naturalLanguageIntent, "email", "邮箱", "邮件") || 
columnName.contains("email")) {
-            return recordInferredValue(clarifiedIntent, 
WorkflowFieldNames.FIELD_SEMANTICS, "email");
-        }
-        if (containsAny(naturalLanguageIntent, "bank card", "银行卡", "银行卡号", 
"卡号")
-                || containsAny(columnName, "bank_card", "card_no", 
"card_number")) {
-            return recordInferredValue(clarifiedIntent, 
WorkflowFieldNames.FIELD_SEMANTICS, "bank_card");
-        }
-        if (containsAny(naturalLanguageIntent, "passport", "护照") || 
columnName.contains("passport")) {
-            return recordInferredValue(clarifiedIntent, 
WorkflowFieldNames.FIELD_SEMANTICS, "passport");
-        }
-        if (containsAny(naturalLanguageIntent, "license plate", "车牌", "车牌号")
-                || containsAny(columnName, "license_plate", "plate_no", 
"plate_number")) {
-            return recordInferredValue(clarifiedIntent, 
WorkflowFieldNames.FIELD_SEMANTICS, "license_plate");
-        }
-        if (containsAny(naturalLanguageIntent, "birth date", "birthday", 
"出生日期", "生日")
-                || containsAny(columnName, "birth_date", "birthday", 
"date_of_birth")) {
-            return recordInferredValue(clarifiedIntent, 
WorkflowFieldNames.FIELD_SEMANTICS, "birth_date");
-        }
-        if (containsAny(naturalLanguageIntent, "address", "地址", "住址") || 
containsAny(columnName, "address", "addr")) {
-            return recordInferredValue(clarifiedIntent, 
WorkflowFieldNames.FIELD_SEMANTICS, "address");
-        }
-        if (containsAny(naturalLanguageIntent, "real name", "full name", "姓名", 
"名字")
-                || containsAny(columnName, "real_name", "full_name", "name")) {
-            return recordInferredValue(clarifiedIntent, 
WorkflowFieldNames.FIELD_SEMANTICS, "name");
+        for (FieldSemanticRule each : FIELD_SEMANTIC_RULES) {
+            if (each.matches(naturalLanguageIntent, columnName)) {
+                return recordInferredValue(clarifiedIntent, 
WorkflowFieldNames.FIELD_SEMANTICS, each.semantics());
+            }
         }
         return recordInferredValue(clarifiedIntent, 
WorkflowFieldNames.FIELD_SEMANTICS, columnName);
     }
@@ -170,10 +156,26 @@ public final class WorkflowIntentResolverSupport {
         return false;
     }
     
+    private static boolean containsAny(final String value, final 
Collection<String> candidates) {
+        for (String each : candidates) {
+            if (value.contains(each)) {
+                return true;
+            }
+        }
+        return false;
+    }
+    
     private static String recordInferredValue(final ClarifiedIntent 
clarifiedIntent, final String fieldName, final String value) {
         if (null != clarifiedIntent) {
             clarifiedIntent.getInferredValues().put(fieldName, value);
         }
         return value;
     }
+    
+    private record FieldSemanticRule(String semantics, Collection<String> 
intentKeywords, Collection<String> columnKeywords) {
+        
+        private boolean matches(final String naturalLanguageIntent, final 
String columnName) {
+            return containsAny(naturalLanguageIntent, intentKeywords) || 
containsAny(columnName, columnKeywords);
+        }
+    }
 }
diff --git 
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowPlanningSupport.java
 
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowPlanningSupport.java
index 6118cc8abb1..d6d2af27db4 100644
--- 
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowPlanningSupport.java
+++ 
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowPlanningSupport.java
@@ -32,6 +32,7 @@ import 
org.apache.shardingsphere.mcp.support.workflow.model.WorkflowLifecycle;
 import org.apache.shardingsphere.mcp.support.workflow.model.WorkflowKind;
 import org.apache.shardingsphere.mcp.support.workflow.model.WorkflowRequest;
 
+import java.util.Collection;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Locale;
@@ -170,6 +171,43 @@ public final class WorkflowPlanningSupport {
         return collectPropertyRequirements(request, clarifiedIntent, snapshot, 
propertyRequirements);
     }
     
+    /**
+     * Ensure workflow identifiers can be rendered into reviewable DistSQL.
+     *
+     * @param fieldName field name for issue details
+     * @param identifiers identifiers to check
+     * @param snapshot workflow snapshot
+     * @param issueStage issue stage
+     * @return whether all identifiers are supported
+     */
+    public boolean ensureSupportedIdentifiers(final String fieldName, final 
Collection<String> identifiers, final WorkflowContextSnapshot snapshot,
+                                              final String issueStage) {
+        return ensureIdentifiers(fieldName, identifiers, snapshot, issueStage, 
false);
+    }
+    
+    /**
+     * Ensure optional workflow identifiers can be rendered into reviewable 
DistSQL when present.
+     *
+     * @param fieldName field name for issue details
+     * @param identifiers identifiers to check
+     * @param snapshot workflow snapshot
+     * @param issueStage issue stage
+     * @return whether all present identifiers are supported
+     */
+    public boolean ensureOptionalSupportedIdentifiers(final String fieldName, 
final Collection<String> identifiers, final WorkflowContextSnapshot snapshot,
+                                                      final String issueStage) 
{
+        return ensureIdentifiers(fieldName, identifiers, snapshot, issueStage, 
true);
+    }
+    
+    private boolean ensureIdentifiers(final String fieldName, final 
Collection<String> identifiers, final WorkflowContextSnapshot snapshot, final 
String issueStage, final boolean allowEmpty) {
+        for (String each : identifiers) {
+            if (!ensureSupportedIdentifier(fieldName, each, snapshot, 
issueStage, allowEmpty)) {
+                return false;
+            }
+        }
+        return true;
+    }
+    
     /**
      * Ensure workflow planning context is complete and valid.
      *
@@ -188,16 +226,16 @@ public final class WorkflowPlanningSupport {
             snapshot.setStatus(WorkflowLifecycle.STATUS_CLARIFYING);
             return false;
         }
-        if (!ensureSupportedIdentifier(WorkflowFieldNames.DATABASE, 
request.getDatabase(), snapshot)
-                || !ensureSupportedIdentifier(WorkflowFieldNames.TABLE, 
request.getTable(), snapshot)
-                || !ensureSupportedIdentifier(WorkflowFieldNames.COLUMN, 
request.getColumn(), snapshot)) {
+        if (!ensureSupportedIdentifiers(WorkflowFieldNames.DATABASE, 
List.of(request.getDatabase()), snapshot, "discovering")
+                || !ensureSupportedIdentifiers(WorkflowFieldNames.TABLE, 
List.of(request.getTable()), snapshot, "discovering")
+                || !ensureSupportedIdentifiers(WorkflowFieldNames.COLUMN, 
List.of(request.getColumn()), snapshot, "discovering")) {
             snapshot.setStatus(WorkflowLifecycle.STATUS_FAILED);
             return false;
         }
         Optional<MCPDatabaseMetadata> databaseMetadata = 
metadataQueryFacade.queryDatabase(WorkflowSQLUtils.normalizeIdentifier(request.getDatabase()));
         String databaseType = 
databaseMetadata.map(MCPDatabaseMetadata::getDatabaseType).orElse("");
         request.setSchema(resolveSchema(databaseMetadata, request, 
clarifiedIntent, databaseType));
-        if (!ensureSupportedIdentifier(WorkflowFieldNames.SCHEMA, 
request.getSchema(), snapshot)) {
+        if (!ensureSupportedIdentifiers(WorkflowFieldNames.SCHEMA, 
List.of(request.getSchema()), snapshot, "discovering")) {
             snapshot.setStatus(WorkflowLifecycle.STATUS_FAILED);
             return false;
         }
@@ -241,16 +279,31 @@ public final class WorkflowPlanningSupport {
         }
     }
     
-    private boolean ensureSupportedIdentifier(final String fieldName, final 
String identifier, final WorkflowContextSnapshot snapshot) {
-        if (WorkflowSQLUtils.isSupportedIdentifier(identifier)) {
+    private boolean ensureSupportedIdentifier(final String fieldName, final 
String identifier, final WorkflowContextSnapshot snapshot, final String 
issueStage, final boolean allowEmpty) {
+        if (allowEmpty && identifier.isEmpty() || 
WorkflowSQLUtils.isSupportedIdentifier(identifier)) {
             return true;
         }
-        snapshot.getIssues().add(new 
WorkflowIssue(WorkflowIssueCode.UNSUPPORTED_IDENTIFIER, "error", "discovering",
-                String.format("%s identifier `%s` contains unsupported 
characters.", fieldName, identifier),
-                "Use a reviewable logical identifier without NUL or line 
terminators.", false, Map.of("field", fieldName, "identifier", identifier)));
+        snapshot.getIssues().add(new 
WorkflowIssue(WorkflowIssueCode.UNSUPPORTED_IDENTIFIER, "error", issueStage, 
createUnsupportedIdentifierMessage(fieldName, identifier),
+                createUnsupportedIdentifierAction(fieldName), false, 
createUnsupportedIdentifierDetails(fieldName, identifier)));
         return false;
     }
     
+    private String createUnsupportedIdentifierMessage(final String fieldName, 
final String identifier) {
+        return fieldName.isEmpty()
+                ? String.format("Identifier `%s` contains unsupported 
characters.", identifier)
+                : String.format("%s identifier `%s` contains unsupported 
characters.", fieldName, identifier);
+    }
+    
+    private String createUnsupportedIdentifierAction(final String fieldName) {
+        return fieldName.isEmpty()
+                ? "Use reviewable logical identifiers without NUL or line 
terminators."
+                : "Use a reviewable logical identifier without NUL or line 
terminators.";
+    }
+    
+    private Map<String, Object> createUnsupportedIdentifierDetails(final 
String fieldName, final String identifier) {
+        return fieldName.isEmpty() ? Map.of("identifier", identifier) : 
Map.of("field", fieldName, "identifier", identifier);
+    }
+    
     private String resolveSchema(final Optional<MCPDatabaseMetadata> 
databaseMetadata, final WorkflowRequest request, final ClarifiedIntent 
clarifiedIntent, final String databaseType) {
         String actualSchema = request.getSchema();
         if (!isEmptyIdentifier(actualSchema)) {
diff --git 
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowValidationSupport.java
 
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowValidationSupport.java
index d43b54bffdf..1b46bed5434 100644
--- 
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowValidationSupport.java
+++ 
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowValidationSupport.java
@@ -32,6 +32,7 @@ import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.function.Supplier;
 
 /**
  * Workflow validation support.
@@ -58,6 +59,26 @@ public final class WorkflowValidationSupport {
         return Map.of();
     }
     
+    /**
+     * Validate workflow snapshot and persist validation response.
+     *
+     * @param workflowSessionContext workflow session context
+     * @param sessionId session identifier
+     * @param snapshot workflow snapshot
+     * @param validationReportSupplier validation report supplier
+     * @return validation response
+     */
+    public Map<String, Object> validateAndFinalize(final 
WorkflowSessionContext workflowSessionContext, final String sessionId, final 
WorkflowContextSnapshot snapshot,
+                                                   final 
Supplier<ValidationReport> validationReportSupplier) {
+        Map<String, Object> rejectedResponse = 
checkValidatePreconditions(sessionId, snapshot);
+        if (!rejectedResponse.isEmpty()) {
+            return rejectedResponse;
+        }
+        ValidationReport validationReport = validationReportSupplier.get();
+        snapshot.setValidationReport(validationReport);
+        return finalizeValidation(workflowSessionContext, snapshot, 
validationReport);
+    }
+    
     /**
      * Resolve overall validation status from validation sections.
      *
diff --git 
a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowDistSQLQueryUtilsTest.java
 
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowDistSQLQueryUtilsTest.java
index ec2809e8f6d..db3b44c10ab 100644
--- 
a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowDistSQLQueryUtilsTest.java
+++ 
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowDistSQLQueryUtilsTest.java
@@ -18,13 +18,22 @@
 package org.apache.shardingsphere.mcp.support.workflow.service;
 
 import 
org.apache.shardingsphere.mcp.api.protocol.exception.MCPQueryFailedException;
+import 
org.apache.shardingsphere.mcp.support.database.spi.MCPFeatureQueryFacade;
 import org.junit.jupiter.api.Test;
 
 import java.sql.SQLException;
 import java.sql.SQLSyntaxErrorException;
+import java.util.List;
+import java.util.Map;
 
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.sameInstance;
 import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 class WorkflowDistSQLQueryUtilsTest {
     
@@ -46,4 +55,28 @@ class WorkflowDistSQLQueryUtilsTest {
         MCPQueryFailedException actualException = new 
MCPQueryFailedException("Connection refused.", new SQLException("Connection 
refused."));
         
assertFalse(WorkflowDistSQLQueryUtils.isUnsupportedDistSQLQueryFailure(actualException));
     }
+    
+    @Test
+    void assertQueryRuleRows() {
+        MCPFeatureQueryFacade queryFacade = mock(MCPFeatureQueryFacade.class);
+        List<Map<String, Object>> expected = List.of(Map.of("name", "orders"));
+        when(queryFacade.query("logic_db", "", "SHOW MASK 
RULES")).thenReturn(expected);
+        assertThat(WorkflowDistSQLQueryUtils.queryRuleRows(queryFacade, 
"logic_db", "SHOW MASK RULES"), is(expected));
+    }
+    
+    @Test
+    void assertQueryRuleRowsWithUnsupportedDistSQL() {
+        MCPFeatureQueryFacade queryFacade = mock(MCPFeatureQueryFacade.class);
+        when(queryFacade.query("logic_db", "", "SHOW MASK 
RULES")).thenThrow(new MCPQueryFailedException("DistSQL syntax is unsupported 
by this runtime backend."));
+        assertTrue(WorkflowDistSQLQueryUtils.queryRuleRows(queryFacade, 
"logic_db", "SHOW MASK RULES").isEmpty());
+    }
+    
+    @Test
+    void assertQueryRuleRowsWithConnectionFailure() {
+        MCPFeatureQueryFacade queryFacade = mock(MCPFeatureQueryFacade.class);
+        MCPQueryFailedException expected = new 
MCPQueryFailedException("Connection refused.", new SQLException("Connection 
refused."));
+        when(queryFacade.query("logic_db", "", "SHOW MASK 
RULES")).thenThrow(expected);
+        MCPQueryFailedException actual = 
assertThrows(MCPQueryFailedException.class, () -> 
WorkflowDistSQLQueryUtils.queryRuleRows(queryFacade, "logic_db", "SHOW MASK 
RULES"));
+        assertThat(actual, sameInstance(expected));
+    }
 }
diff --git 
a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowPlanningSupportTest.java
 
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowPlanningSupportTest.java
index bee145b6509..d971ea1a162 100644
--- 
a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowPlanningSupportTest.java
+++ 
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowPlanningSupportTest.java
@@ -228,6 +228,24 @@ class WorkflowPlanningSupportTest {
         assertThat(clarifiedIntent.getInferredValues().get("execution_mode"), 
is("manual-only"));
     }
     
+    @Test
+    void assertEnsureOptionalSupportedIdentifiersAllowsEmptyIdentifier() {
+        WorkflowContextSnapshot snapshot = new WorkflowContextSnapshot();
+        boolean actual = 
planningSupport.ensureOptionalSupportedIdentifiers("rule", List.of(""), 
snapshot, "intaking");
+        assertTrue(actual);
+        assertTrue(snapshot.getIssues().isEmpty());
+    }
+    
+    @Test
+    void assertEnsureSupportedIdentifiersRejectsUnsupportedIdentifier() {
+        WorkflowContextSnapshot snapshot = new WorkflowContextSnapshot();
+        boolean actual = planningSupport.ensureSupportedIdentifiers("", 
List.of("orders\ndrop"), snapshot, "intaking");
+        assertFalse(actual);
+        assertThat(snapshot.getIssues().getFirst().getMessage(), 
is("Identifier `orders\ndrop` contains unsupported characters."));
+        assertThat(snapshot.getIssues().getFirst().getUserAction(), is("Use 
reviewable logical identifiers without NUL or line terminators."));
+        assertThat(snapshot.getIssues().getFirst().getDetails(), 
is(Map.of("identifier", "orders\ndrop")));
+    }
+    
     @ParameterizedTest(name = "{0}")
     @MethodSource("getEnsureLifecycleStateCases")
     void assertEnsureLifecycleState(final String name, final String 
operationType, final boolean ruleExists, final boolean expectedResult, final 
String expectedIssueCode) {
diff --git 
a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowValidationSupportTest.java
 
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowValidationSupportTest.java
index 559e4f8e792..2aeee3790bc 100644
--- 
a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowValidationSupportTest.java
+++ 
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/workflow/service/WorkflowValidationSupportTest.java
@@ -30,6 +30,7 @@ import org.junit.jupiter.api.Test;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.is;
@@ -77,6 +78,37 @@ class WorkflowValidationSupportTest {
         assertTrue(validationSupport.checkValidatePreconditions("session-1", 
snapshot).isEmpty());
     }
     
+    @Test
+    void assertValidateAndFinalize() {
+        WorkflowContextSnapshot snapshot = new WorkflowContextSnapshot();
+        snapshot.setPlanId("plan-1");
+        snapshot.setSessionId("session-1");
+        snapshot.setStatus("executed");
+        ValidationReport validationReport = new ValidationReport();
+        validationReport.setOverallStatus("passed");
+        WorkflowSessionContext workflowSessionContext = new 
TestWorkflowSessionContext();
+        workflowSessionContext.save(snapshot);
+        Map<String, Object> actualResult = 
validationSupport.validateAndFinalize(workflowSessionContext, "session-1", 
snapshot, () -> validationReport);
+        assertThat(actualResult.get("response_mode"), is("validation"));
+        assertThat(actualResult.get("status"), is("validated"));
+        assertThat(snapshot.getValidationReport(), is(validationReport));
+    }
+    
+    @Test
+    void assertValidateAndFinalizeRejectsInvalidPrecondition() {
+        WorkflowContextSnapshot snapshot = new WorkflowContextSnapshot();
+        snapshot.setPlanId("plan-1");
+        snapshot.setSessionId("session-1");
+        snapshot.setStatus("clarifying");
+        AtomicBoolean reportCreated = new AtomicBoolean();
+        Map<String, Object> actualResult = 
validationSupport.validateAndFinalize(new TestWorkflowSessionContext(), 
"session-1", snapshot, () -> {
+            reportCreated.set(true);
+            return new ValidationReport();
+        });
+        assertThat(actualResult.get("response_mode"), is("terminal"));
+        assertFalse(reportCreated.get());
+    }
+    
     @Test
     void assertResolveOverallStatusReturnsFailedWhenAnySectionFails() {
         String actualStatus = validationSupport.resolveOverallStatus(
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/programmatic/AbstractHttpProgrammaticRuntimeE2ETest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/programmatic/AbstractHttpProgrammaticRuntimeE2ETest.java
index b7fefc7c313..5bc0b1caff0 100644
--- 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/programmatic/AbstractHttpProgrammaticRuntimeE2ETest.java
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/programmatic/AbstractHttpProgrammaticRuntimeE2ETest.java
@@ -142,6 +142,21 @@ abstract class AbstractHttpProgrammaticRuntimeE2ETest 
extends AbstractConfigBack
         return result;
     }
     
+    protected final String createMaskRulePlan(final HttpClient httpClient, 
final String sessionId) throws IOException, InterruptedException {
+        HttpResponse<String> actual = sendToolCallRequest(httpClient, 
sessionId, "database_gateway_plan_mask_rule", Map.of(
+                "database", "logic_db",
+                "schema", "logic_db",
+                "table", "orders",
+                "column", "status",
+                "operation_type", "create",
+                "algorithm_type", "KEEP_FIRST_N_LAST_M",
+                "primary_algorithm_properties", Map.of("first-n", "1", 
"last-m", "1", "replace-char", "*")));
+        assertThat(actual.statusCode(), is(200));
+        Map<String, Object> payload = getStructuredContent(actual.body());
+        assertThat(String.valueOf(payload.get("status")), is("planned"));
+        return String.valueOf(payload.get("plan_id"));
+    }
+    
     protected final URI getEndpointUri() throws IOException {
         return getHttpEndpointUri();
     }
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/programmatic/HttpTransportApprovalSafetyE2ETest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/programmatic/HttpTransportApprovalSafetyE2ETest.java
index fd212ed5e8d..909444bd32f 100644
--- 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/programmatic/HttpTransportApprovalSafetyE2ETest.java
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/programmatic/HttpTransportApprovalSafetyE2ETest.java
@@ -110,21 +110,6 @@ class HttpTransportApprovalSafetyE2ETest extends 
AbstractSharedHttpProgrammaticR
         return getStructuredContent(actual.body());
     }
     
-    private String createMaskRulePlan(final HttpClient httpClient, final 
String sessionId) throws IOException, InterruptedException {
-        HttpResponse<String> actual = sendToolCallRequest(httpClient, 
sessionId, "database_gateway_plan_mask_rule", Map.of(
-                "database", "logic_db",
-                "schema", "logic_db",
-                "table", "orders",
-                "column", "status",
-                "operation_type", "create",
-                "algorithm_type", "KEEP_FIRST_N_LAST_M",
-                "primary_algorithm_properties", Map.of("first-n", "1", 
"last-m", "1", "replace-char", "*")));
-        assertThat(actual.statusCode(), is(200));
-        Map<String, Object> payload = getStructuredContent(actual.body());
-        assertThat(String.valueOf(payload.get("status")), is("planned"));
-        return String.valueOf(payload.get("plan_id"));
-    }
-    
     private void assertModelFacingPayloadContract(final Map<String, Object> 
payload) {
         MCPModelContractAssertions.assertCanonicalNextActionLists(payload);
     }
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/programmatic/HttpTransportCompletionE2ETest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/programmatic/HttpTransportCompletionE2ETest.java
index ef1152a7ed1..b182584f9af 100644
--- 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/programmatic/HttpTransportCompletionE2ETest.java
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/programmatic/HttpTransportCompletionE2ETest.java
@@ -154,18 +154,4 @@ class HttpTransportCompletionE2ETest extends 
AbstractSharedHttpProgrammaticRunti
                 "completion-1", "completion/complete", params));
     }
     
-    private String createMaskRulePlan(final HttpClient httpClient, final 
String sessionId) throws IOException, InterruptedException {
-        HttpResponse<String> actual = sendToolCallRequest(httpClient, 
sessionId, "database_gateway_plan_mask_rule", Map.of(
-                "database", "logic_db",
-                "schema", "logic_db",
-                "table", "orders",
-                "column", "status",
-                "operation_type", "create",
-                "algorithm_type", "KEEP_FIRST_N_LAST_M",
-                "primary_algorithm_properties", Map.of("first-n", "1", 
"last-m", "1", "replace-char", "*")));
-        assertThat(actual.statusCode(), is(200));
-        Map<String, Object> payload = getStructuredContent(actual.body());
-        assertThat(String.valueOf(payload.get("status")), is("planned"));
-        return String.valueOf(payload.get("plan_id"));
-    }
 }
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/runtime/MySQLRuntimeTestSupport.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/runtime/MySQLRuntimeTestSupport.java
index 68c6540c7fd..1ad17219004 100644
--- 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/runtime/MySQLRuntimeTestSupport.java
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/runtime/MySQLRuntimeTestSupport.java
@@ -203,14 +203,7 @@ public final class MySQLRuntimeTestSupport {
      * @throws SQLException SQL exception
      */
     public static void initializeDatabase(final GenericContainer<?> container) 
throws SQLException {
-        executeStatements(container, DATABASE_NAME,
-                "CREATE TABLE IF NOT EXISTS orders (order_id INT PRIMARY KEY, 
status VARCHAR(32), amount INT)",
-                "CREATE TABLE IF NOT EXISTS order_items (item_id INT PRIMARY 
KEY, order_id INT, sku VARCHAR(64))",
-                "INSERT INTO orders (order_id, status, amount) VALUES (1, 
'NEW', 10) ON DUPLICATE KEY UPDATE status = VALUES(status), amount = 
VALUES(amount)",
-                "INSERT INTO orders (order_id, status, amount) VALUES (2, 
'DONE', 20) ON DUPLICATE KEY UPDATE status = VALUES(status), amount = 
VALUES(amount)",
-                "INSERT INTO order_items (item_id, order_id, sku) VALUES (1, 
1, 'sku-1') ON DUPLICATE KEY UPDATE order_id = VALUES(order_id), sku = 
VALUES(sku)",
-                "CREATE OR REPLACE VIEW active_orders AS SELECT order_id, 
status FROM orders WHERE status <> 'DONE'",
-                "CREATE INDEX idx_orders_status ON orders(status)");
+        initializeOrdersSchema(container, DATABASE_NAME);
     }
     
     private static void initializeProgrammaticDatabases(final 
GenericContainer<?> container) throws SQLException {
@@ -228,14 +221,7 @@ public final class MySQLRuntimeTestSupport {
                 "GRANT ALL PRIVILEGES ON analytics_db.* TO 
'mcp_analytics'@'%'",
                 "GRANT ALL PRIVILEGES ON warehouse.* TO 'mcp_warehouse'@'%'",
                 "FLUSH PRIVILEGES");
-        executeStatements(container, "logic_db",
-                "CREATE TABLE IF NOT EXISTS orders (order_id INT PRIMARY KEY, 
status VARCHAR(32), amount INT)",
-                "CREATE TABLE IF NOT EXISTS order_items (item_id INT PRIMARY 
KEY, order_id INT, sku VARCHAR(64))",
-                "INSERT INTO orders (order_id, status, amount) VALUES (1, 
'NEW', 10) ON DUPLICATE KEY UPDATE status = VALUES(status), amount = 
VALUES(amount)",
-                "INSERT INTO orders (order_id, status, amount) VALUES (2, 
'DONE', 20) ON DUPLICATE KEY UPDATE status = VALUES(status), amount = 
VALUES(amount)",
-                "INSERT INTO order_items (item_id, order_id, sku) VALUES (1, 
1, 'sku-1') ON DUPLICATE KEY UPDATE order_id = VALUES(order_id), sku = 
VALUES(sku)",
-                "CREATE OR REPLACE VIEW active_orders AS SELECT order_id, 
status FROM orders WHERE status <> 'DONE'",
-                "CREATE INDEX idx_orders_status ON orders(status)");
+        initializeOrdersSchema(container, "logic_db");
         executeStatements(container, "analytics_db",
                 "CREATE TABLE IF NOT EXISTS metrics (metric_id INT PRIMARY 
KEY, metric_name VARCHAR(32))",
                 "INSERT INTO metrics (metric_id, metric_name) VALUES (10, 
'cpu') ON DUPLICATE KEY UPDATE metric_name = VALUES(metric_name)",
@@ -246,6 +232,17 @@ public final class MySQLRuntimeTestSupport {
                 "INSERT INTO facts (fact_id, total) VALUES (200, 2) ON 
DUPLICATE KEY UPDATE total = VALUES(total)");
     }
     
+    private static void initializeOrdersSchema(final GenericContainer<?> 
container, final String databaseName) throws SQLException {
+        executeStatements(container, databaseName,
+                "CREATE TABLE IF NOT EXISTS orders (order_id INT PRIMARY KEY, 
status VARCHAR(32), amount INT)",
+                "CREATE TABLE IF NOT EXISTS order_items (item_id INT PRIMARY 
KEY, order_id INT, sku VARCHAR(64))",
+                "INSERT INTO orders (order_id, status, amount) VALUES (1, 
'NEW', 10) ON DUPLICATE KEY UPDATE status = VALUES(status), amount = 
VALUES(amount)",
+                "INSERT INTO orders (order_id, status, amount) VALUES (2, 
'DONE', 20) ON DUPLICATE KEY UPDATE status = VALUES(status), amount = 
VALUES(amount)",
+                "INSERT INTO order_items (item_id, order_id, sku) VALUES (1, 
1, 'sku-1') ON DUPLICATE KEY UPDATE order_id = VALUES(order_id), sku = 
VALUES(sku)",
+                "CREATE OR REPLACE VIEW active_orders AS SELECT order_id, 
status FROM orders WHERE status <> 'DONE'",
+                "CREATE INDEX idx_orders_status ON orders(status)");
+    }
+    
     /**
      * Detect the schema value surfaced by JDBC metadata for the orders table.
      *

Reply via email to