You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@shardingsphere.apache.org by zh...@apache.org on 2026/07/04 05:38:48 UTC

(shardingsphere) branch master updated: Refactor MCP large class responsibilities (#38988)

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 7a03f99acdc Refactor MCP large class responsibilities (#38988)
7a03f99acdc is described below

commit 7a03f99acdc277dd809665699e296072e5e9176d
Author: Liang Zhang <zh...@apache.org>
AuthorDate: Sat Jul 4 13:38:42 2026 +0800

    Refactor MCP large class responsibilities (#38988)
    
    * Refactor MCP large class responsibilities
    
    - Split descriptor catalog validation into focused validators
    - Split bootstrap tool specification tests by result and elicitation flows
    - Extract sharding workflow input and lifecycle planning helpers
    - Extract MCP E2E runtime, Docker, LLM readiness and trace helpers
    - Split JDBC metadata loader tests by focused scenarios
    
    * Refactor MCP large class responsibilities
    
    - Split descriptor catalog validation into focused validators
    - Split bootstrap tool specification tests by result and elicitation flows
    - Extract sharding workflow input and lifecycle planning helpers
    - Extract MCP E2E runtime, Docker, LLM readiness and trace helpers
    - Split JDBC metadata loader tests by focused scenarios
---
 .../tool/MCPCallToolResultFactoryTest.java         | 297 ++++++++++
 ...ryTest.java => MCPToolElicitationFlowTest.java} | 369 +-----------
 .../tool/MCPToolSpecificationFactoryTest.java      | 611 +-------------------
 .../service/ShardingWorkflowInputValidator.java    | 180 ++++++
 .../service/ShardingWorkflowLifecycleSpec.java     |  84 +++
 .../service/ShardingWorkflowPlanningKernel.java    | 224 ++------
 .../MCPCompletionTargetDescriptorValidator.java    | 127 +++++
 .../descriptor/MCPDescriptorCatalogValidator.java  | 605 +-------------------
 .../descriptor/MCPPromptDescriptorValidator.java   | 125 +++++
 .../descriptor/MCPResourceDescriptorValidator.java |  96 ++++
 .../MCPResourceNavigationDescriptorValidator.java  |  63 +++
 .../MCPToolDescriptorCatalogValidator.java         | 159 ++++++
 .../descriptor/MCPToolOutputSchemaValidator.java   | 285 ++++++++++
 .../jdbc/AbstractMCPJdbcMetadataLoaderTest.java    | 454 +++++++++++++++
 .../jdbc/MCPJdbcMetadataLoaderFailureTest.java     |  43 ++
 .../MCPJdbcMetadataLoaderSchemaFilteringTest.java  | 174 ++++++
 .../jdbc/MCPJdbcMetadataLoaderSequenceTest.java    |  90 +++
 .../metadata/jdbc/MCPJdbcMetadataLoaderTest.java   | 620 +--------------------
 .../MCPDescriptorComponentValidatorsTest.java      | 115 ++++
 .../llm/conversation/LLMMCPConversationRunner.java |  43 +-
 .../LLMMCPConversationRunnerNextActionTest.java    |  30 +
 .../LLMMCPConversationTurnPlanner.java             |  16 +-
 .../LLMMCPConversationTurnPlannerTest.java         |  11 +
 .../llm/conversation/LLMMCPTraceRecordFactory.java |  54 ++
 .../conversation/client/LLMChatModelClient.java    | 104 +---
 .../conversation/client/LLMChatReadinessProbe.java | 125 +++++
 .../AbstractProductionMySQLRuntimeE2ETest.java     | 107 +---
 .../ProductionMCPClientTransportFactory.java       | 117 ++++
 .../ProductionRuntimeTransportCases.java           |  80 +++
 .../support/runtime/MySQLRuntimeDockerSupport.java |  87 +++
 .../support/runtime/MySQLRuntimeTestSupport.java   |  50 +-
 31 files changed, 2888 insertions(+), 2657 deletions(-)

diff --git a/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPCallToolResultFactoryTest.java b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPCallToolResultFactoryTest.java
new file mode 100644
index 00000000000..80a5d9a28ac
--- /dev/null
+++ b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPCallToolResultFactoryTest.java
@@ -0,0 +1,297 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.mcp.bootstrap.transport.capability.tool;
+
+import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
+import io.modelcontextprotocol.json.schema.JsonSchemaValidator.ValidationResponse;
+import io.modelcontextprotocol.json.schema.jackson2.DefaultJsonSchemaValidator;
+import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
+import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
+import io.modelcontextprotocol.spec.McpSchema.ResourceLink;
+import io.modelcontextprotocol.spec.McpSchema.TextContent;
+import org.apache.shardingsphere.mcp.api.protocol.response.MCPResponse;
+import org.apache.shardingsphere.mcp.api.tool.MCPToolCall;
+import org.apache.shardingsphere.mcp.api.tool.descriptor.MCPToolDescriptor;
+import org.apache.shardingsphere.mcp.core.protocol.response.MCPErrorResponse;
+import org.apache.shardingsphere.mcp.core.tool.handler.MCPToolDefinition;
+import org.apache.shardingsphere.mcp.core.tool.handler.ToolDefinitionRegistry;
+import org.apache.shardingsphere.mcp.core.workflow.InMemoryWorkflowSessionContext;
+import org.apache.shardingsphere.mcp.feature.encrypt.EncryptFeatureDefinition;
+import org.apache.shardingsphere.mcp.feature.encrypt.tool.handler.PlanEncryptRuleToolHandler;
+import org.apache.shardingsphere.mcp.feature.encrypt.tool.model.EncryptWorkflowRequest;
+import org.apache.shardingsphere.mcp.feature.encrypt.tool.service.EncryptWorkflowPlanningService;
+import org.apache.shardingsphere.mcp.feature.mask.MaskFeatureDefinition;
+import org.apache.shardingsphere.mcp.feature.mask.tool.handler.PlanMaskRuleToolHandler;
+import org.apache.shardingsphere.mcp.feature.mask.tool.service.MaskWorkflowPlanningService;
+import org.apache.shardingsphere.mcp.support.database.MCPDatabaseHandlerContext;
+import org.apache.shardingsphere.mcp.support.database.spi.MCPFeatureQueryFacade;
+import org.apache.shardingsphere.mcp.support.database.spi.MCPMetadataQueryFacade;
+import org.apache.shardingsphere.mcp.support.descriptor.MCPShardingSphereMetadataKeys;
+import org.apache.shardingsphere.mcp.support.protocol.MCPResourceHintUtils;
+import org.apache.shardingsphere.mcp.support.protocol.response.MCPMapResponse;
+import org.apache.shardingsphere.mcp.support.workflow.MCPWorkflowHandlerContext;
+import org.apache.shardingsphere.mcp.support.workflow.WorkflowSessionContext;
+import org.apache.shardingsphere.mcp.support.workflow.model.AlgorithmPropertyRequirement;
+import org.apache.shardingsphere.mcp.support.workflow.model.ClarifiedIntent;
+import org.apache.shardingsphere.mcp.support.workflow.model.InteractionPlan;
+import org.apache.shardingsphere.mcp.support.workflow.model.RuleArtifact;
+import org.apache.shardingsphere.mcp.support.workflow.model.WorkflowContextSnapshot;
+import org.apache.shardingsphere.mcp.support.workflow.model.WorkflowKind;
+import org.apache.shardingsphere.mcp.support.workflow.model.WorkflowLifecycle;
+import org.apache.shardingsphere.mcp.support.workflow.model.WorkflowRequest;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.mockito.internal.configuration.plugins.Plugins;
+
+import java.lang.reflect.Field;
+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.isA;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.when;
+
+class MCPCallToolResultFactoryTest extends AbstractMCPToolSpecificationFactoryTest {
+    
+    @Test
+    void assertCreateToolSpecificationsHandleNullArguments() {
+        try (MockedStatic<ToolDefinitionRegistry> mockedToolDefinitionRegistry = mockStatic(ToolDefinitionRegistry.class)) {
+            MCPResponse response = new MCPMapResponse(Map.of("status", "ok"));
+            MCPToolDefinition toolDefinition = mockSupportedTool(mockedToolDefinitionRegistry, createToolDescriptor("database_gateway_search_metadata"));
+            mockToolDispatch(mockedToolDefinitionRegistry, toolDefinition, Map.of(), response);
+            CallToolResult actual = createToolSpecification("stdio").callHandler().apply(createExchange(), new CallToolRequest(
+                    "database_gateway_search_metadata", null));
+            assertThat(actual.structuredContent(), is(Map.of("status", "ok")));
+            assertThat(((TextContent) actual.content().getFirst()).text(), is("{\"status\":\"ok\"}"));
+            assertFalse(actual.isError());
+        }
+    }
+    
+    @Test
+    void assertCreateToolSpecificationsHandleErrorResponse() {
+        try (MockedStatic<ToolDefinitionRegistry> mockedToolDefinitionRegistry = mockStatic(ToolDefinitionRegistry.class)) {
+            MCPToolDefinition toolDefinition = mockSupportedTool(mockedToolDefinitionRegistry, createToolDescriptor("database_gateway_search_metadata"));
+            mockToolDispatch(mockedToolDefinitionRegistry, toolDefinition, Map.of("query", "foo_query"), new MCPErrorResponse(""));
+            CallToolResult actual = callTool(createToolSpecification("stdio"), createExchange(), "database_gateway_search_metadata", Map.of("query", "foo_query"));
+            @SuppressWarnings("unchecked")
+            Map<String, Object> actualPayload = (Map<String, Object>) actual.structuredContent();
+            assertThat(actualPayload.get("response_mode"), is("recovery"));
+            assertThat(actualPayload.get("message"), is(""));
+            assertTrue(actual.isError());
+        }
+    }
+    
+    @Test
+    void assertCreateToolSpecificationsHandlePlainPayload() {
+        CallToolResult actual = createCallToolResult("fixture_ping", new MCPMapResponse(Map.of("message", "invalid_request")));
+        assertFalse(actual.isError());
+    }
+    
+    @Test
+    void assertCreateWithMaskWorkflowPlanSchema() throws ReflectiveOperationException {
+        CallToolResult actual = createRealDescriptorCallToolResult(MaskFeatureDefinition.PLAN_TOOL_NAME, createMaskPlanResponse());
+        assertFalse(actual.isError(), () -> String.valueOf(actual.structuredContent()));
+        assertThat(getStructuredContent(actual).get("status"), is("planned"));
+    }
+    
+    @Test
+    void assertCreateWithEncryptWorkflowPlanSchema() throws ReflectiveOperationException {
+        CallToolResult actual = createRealDescriptorCallToolResult(EncryptFeatureDefinition.PLAN_TOOL_NAME, createEncryptPlanResponse());
+        assertFalse(actual.isError(), () -> String.valueOf(actual.structuredContent()));
+        assertThat(getStructuredContent(actual).get("status"), is("planned"));
+    }
+    
+    @Test
+    void assertCreateToolSpecificationsHandleResourceLinks() {
+        Map<String, Object> payload = Map.of("resources_to_read", List.of(
+                MCPResourceHintUtils.create("shardingsphere://databases/logic_db", "logical-database", "read_first", "Read logical database.", "resources_to_read")));
+        CallToolResult actual = createCallToolResult("fixture_ping", new MCPMapResponse(payload));
+        assertThat(actual.structuredContent(), is(payload));
+        assertThat(actual.content().get(1), isA(ResourceLink.class));
+        ResourceLink actualLink = (ResourceLink) actual.content().get(1);
+        assertThat(actualLink.uri(), is("shardingsphere://databases/logic_db"));
+        assertThat(actualLink.title(), is("logical-database"));
+        assertThat(actualLink.mimeType(), is("application/json"));
+    }
+    
+    @Test
+    void assertCreateToolSpecificationsHandleItemResourceLinks() {
+        Map<String, Object> payload = Map.of("items", List.of(Map.of(
+                "resource", MCPResourceHintUtils.create("shardingsphere://databases/logic_db/tables/t_order", "table", "inspect_detail", "Read table.", "resource"),
+                "parent_resource", MCPResourceHintUtils.create("shardingsphere://databases/logic_db", "logical-database", "inspect_parent", "Read database.", "parent_resource"),
+                "next_resources", List.of(MCPResourceHintUtils.create(
+                        "shardingsphere://databases/logic_db/tables/t_order/columns", "column-list", "inspect_children", "Read columns.", "next_resources")))));
+        CallToolResult actual = createCallToolResult("fixture_ping", new MCPMapResponse(payload));
+        assertThat(actual.content().size(), is(4));
+        assertThat(((ResourceLink) actual.content().get(1)).uri(), is("shardingsphere://databases/logic_db/tables/t_order"));
+        assertThat(((ResourceLink) actual.content().get(2)).uri(), is("shardingsphere://databases/logic_db"));
+        assertThat(((ResourceLink) actual.content().get(3)).uri(), is("shardingsphere://databases/logic_db/tables/t_order/columns"));
+        assertThat(((Map<?, ?>) actual.content().get(1).meta()).get(MCPShardingSphereMetadataKeys.SOURCE_FIELD), is("resource"));
+    }
+    
+    @Test
+    void assertCreateToolSpecificationsHandleRecoveryResourceLinks() {
+        Map<String, Object> recovery = Map.of("resources_to_read", List.of(
+                MCPResourceHintUtils.create("shardingsphere://capabilities", "capability", "read_first", "Read capabilities.", "resources_to_read")));
+        CallToolResult actual = createCallToolResult("fixture_ping", new MCPErrorResponse("", recovery));
+        assertTrue(actual.isError());
+        assertThat(actual.content().get(1), isA(ResourceLink.class));
+        assertThat(((ResourceLink) actual.content().get(1)).uri(), is("shardingsphere://capabilities"));
+    }
+    
+    @Test
+    void assertCreateToolSpecificationsHandleBoundedResourceLinks() {
+        Map<String, Object> payload = Map.of(
+                "next_resources", createResourceHints("shardingsphere://databases/next_", "next_resources", 30),
+                "parent_resource", MCPResourceHintUtils.create("shardingsphere://databases", "logical-database", "inspect_parent", "Read parent.", "parent_resource"),
+                "resource", MCPResourceHintUtils.create("shardingsphere://databases/logic_db", "logical-database", "inspect_detail", "Read detail.", "resource"),
+                "resources_to_read", List.of(MCPResourceHintUtils.create("shardingsphere://capabilities", "capability", "read_first", "Read capabilities.", "resources_to_read")));
+        CallToolResult actual = createCallToolResult("fixture_ping", new MCPMapResponse(payload));
+        assertThat(actual.structuredContent(), is(payload));
+        assertThat(actual.content().size(), is(25));
+        assertThat(actual.meta().get(MCPShardingSphereMetadataKeys.RESOURCE_LINKS_EMITTED), is(24));
+        assertThat(actual.meta().get(MCPShardingSphereMetadataKeys.RESOURCE_LINKS_OMITTED), is(9));
+        assertThat(((ResourceLink) actual.content().get(1)).uri(), is("shardingsphere://capabilities"));
+        assertThat(((ResourceLink) actual.content().get(2)).uri(), is("shardingsphere://databases/logic_db"));
+        assertThat(((ResourceLink) actual.content().get(3)).uri(), is("shardingsphere://databases"));
+        assertThat(((Map<?, ?>) actual.content().get(1).meta()).get(MCPShardingSphereMetadataKeys.SOURCE_FIELD), is("resources_to_read"));
+    }
+    
+    @Test
+    void assertCreateToolSpecificationsIgnoreRawUriLink() {
+        CallToolResult actual = createCallToolResult("fixture_ping", new MCPMapResponse(Map.of("resource_uri", "shardingsphere://databases/logic_db")));
+        assertThat(actual.content().size(), is(1));
+    }
+    
+    @Test
+    void assertCreateToolSpecificationsIgnoreArbitraryNestedResourceHint() {
+        Map<String, Object> payload = Map.of("debug", Map.of("resource", MCPResourceHintUtils.create(
+                "shardingsphere://databases/logic_db", "logical-database", "inspect_detail", "Read logical database.", "resource")));
+        CallToolResult actual = createCallToolResult("fixture_ping", new MCPMapResponse(payload));
+        assertThat(actual.content().size(), is(1));
+    }
+    
+    @Test
+    void assertToolOutputSchemaExamplesMatchSchemas() {
+        JsonSchemaValidator validator = new DefaultJsonSchemaValidator();
+        for (MCPToolDescriptor each : ToolDefinitionRegistry.getSupportedToolDescriptors()) {
+            for (Map<String, Object> example : getOutputSchemaExamples(each)) {
+                ValidationResponse actual = validator.validate(each.getOutputSchema(), example);
+                assertTrue(actual.valid(), () -> String.format("Invalid outputSchema example for `%s`: %s", each.getName(), actual.errorMessage()));
+            }
+        }
+    }
+    
+    private CallToolResult createRealDescriptorCallToolResult(final String toolName, final MCPResponse response) {
+        MCPToolDescriptor descriptor = ToolDefinitionRegistry.getToolDefinition(toolName).getDescriptor();
+        return new MCPCallToolResultFactory().create(descriptor, response);
+    }
+    
+    private MCPResponse createMaskPlanResponse() throws ReflectiveOperationException {
+        PlanMaskRuleToolHandler handler = new PlanMaskRuleToolHandler();
+        MaskWorkflowPlanningService planningService = mock(MaskWorkflowPlanningService.class);
+        when(planningService.plan(any(), any(), any(), any(), any())).thenReturn(createMaskSnapshot());
+        setField(handler, "planningService", planningService);
+        return handler.handle(createWorkflowContext(), new MCPToolCall("session-id", Map.of("database", "logic_db", "table", "orders", "column", "phone")));
+    }
+    
+    private WorkflowContextSnapshot createMaskSnapshot() {
+        WorkflowRequest request = createWorkflowRequest();
+        request.getPrimaryAlgorithmProperties().put("first-n", "3");
+        request.getPrimaryAlgorithmProperties().put("last-m", "2");
+        request.getPrimaryAlgorithmProperties().put("replace-char", "*");
+        WorkflowContextSnapshot result = createWorkflowContextSnapshot(MaskFeatureDefinition.WORKFLOW_KIND, request);
+        result.getPropertyRequirements().add(new AlgorithmPropertyRequirement("primary", "first-n", true, false, "from", ""));
+        result.getRuleArtifacts().add(new RuleArtifact("create", "CREATE MASK RULE orders (TYPE(NAME='keep_first_n_last_m'))"));
+        return result;
+    }
+    
+    private MCPResponse createEncryptPlanResponse() throws ReflectiveOperationException {
+        PlanEncryptRuleToolHandler handler = new PlanEncryptRuleToolHandler();
+        EncryptWorkflowPlanningService planningService = mock(EncryptWorkflowPlanningService.class);
+        when(planningService.plan(any(), any(), any(), any(), any())).thenReturn(createEncryptSnapshot());
+        setField(handler, "planningService", planningService);
+        return handler.handle(createWorkflowContext(), new MCPToolCall("session-id", Map.of("database", "logic_db", "table", "orders", "column", "phone")));
+    }
+    
+    private WorkflowContextSnapshot createEncryptSnapshot() {
+        EncryptWorkflowRequest request = new EncryptWorkflowRequest();
+        request.setDatabase("logic_db");
+        request.setSchema("public");
+        request.setTable("orders");
+        request.getPrimaryAlgorithmProperties().put("aes-key-value", "123456");
+        WorkflowContextSnapshot result = createWorkflowContextSnapshot(EncryptFeatureDefinition.WORKFLOW_KIND, request);
+        result.getPropertyRequirements().add(new AlgorithmPropertyRequirement("primary", "aes-key-value", true, true, "key", ""));
+        result.getRuleArtifacts().add(new RuleArtifact("create", "CREATE ENCRYPT RULE orders (PROPERTIES('aes-key-value'='123456'))"));
+        return result;
+    }
+    
+    private WorkflowRequest createWorkflowRequest() {
+        WorkflowRequest result = new WorkflowRequest();
+        result.setDatabase("logic_db");
+        result.setSchema("public");
+        result.setTable("orders");
+        return result;
+    }
+    
+    private WorkflowContextSnapshot createWorkflowContextSnapshot(final WorkflowKind workflowKind, final WorkflowRequest request) {
+        WorkflowContextSnapshot result = new WorkflowContextSnapshot();
+        result.setPlanId("plan-1");
+        result.setWorkflowKind(workflowKind);
+        result.setStatus(WorkflowLifecycle.STATUS_PLANNED);
+        result.setRequest(request);
+        result.setClarifiedIntent(new ClarifiedIntent());
+        result.setInteractionPlan(createInteractionPlan());
+        return result;
+    }
+    
+    private InteractionPlan createInteractionPlan() {
+        InteractionPlan result = new InteractionPlan();
+        result.setCurrentStep("review");
+        result.setDeliveryMode("interactive");
+        result.setExecutionMode("review-then-execute");
+        return result;
+    }
+    
+    private MCPWorkflowHandlerContext createWorkflowContext() {
+        MCPWorkflowHandlerContext result = mock(MCPWorkflowHandlerContext.class);
+        MCPDatabaseHandlerContext databaseContext = mock(MCPDatabaseHandlerContext.class);
+        WorkflowSessionContext workflowSessionContext = new InMemoryWorkflowSessionContext();
+        when(result.getDatabaseContext()).thenReturn(databaseContext);
+        when(result.getWorkflowSessionContext()).thenReturn(workflowSessionContext);
+        when(databaseContext.getMetadataQueryFacade()).thenReturn(mock(MCPMetadataQueryFacade.class));
+        when(databaseContext.getQueryFacade()).thenReturn(mock(MCPFeatureQueryFacade.class));
+        return result;
+    }
+    
+    private void setField(final Object target, final String fieldName, final Object value) throws ReflectiveOperationException {
+        Field field = target.getClass().getDeclaredField(fieldName);
+        Plugins.getMemberAccessor().set(field, target, value);
+    }
+    
+    @SuppressWarnings("unchecked")
+    private List<Map<String, Object>> getOutputSchemaExamples(final MCPToolDescriptor toolDescriptor) {
+        return toolDescriptor.getOutputSchema().containsKey("examples") ? (List<Map<String, Object>>) toolDescriptor.getOutputSchema().get("examples") : List.of();
+    }
+}
diff --git a/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPToolSpecificationFactoryTest.java b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPToolElicitationFlowTest.java
similarity index 51%
copy from mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPToolSpecificationFactoryTest.java
copy to mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPToolElicitationFlowTest.java
index 3bc9b54783a..250484461bb 100644
--- a/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPToolSpecificationFactoryTest.java
+++ b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPToolElicitationFlowTest.java
@@ -17,57 +17,20 @@
 
 package org.apache.shardingsphere.mcp.bootstrap.transport.capability.tool;
 
-import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
-import io.modelcontextprotocol.json.schema.JsonSchemaValidator.ValidationResponse;
-import io.modelcontextprotocol.json.schema.jackson2.DefaultJsonSchemaValidator;
-import io.modelcontextprotocol.server.McpServerFeatures.SyncToolSpecification;
 import io.modelcontextprotocol.server.McpSyncServerExchange;
-import io.modelcontextprotocol.spec.McpError;
 import io.modelcontextprotocol.spec.McpSchema;
-import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
 import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
-import io.modelcontextprotocol.spec.McpSchema.ResourceLink;
-import io.modelcontextprotocol.spec.McpSchema.TextContent;
 import org.apache.shardingsphere.mcp.api.protocol.response.MCPResponse;
-import org.apache.shardingsphere.mcp.api.tool.MCPToolCall;
-import org.apache.shardingsphere.mcp.api.tool.descriptor.MCPToolAnnotations;
-import org.apache.shardingsphere.mcp.api.tool.descriptor.MCPToolDescriptor;
 import org.apache.shardingsphere.mcp.core.context.MCPRequestScope;
 import org.apache.shardingsphere.mcp.core.context.MCPRuntimeContext;
-import org.apache.shardingsphere.mcp.core.protocol.exception.UnsupportedToolException;
-import org.apache.shardingsphere.mcp.core.protocol.response.MCPErrorResponse;
 import org.apache.shardingsphere.mcp.core.tool.handler.MCPToolDefinition;
 import org.apache.shardingsphere.mcp.core.tool.handler.ToolDefinitionRegistry;
-import org.apache.shardingsphere.mcp.core.workflow.InMemoryWorkflowSessionContext;
-import org.apache.shardingsphere.mcp.feature.encrypt.EncryptFeatureDefinition;
-import org.apache.shardingsphere.mcp.feature.encrypt.tool.handler.PlanEncryptRuleToolHandler;
-import org.apache.shardingsphere.mcp.feature.encrypt.tool.model.EncryptWorkflowRequest;
-import org.apache.shardingsphere.mcp.feature.encrypt.tool.service.EncryptWorkflowPlanningService;
-import org.apache.shardingsphere.mcp.feature.mask.MaskFeatureDefinition;
-import org.apache.shardingsphere.mcp.feature.mask.tool.handler.PlanMaskRuleToolHandler;
-import org.apache.shardingsphere.mcp.feature.mask.tool.service.MaskWorkflowPlanningService;
-import org.apache.shardingsphere.mcp.support.database.MCPDatabaseHandlerContext;
-import org.apache.shardingsphere.mcp.support.database.spi.MCPFeatureQueryFacade;
-import org.apache.shardingsphere.mcp.support.database.spi.MCPMetadataQueryFacade;
 import org.apache.shardingsphere.mcp.support.descriptor.MCPShardingSphereMetadataKeys;
-import org.apache.shardingsphere.mcp.support.protocol.MCPResourceHintUtils;
 import org.apache.shardingsphere.mcp.support.protocol.response.MCPMapResponse;
-import org.apache.shardingsphere.mcp.support.workflow.MCPWorkflowHandlerContext;
-import org.apache.shardingsphere.mcp.support.workflow.WorkflowSessionContext;
-import org.apache.shardingsphere.mcp.support.workflow.model.AlgorithmPropertyRequirement;
-import org.apache.shardingsphere.mcp.support.workflow.model.ClarifiedIntent;
-import org.apache.shardingsphere.mcp.support.workflow.model.InteractionPlan;
-import org.apache.shardingsphere.mcp.support.workflow.model.RuleArtifact;
-import org.apache.shardingsphere.mcp.support.workflow.model.WorkflowContextSnapshot;
-import org.apache.shardingsphere.mcp.support.workflow.model.WorkflowKind;
-import org.apache.shardingsphere.mcp.support.workflow.model.WorkflowLifecycle;
-import org.apache.shardingsphere.mcp.support.workflow.model.WorkflowRequest;
 import org.junit.jupiter.api.Test;
 import org.mockito.ArgumentCaptor;
 import org.mockito.MockedStatic;
-import org.mockito.internal.configuration.plugins.Plugins;
 
-import java.lang.reflect.Field;
 import java.time.Clock;
 import java.time.Duration;
 import java.util.Collections;
@@ -77,11 +40,6 @@ import java.util.Map;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.isA;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
@@ -91,228 +49,7 @@ import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-class MCPToolSpecificationFactoryTest extends AbstractMCPToolSpecificationFactoryTest {
-    
-    @Test
-    void assertCreateToolSpecifications() {
-        try (MockedStatic<ToolDefinitionRegistry> mockedToolDefinitionRegistry = mockStatic(ToolDefinitionRegistry.class)) {
-            mockedToolDefinitionRegistry.when(ToolDefinitionRegistry::getSupportedToolDescriptors).thenReturn(List.of(createToolDescriptor("database_gateway_search_metadata")));
-            MCPToolSpecificationFactory actualFactory = new MCPToolSpecificationFactory(mock(MCPRuntimeContext.class, RETURNS_DEEP_STUBS));
-            List<SyncToolSpecification> actual = actualFactory.createToolSpecifications();
-            assertThat(actual.size(), is(1));
-            assertThat(actual.getFirst().tool().name(), is("database_gateway_search_metadata"));
-            assertThat(actual.getFirst().tool().title(), is("Search Metadata"));
-            assertThat(actual.getFirst().tool().description(), is("Search database metadata."));
-            assertThat(actual.getFirst().tool().inputSchema().type(), is("object"));
-            assertThat(actual.getFirst().tool().inputSchema().required(), is(List.of("query")));
-            assertFalse(actual.getFirst().tool().inputSchema().additionalProperties());
-            assertThat(actual.getFirst().tool().inputSchema().properties().get("query"), is(Map.of("type", "string", "description", "Search query.")));
-            assertThat(actual.getFirst().tool().inputSchema().properties().get("object_types"), is(Map.of(
-                    "type", "array",
-                    "description", "Optional object-type filter.",
-                    "items", Map.of("type", "string", "description", "Object type.", "enum", List.of("TABLE", "VIEW")))));
-            assertThat(actual.getFirst().tool().outputSchema(), is(Map.of("type", "object")));
-            assertTrue(actual.getFirst().tool().annotations().readOnlyHint());
-            assertNull(actual.getFirst().tool().annotations().returnDirect());
-            assertThat(actual.getFirst().tool().meta(), is(Map.of(MCPShardingSphereMetadataKeys.RELATED_RESOURCE_URIS, List.of("shardingsphere://databases"))));
-            assertNotNull(actual.getFirst().callHandler());
-        }
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsMapAnnotationPresence() {
-        try (MockedStatic<ToolDefinitionRegistry> mockedToolDefinitionRegistry = mockStatic(ToolDefinitionRegistry.class)) {
-            mockedToolDefinitionRegistry.when(ToolDefinitionRegistry::getSupportedToolDescriptors).thenReturn(List.of(createToolDescriptor(
-                    "fixture_declared_defaults", new MCPToolAnnotations(null, false, true, false, true))));
-            MCPToolSpecificationFactory actualFactory = new MCPToolSpecificationFactory(mock(MCPRuntimeContext.class, RETURNS_DEEP_STUBS));
-            List<SyncToolSpecification> actual = actualFactory.createToolSpecifications();
-            assertNotNull(actual.getFirst().tool().annotations());
-            assertFalse(actual.getFirst().tool().annotations().readOnlyHint());
-            assertTrue(actual.getFirst().tool().annotations().destructiveHint());
-            assertFalse(actual.getFirst().tool().annotations().idempotentHint());
-            assertTrue(actual.getFirst().tool().annotations().openWorldHint());
-        }
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsHandleNullArguments() {
-        try (MockedStatic<ToolDefinitionRegistry> mockedToolDefinitionRegistry = mockStatic(ToolDefinitionRegistry.class)) {
-            MCPResponse response = new MCPMapResponse(Map.of("status", "ok"));
-            MCPToolDefinition toolDefinition = mockSupportedTool(mockedToolDefinitionRegistry, createToolDescriptor("database_gateway_search_metadata"));
-            mockToolDispatch(mockedToolDefinitionRegistry, toolDefinition, Map.of(), response);
-            SyncToolSpecification actualSpecification = createToolSpecification("stdio");
-            McpSyncServerExchange exchange = createExchange();
-            CallToolResult actual = actualSpecification.callHandler().apply(exchange, new CallToolRequest("database_gateway_search_metadata", null));
-            assertThat(actual.structuredContent(), is(Map.of("status", "ok")));
-            assertThat(((TextContent) actual.content().getFirst()).text(), is("{\"status\":\"ok\"}"));
-            assertFalse(actual.isError());
-        }
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsHandleErrorResponse() {
-        try (MockedStatic<ToolDefinitionRegistry> mockedToolDefinitionRegistry = mockStatic(ToolDefinitionRegistry.class)) {
-            MCPToolDefinition toolDefinition = mockSupportedTool(mockedToolDefinitionRegistry, createToolDescriptor("database_gateway_search_metadata"));
-            mockToolDispatch(mockedToolDefinitionRegistry, toolDefinition, Map.of("query", "foo_query"), new MCPErrorResponse(""));
-            CallToolResult actual = callTool(createToolSpecification("stdio"), createExchange(), "database_gateway_search_metadata", Map.of("query", "foo_query"));
-            @SuppressWarnings("unchecked")
-            Map<String, Object> actualPayload = (Map<String, Object>) actual.structuredContent();
-            assertThat(actualPayload.get("response_mode"), is("recovery"));
-            assertThat(actualPayload.get("message"), is(""));
-            assertTrue(actual.isError());
-        }
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsHandlePlainPayload() {
-        CallToolResult actual = createCallToolResult("fixture_ping", new MCPMapResponse(Map.of("message", "invalid_request")));
-        assertFalse(actual.isError());
-    }
-    
-    @Test
-    void assertCreateWithMaskWorkflowPlanSchema() throws ReflectiveOperationException {
-        CallToolResult actual = createRealDescriptorCallToolResult(MaskFeatureDefinition.PLAN_TOOL_NAME, createMaskPlanResponse());
-        assertFalse(actual.isError(), () -> String.valueOf(actual.structuredContent()));
-        assertThat(getStructuredContent(actual).get("status"), is("planned"));
-    }
-    
-    @Test
-    void assertCreateWithEncryptWorkflowPlanSchema() throws ReflectiveOperationException {
-        CallToolResult actual = createRealDescriptorCallToolResult(EncryptFeatureDefinition.PLAN_TOOL_NAME, createEncryptPlanResponse());
-        assertFalse(actual.isError(), () -> String.valueOf(actual.structuredContent()));
-        assertThat(getStructuredContent(actual).get("status"), is("planned"));
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsHandleResourceLinks() {
-        Map<String, Object> payload = Map.of("resources_to_read", List.of(
-                MCPResourceHintUtils.create("shardingsphere://databases/logic_db", "logical-database", "read_first", "Read logical database.", "resources_to_read")));
-        CallToolResult actual = createCallToolResult("fixture_ping", new MCPMapResponse(payload));
-        assertThat(actual.structuredContent(), is(payload));
-        assertThat(actual.content().get(1), isA(ResourceLink.class));
-        ResourceLink actualLink = (ResourceLink) actual.content().get(1);
-        assertThat(actualLink.uri(), is("shardingsphere://databases/logic_db"));
-        assertThat(actualLink.title(), is("logical-database"));
-        assertThat(actualLink.mimeType(), is("application/json"));
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsHandleItemResourceLinks() {
-        Map<String, Object> payload = Map.of("items", List.of(Map.of(
-                "resource", MCPResourceHintUtils.create("shardingsphere://databases/logic_db/tables/t_order", "table", "inspect_detail", "Read table.", "resource"),
-                "parent_resource", MCPResourceHintUtils.create("shardingsphere://databases/logic_db", "logical-database", "inspect_parent", "Read database.", "parent_resource"),
-                "next_resources", List.of(MCPResourceHintUtils.create(
-                        "shardingsphere://databases/logic_db/tables/t_order/columns", "column-list", "inspect_children", "Read columns.", "next_resources")))));
-        CallToolResult actual = createCallToolResult("fixture_ping", new MCPMapResponse(payload));
-        assertThat(actual.content().size(), is(4));
-        assertThat(((ResourceLink) actual.content().get(1)).uri(), is("shardingsphere://databases/logic_db/tables/t_order"));
-        assertThat(((ResourceLink) actual.content().get(2)).uri(), is("shardingsphere://databases/logic_db"));
-        assertThat(((ResourceLink) actual.content().get(3)).uri(), is("shardingsphere://databases/logic_db/tables/t_order/columns"));
-        assertThat(((Map<?, ?>) actual.content().get(1).meta()).get(MCPShardingSphereMetadataKeys.SOURCE_FIELD), is("resource"));
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsHandleRecoveryResourceLinks() {
-        Map<String, Object> recovery = Map.of("resources_to_read", List.of(
-                MCPResourceHintUtils.create("shardingsphere://capabilities", "capability", "read_first", "Read capabilities.", "resources_to_read")));
-        CallToolResult actual = createCallToolResult("fixture_ping", new MCPErrorResponse("", recovery));
-        assertTrue(actual.isError());
-        assertThat(actual.content().get(1), isA(ResourceLink.class));
-        assertThat(((ResourceLink) actual.content().get(1)).uri(), is("shardingsphere://capabilities"));
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsHandleBoundedResourceLinks() {
-        Map<String, Object> payload = Map.of(
-                "next_resources", createResourceHints("shardingsphere://databases/next_", "next_resources", 30),
-                "parent_resource", MCPResourceHintUtils.create("shardingsphere://databases", "logical-database", "inspect_parent", "Read parent.", "parent_resource"),
-                "resource", MCPResourceHintUtils.create("shardingsphere://databases/logic_db", "logical-database", "inspect_detail", "Read detail.", "resource"),
-                "resources_to_read", List.of(MCPResourceHintUtils.create("shardingsphere://capabilities", "capability", "read_first", "Read capabilities.", "resources_to_read")));
-        CallToolResult actual = createCallToolResult("fixture_ping", new MCPMapResponse(payload));
-        assertThat(actual.structuredContent(), is(payload));
-        assertThat(actual.content().size(), is(25));
-        assertThat(actual.meta().get(MCPShardingSphereMetadataKeys.RESOURCE_LINKS_EMITTED), is(24));
-        assertThat(actual.meta().get(MCPShardingSphereMetadataKeys.RESOURCE_LINKS_OMITTED), is(9));
-        assertThat(((ResourceLink) actual.content().get(1)).uri(), is("shardingsphere://capabilities"));
-        assertThat(((ResourceLink) actual.content().get(2)).uri(), is("shardingsphere://databases/logic_db"));
-        assertThat(((ResourceLink) actual.content().get(3)).uri(), is("shardingsphere://databases"));
-        assertThat(((Map<?, ?>) actual.content().get(1).meta()).get(MCPShardingSphereMetadataKeys.SOURCE_FIELD), is("resources_to_read"));
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsIgnoreRawUriLink() {
-        CallToolResult actual = createCallToolResult("fixture_ping", new MCPMapResponse(Map.of("resource_uri", "shardingsphere://databases/logic_db")));
-        assertThat(actual.content().size(), is(1));
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsIgnoreArbitraryNestedResourceHint() {
-        Map<String, Object> payload = Map.of("debug", Map.of("resource", MCPResourceHintUtils.create(
-                "shardingsphere://databases/logic_db", "logical-database", "inspect_detail", "Read logical database.", "resource")));
-        CallToolResult actual = createCallToolResult("fixture_ping", new MCPMapResponse(payload));
-        assertThat(actual.content().size(), is(1));
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsHandleToolDefinitionDescriptor() {
-        try (MockedStatic<ToolDefinitionRegistry> mockedToolDefinitionRegistry = mockStatic(ToolDefinitionRegistry.class)) {
-            MCPToolDefinition toolDefinition = mockSupportedTool(mockedToolDefinitionRegistry, createToolDescriptor("fixture_ping"));
-            mockToolDispatch(mockedToolDefinitionRegistry, toolDefinition, Map.of(), new MCPMapResponse(Map.of("status", "ok")));
-            CallToolResult actual = callTool(createToolSpecification("stdio"), createExchange(), "fixture_ping", Map.of());
-            assertThat(actual.structuredContent(), is(Map.of("status", "ok")));
-            assertThat(((TextContent) actual.content().getFirst()).text(), is("{\"status\":\"ok\"}"));
-        }
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsHandleUnsupportedToolAsProtocolError() {
-        try (MockedStatic<ToolDefinitionRegistry> mockedToolDefinitionRegistry = mockStatic(ToolDefinitionRegistry.class)) {
-            mockedToolDefinitionRegistry.when(ToolDefinitionRegistry::getSupportedToolDescriptors).thenReturn(List.of(createToolDescriptor("database_gateway_search_metadata")));
-            mockedToolDefinitionRegistry.when(() -> ToolDefinitionRegistry.getToolDefinition("database_gateway_search_metadata")).thenThrow(UnsupportedToolException.class);
-            MCPRuntimeContext runtimeContext = mock(MCPRuntimeContext.class, RETURNS_DEEP_STUBS);
-            when(runtimeContext.getSessionManager().getTransactionResourceManager().getRuntimeDatabases()).thenReturn(Collections.emptyMap());
-            SyncToolSpecification actualSpecification = createToolSpecification(runtimeContext);
-            McpSyncServerExchange exchange = createExchange();
-            McpError actual = assertThrows(McpError.class, () -> callTool(actualSpecification, exchange, "database_gateway_search_metadata", Map.of()));
-            assertThat(actual.getJsonRpcError().code(), is(McpSchema.ErrorCodes.INVALID_PARAMS));
-            assertThat(actual.getJsonRpcError().message(), is("Unsupported tool `database_gateway_search_metadata`."));
-            @SuppressWarnings("unchecked")
-            Map<String, Object> actualData = (Map<String, Object>) actual.getJsonRpcError().data();
-            assertThat(actualData.get("message"), is("Unsupported tool `database_gateway_search_metadata`."));
-        }
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsRejectInvalidInputSchema() {
-        MCPRuntimeContext runtimeContext = mock(MCPRuntimeContext.class, RETURNS_DEEP_STUBS);
-        when(runtimeContext.getSessionManager().getTransactionResourceManager().getRuntimeDatabases()).thenReturn(Collections.emptyMap());
-        SyncToolSpecification actualSpecification = findToolSpecification(new MCPToolSpecificationFactory(runtimeContext).createToolSpecifications(), "database_gateway_search_metadata");
-        CallToolResult actual = callTool(actualSpecification, createExchange(), "database_gateway_search_metadata", Map.of("query", "order", "object_types", List.of("TABLE")));
-        @SuppressWarnings("unchecked")
-        Map<String, Object> actualPayload = (Map<String, Object>) actual.structuredContent();
-        Map<?, ?> actualRecovery = (Map<?, ?>) actualPayload.get("recovery");
-        assertThat(actualPayload.get("message"), is("object_types[0] must be one of [database, schema, table, view, column, index, storage_unit, sequence]."));
-        assertThat(actualRecovery.get("category"), is("invalid_enum_value"));
-        assertThat(actualRecovery.get("field"), is("object_types[0]"));
-        assertThat(actualRecovery.get("allowed_values"), is(List.of("database", "schema", "table", "view", "column", "index", "storage_unit", "sequence")));
-        assertThat(actualRecovery.get("suggested_arguments"), is(Map.of("query", "order")));
-        assertTrue(actual.isError());
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsValidateStructuredOutput() {
-        try (MockedStatic<ToolDefinitionRegistry> mockedToolDefinitionRegistry = mockStatic(ToolDefinitionRegistry.class)) {
-            MCPToolDefinition toolDefinition = mockSupportedTool(mockedToolDefinitionRegistry, createStrictToolDescriptor("database_gateway_search_metadata"));
-            mockToolDispatch(mockedToolDefinitionRegistry, toolDefinition, Map.of(), new MCPMapResponse(Map.of("count", 1)));
-            MCPRuntimeContext runtimeContext = mock(MCPRuntimeContext.class, RETURNS_DEEP_STUBS);
-            when(runtimeContext.getSessionManager().getTransactionResourceManager().getRuntimeDatabases()).thenReturn(Collections.emptyMap());
-            CallToolResult actual = callTool(createToolSpecification(runtimeContext), createExchange(), "database_gateway_search_metadata", Map.of());
-            @SuppressWarnings("unchecked")
-            Map<String, Object> actualPayload = (Map<String, Object>) actual.structuredContent();
-            assertTrue(String.valueOf(actualPayload.get("message")).contains("database_gateway_search_metadata"));
-            assertTrue(actual.isError());
-        }
-    }
+class MCPToolElicitationFlowTest extends AbstractMCPToolSpecificationFactoryTest {
     
     @Test
     void assertCreateToolSpecificationsHandleInteractiveElicitation() {
@@ -640,93 +377,6 @@ class MCPToolSpecificationFactoryTest extends AbstractMCPToolSpecificationFactor
         }
     }
     
-    private CallToolResult createRealDescriptorCallToolResult(final String toolName, final MCPResponse response) {
-        MCPToolDescriptor descriptor = ToolDefinitionRegistry.getToolDefinition(toolName).getDescriptor();
-        return new MCPCallToolResultFactory().create(descriptor, response);
-    }
-    
-    private MCPResponse createMaskPlanResponse() throws ReflectiveOperationException {
-        PlanMaskRuleToolHandler handler = new PlanMaskRuleToolHandler();
-        MaskWorkflowPlanningService planningService = mock(MaskWorkflowPlanningService.class);
-        when(planningService.plan(any(), any(), any(), any(), any())).thenReturn(createMaskSnapshot());
-        setField(handler, "planningService", planningService);
-        return handler.handle(createWorkflowContext(), new MCPToolCall("session-id", Map.of("database", "logic_db", "table", "orders", "column", "phone")));
-    }
-    
-    private WorkflowContextSnapshot createMaskSnapshot() {
-        WorkflowRequest request = createWorkflowRequest();
-        request.getPrimaryAlgorithmProperties().put("first-n", "3");
-        request.getPrimaryAlgorithmProperties().put("last-m", "2");
-        request.getPrimaryAlgorithmProperties().put("replace-char", "*");
-        WorkflowContextSnapshot result = createWorkflowContextSnapshot(MaskFeatureDefinition.WORKFLOW_KIND, request);
-        result.getPropertyRequirements().add(new AlgorithmPropertyRequirement("primary", "first-n", true, false, "from", ""));
-        result.getRuleArtifacts().add(new RuleArtifact("create", "CREATE MASK RULE orders (TYPE(NAME='keep_first_n_last_m'))"));
-        return result;
-    }
-    
-    private MCPResponse createEncryptPlanResponse() throws ReflectiveOperationException {
-        PlanEncryptRuleToolHandler handler = new PlanEncryptRuleToolHandler();
-        EncryptWorkflowPlanningService planningService = mock(EncryptWorkflowPlanningService.class);
-        when(planningService.plan(any(), any(), any(), any(), any())).thenReturn(createEncryptSnapshot());
-        setField(handler, "planningService", planningService);
-        return handler.handle(createWorkflowContext(), new MCPToolCall("session-id", Map.of("database", "logic_db", "table", "orders", "column", "phone")));
-    }
-    
-    private WorkflowContextSnapshot createEncryptSnapshot() {
-        EncryptWorkflowRequest request = new EncryptWorkflowRequest();
-        request.setDatabase("logic_db");
-        request.setSchema("public");
-        request.setTable("orders");
-        request.getPrimaryAlgorithmProperties().put("aes-key-value", "123456");
-        WorkflowContextSnapshot result = createWorkflowContextSnapshot(EncryptFeatureDefinition.WORKFLOW_KIND, request);
-        result.getPropertyRequirements().add(new AlgorithmPropertyRequirement("primary", "aes-key-value", true, true, "key", ""));
-        result.getRuleArtifacts().add(new RuleArtifact("create", "CREATE ENCRYPT RULE orders (PROPERTIES('aes-key-value'='123456'))"));
-        return result;
-    }
-    
-    private WorkflowRequest createWorkflowRequest() {
-        WorkflowRequest result = new WorkflowRequest();
-        result.setDatabase("logic_db");
-        result.setSchema("public");
-        result.setTable("orders");
-        return result;
-    }
-    
-    private WorkflowContextSnapshot createWorkflowContextSnapshot(final WorkflowKind workflowKind, final WorkflowRequest request) {
-        WorkflowContextSnapshot result = new WorkflowContextSnapshot();
-        result.setPlanId("plan-1");
-        result.setWorkflowKind(workflowKind);
-        result.setStatus(WorkflowLifecycle.STATUS_PLANNED);
-        result.setRequest(request);
-        result.setClarifiedIntent(new ClarifiedIntent());
-        result.setInteractionPlan(createInteractionPlan());
-        return result;
-    }
-    
-    private InteractionPlan createInteractionPlan() {
-        InteractionPlan result = new InteractionPlan();
-        result.setCurrentStep("review");
-        result.setDeliveryMode("interactive");
-        result.setExecutionMode("review-then-execute");
-        return result;
-    }
-    
-    private MCPWorkflowHandlerContext createWorkflowContext() {
-        MCPWorkflowHandlerContext result = mock(MCPWorkflowHandlerContext.class);
-        MCPDatabaseHandlerContext databaseContext = mock(MCPDatabaseHandlerContext.class);
-        WorkflowSessionContext workflowSessionContext = new InMemoryWorkflowSessionContext();
-        when(result.getDatabaseContext()).thenReturn(databaseContext);
-        when(result.getWorkflowSessionContext()).thenReturn(workflowSessionContext);
-        when(databaseContext.getMetadataQueryFacade()).thenReturn(mock(MCPMetadataQueryFacade.class));
-        when(databaseContext.getQueryFacade()).thenReturn(mock(MCPFeatureQueryFacade.class));
-        return result;
-    }
-    
-    private void setField(final Object target, final String fieldName, final Object value) throws ReflectiveOperationException {
-        Field field = target.getClass().getDeclaredField(fieldName);
-        Plugins.getMemberAccessor().set(field, target, value);
-    }
-    
     private void assertCreateToolSpecificationsFallbackWhenElicitationAction(final McpSchema.ElicitResult.Action action) {
         try (MockedStatic<ToolDefinitionRegistry> mockedToolDefinitionRegistry = mockStatic(ToolDefinitionRegistry.class)) {
             Map<String, Object> expectedPayload = createClarifyingPayload();
@@ -739,21 +389,4 @@ class MCPToolSpecificationFactoryTest extends AbstractMCPToolSpecificationFactor
             verify(exchange).createElicitation(any());
         }
     }
-    
-    @Test
-    void assertToolOutputSchemaExamplesMatchSchemas() {
-        JsonSchemaValidator validator = new DefaultJsonSchemaValidator();
-        for (MCPToolDescriptor each : ToolDefinitionRegistry.getSupportedToolDescriptors()) {
-            for (Map<String, Object> example : getOutputSchemaExamples(each)) {
-                ValidationResponse actual = validator.validate(each.getOutputSchema(), example);
-                assertTrue(actual.valid(), () -> String.format("Invalid outputSchema example for `%s`: %s", each.getName(), actual.errorMessage()));
-            }
-        }
-    }
-    
-    @SuppressWarnings("unchecked")
-    private List<Map<String, Object>> getOutputSchemaExamples(final MCPToolDescriptor toolDescriptor) {
-        return toolDescriptor.getOutputSchema().containsKey("examples") ? (List<Map<String, Object>>) toolDescriptor.getOutputSchema().get("examples") : List.of();
-    }
-    
 }
diff --git a/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPToolSpecificationFactoryTest.java b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPToolSpecificationFactoryTest.java
index 3bc9b54783a..dc9e6cde946 100644
--- a/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPToolSpecificationFactoryTest.java
+++ b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPToolSpecificationFactoryTest.java
@@ -17,78 +17,35 @@
 
 package org.apache.shardingsphere.mcp.bootstrap.transport.capability.tool;
 
-import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
-import io.modelcontextprotocol.json.schema.JsonSchemaValidator.ValidationResponse;
-import io.modelcontextprotocol.json.schema.jackson2.DefaultJsonSchemaValidator;
 import io.modelcontextprotocol.server.McpServerFeatures.SyncToolSpecification;
 import io.modelcontextprotocol.server.McpSyncServerExchange;
 import io.modelcontextprotocol.spec.McpError;
-import io.modelcontextprotocol.spec.McpSchema;
-import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
+import io.modelcontextprotocol.spec.McpSchema.ErrorCodes;
 import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
-import io.modelcontextprotocol.spec.McpSchema.ResourceLink;
-import io.modelcontextprotocol.spec.McpSchema.TextContent;
-import org.apache.shardingsphere.mcp.api.protocol.response.MCPResponse;
-import org.apache.shardingsphere.mcp.api.tool.MCPToolCall;
 import org.apache.shardingsphere.mcp.api.tool.descriptor.MCPToolAnnotations;
-import org.apache.shardingsphere.mcp.api.tool.descriptor.MCPToolDescriptor;
-import org.apache.shardingsphere.mcp.core.context.MCPRequestScope;
 import org.apache.shardingsphere.mcp.core.context.MCPRuntimeContext;
 import org.apache.shardingsphere.mcp.core.protocol.exception.UnsupportedToolException;
-import org.apache.shardingsphere.mcp.core.protocol.response.MCPErrorResponse;
 import org.apache.shardingsphere.mcp.core.tool.handler.MCPToolDefinition;
 import org.apache.shardingsphere.mcp.core.tool.handler.ToolDefinitionRegistry;
-import org.apache.shardingsphere.mcp.core.workflow.InMemoryWorkflowSessionContext;
-import org.apache.shardingsphere.mcp.feature.encrypt.EncryptFeatureDefinition;
-import org.apache.shardingsphere.mcp.feature.encrypt.tool.handler.PlanEncryptRuleToolHandler;
-import org.apache.shardingsphere.mcp.feature.encrypt.tool.model.EncryptWorkflowRequest;
-import org.apache.shardingsphere.mcp.feature.encrypt.tool.service.EncryptWorkflowPlanningService;
-import org.apache.shardingsphere.mcp.feature.mask.MaskFeatureDefinition;
-import org.apache.shardingsphere.mcp.feature.mask.tool.handler.PlanMaskRuleToolHandler;
-import org.apache.shardingsphere.mcp.feature.mask.tool.service.MaskWorkflowPlanningService;
-import org.apache.shardingsphere.mcp.support.database.MCPDatabaseHandlerContext;
-import org.apache.shardingsphere.mcp.support.database.spi.MCPFeatureQueryFacade;
-import org.apache.shardingsphere.mcp.support.database.spi.MCPMetadataQueryFacade;
 import org.apache.shardingsphere.mcp.support.descriptor.MCPShardingSphereMetadataKeys;
-import org.apache.shardingsphere.mcp.support.protocol.MCPResourceHintUtils;
 import org.apache.shardingsphere.mcp.support.protocol.response.MCPMapResponse;
-import org.apache.shardingsphere.mcp.support.workflow.MCPWorkflowHandlerContext;
-import org.apache.shardingsphere.mcp.support.workflow.WorkflowSessionContext;
-import org.apache.shardingsphere.mcp.support.workflow.model.AlgorithmPropertyRequirement;
-import org.apache.shardingsphere.mcp.support.workflow.model.ClarifiedIntent;
-import org.apache.shardingsphere.mcp.support.workflow.model.InteractionPlan;
-import org.apache.shardingsphere.mcp.support.workflow.model.RuleArtifact;
-import org.apache.shardingsphere.mcp.support.workflow.model.WorkflowContextSnapshot;
-import org.apache.shardingsphere.mcp.support.workflow.model.WorkflowKind;
-import org.apache.shardingsphere.mcp.support.workflow.model.WorkflowLifecycle;
-import org.apache.shardingsphere.mcp.support.workflow.model.WorkflowRequest;
 import org.junit.jupiter.api.Test;
-import org.mockito.ArgumentCaptor;
 import org.mockito.MockedStatic;
-import org.mockito.internal.configuration.plugins.Plugins;
 
-import java.lang.reflect.Field;
-import java.time.Clock;
-import java.time.Duration;
 import java.util.Collections;
 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.isA;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.mockStatic;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 class MCPToolSpecificationFactoryTest extends AbstractMCPToolSpecificationFactoryTest {
@@ -134,125 +91,6 @@ class MCPToolSpecificationFactoryTest extends AbstractMCPToolSpecificationFactor
         }
     }
     
-    @Test
-    void assertCreateToolSpecificationsHandleNullArguments() {
-        try (MockedStatic<ToolDefinitionRegistry> mockedToolDefinitionRegistry = mockStatic(ToolDefinitionRegistry.class)) {
-            MCPResponse response = new MCPMapResponse(Map.of("status", "ok"));
-            MCPToolDefinition toolDefinition = mockSupportedTool(mockedToolDefinitionRegistry, createToolDescriptor("database_gateway_search_metadata"));
-            mockToolDispatch(mockedToolDefinitionRegistry, toolDefinition, Map.of(), response);
-            SyncToolSpecification actualSpecification = createToolSpecification("stdio");
-            McpSyncServerExchange exchange = createExchange();
-            CallToolResult actual = actualSpecification.callHandler().apply(exchange, new CallToolRequest("database_gateway_search_metadata", null));
-            assertThat(actual.structuredContent(), is(Map.of("status", "ok")));
-            assertThat(((TextContent) actual.content().getFirst()).text(), is("{\"status\":\"ok\"}"));
-            assertFalse(actual.isError());
-        }
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsHandleErrorResponse() {
-        try (MockedStatic<ToolDefinitionRegistry> mockedToolDefinitionRegistry = mockStatic(ToolDefinitionRegistry.class)) {
-            MCPToolDefinition toolDefinition = mockSupportedTool(mockedToolDefinitionRegistry, createToolDescriptor("database_gateway_search_metadata"));
-            mockToolDispatch(mockedToolDefinitionRegistry, toolDefinition, Map.of("query", "foo_query"), new MCPErrorResponse(""));
-            CallToolResult actual = callTool(createToolSpecification("stdio"), createExchange(), "database_gateway_search_metadata", Map.of("query", "foo_query"));
-            @SuppressWarnings("unchecked")
-            Map<String, Object> actualPayload = (Map<String, Object>) actual.structuredContent();
-            assertThat(actualPayload.get("response_mode"), is("recovery"));
-            assertThat(actualPayload.get("message"), is(""));
-            assertTrue(actual.isError());
-        }
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsHandlePlainPayload() {
-        CallToolResult actual = createCallToolResult("fixture_ping", new MCPMapResponse(Map.of("message", "invalid_request")));
-        assertFalse(actual.isError());
-    }
-    
-    @Test
-    void assertCreateWithMaskWorkflowPlanSchema() throws ReflectiveOperationException {
-        CallToolResult actual = createRealDescriptorCallToolResult(MaskFeatureDefinition.PLAN_TOOL_NAME, createMaskPlanResponse());
-        assertFalse(actual.isError(), () -> String.valueOf(actual.structuredContent()));
-        assertThat(getStructuredContent(actual).get("status"), is("planned"));
-    }
-    
-    @Test
-    void assertCreateWithEncryptWorkflowPlanSchema() throws ReflectiveOperationException {
-        CallToolResult actual = createRealDescriptorCallToolResult(EncryptFeatureDefinition.PLAN_TOOL_NAME, createEncryptPlanResponse());
-        assertFalse(actual.isError(), () -> String.valueOf(actual.structuredContent()));
-        assertThat(getStructuredContent(actual).get("status"), is("planned"));
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsHandleResourceLinks() {
-        Map<String, Object> payload = Map.of("resources_to_read", List.of(
-                MCPResourceHintUtils.create("shardingsphere://databases/logic_db", "logical-database", "read_first", "Read logical database.", "resources_to_read")));
-        CallToolResult actual = createCallToolResult("fixture_ping", new MCPMapResponse(payload));
-        assertThat(actual.structuredContent(), is(payload));
-        assertThat(actual.content().get(1), isA(ResourceLink.class));
-        ResourceLink actualLink = (ResourceLink) actual.content().get(1);
-        assertThat(actualLink.uri(), is("shardingsphere://databases/logic_db"));
-        assertThat(actualLink.title(), is("logical-database"));
-        assertThat(actualLink.mimeType(), is("application/json"));
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsHandleItemResourceLinks() {
-        Map<String, Object> payload = Map.of("items", List.of(Map.of(
-                "resource", MCPResourceHintUtils.create("shardingsphere://databases/logic_db/tables/t_order", "table", "inspect_detail", "Read table.", "resource"),
-                "parent_resource", MCPResourceHintUtils.create("shardingsphere://databases/logic_db", "logical-database", "inspect_parent", "Read database.", "parent_resource"),
-                "next_resources", List.of(MCPResourceHintUtils.create(
-                        "shardingsphere://databases/logic_db/tables/t_order/columns", "column-list", "inspect_children", "Read columns.", "next_resources")))));
-        CallToolResult actual = createCallToolResult("fixture_ping", new MCPMapResponse(payload));
-        assertThat(actual.content().size(), is(4));
-        assertThat(((ResourceLink) actual.content().get(1)).uri(), is("shardingsphere://databases/logic_db/tables/t_order"));
-        assertThat(((ResourceLink) actual.content().get(2)).uri(), is("shardingsphere://databases/logic_db"));
-        assertThat(((ResourceLink) actual.content().get(3)).uri(), is("shardingsphere://databases/logic_db/tables/t_order/columns"));
-        assertThat(((Map<?, ?>) actual.content().get(1).meta()).get(MCPShardingSphereMetadataKeys.SOURCE_FIELD), is("resource"));
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsHandleRecoveryResourceLinks() {
-        Map<String, Object> recovery = Map.of("resources_to_read", List.of(
-                MCPResourceHintUtils.create("shardingsphere://capabilities", "capability", "read_first", "Read capabilities.", "resources_to_read")));
-        CallToolResult actual = createCallToolResult("fixture_ping", new MCPErrorResponse("", recovery));
-        assertTrue(actual.isError());
-        assertThat(actual.content().get(1), isA(ResourceLink.class));
-        assertThat(((ResourceLink) actual.content().get(1)).uri(), is("shardingsphere://capabilities"));
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsHandleBoundedResourceLinks() {
-        Map<String, Object> payload = Map.of(
-                "next_resources", createResourceHints("shardingsphere://databases/next_", "next_resources", 30),
-                "parent_resource", MCPResourceHintUtils.create("shardingsphere://databases", "logical-database", "inspect_parent", "Read parent.", "parent_resource"),
-                "resource", MCPResourceHintUtils.create("shardingsphere://databases/logic_db", "logical-database", "inspect_detail", "Read detail.", "resource"),
-                "resources_to_read", List.of(MCPResourceHintUtils.create("shardingsphere://capabilities", "capability", "read_first", "Read capabilities.", "resources_to_read")));
-        CallToolResult actual = createCallToolResult("fixture_ping", new MCPMapResponse(payload));
-        assertThat(actual.structuredContent(), is(payload));
-        assertThat(actual.content().size(), is(25));
-        assertThat(actual.meta().get(MCPShardingSphereMetadataKeys.RESOURCE_LINKS_EMITTED), is(24));
-        assertThat(actual.meta().get(MCPShardingSphereMetadataKeys.RESOURCE_LINKS_OMITTED), is(9));
-        assertThat(((ResourceLink) actual.content().get(1)).uri(), is("shardingsphere://capabilities"));
-        assertThat(((ResourceLink) actual.content().get(2)).uri(), is("shardingsphere://databases/logic_db"));
-        assertThat(((ResourceLink) actual.content().get(3)).uri(), is("shardingsphere://databases"));
-        assertThat(((Map<?, ?>) actual.content().get(1).meta()).get(MCPShardingSphereMetadataKeys.SOURCE_FIELD), is("resources_to_read"));
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsIgnoreRawUriLink() {
-        CallToolResult actual = createCallToolResult("fixture_ping", new MCPMapResponse(Map.of("resource_uri", "shardingsphere://databases/logic_db")));
-        assertThat(actual.content().size(), is(1));
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsIgnoreArbitraryNestedResourceHint() {
-        Map<String, Object> payload = Map.of("debug", Map.of("resource", MCPResourceHintUtils.create(
-                "shardingsphere://databases/logic_db", "logical-database", "inspect_detail", "Read logical database.", "resource")));
-        CallToolResult actual = createCallToolResult("fixture_ping", new MCPMapResponse(payload));
-        assertThat(actual.content().size(), is(1));
-    }
-    
     @Test
     void assertCreateToolSpecificationsHandleToolDefinitionDescriptor() {
         try (MockedStatic<ToolDefinitionRegistry> mockedToolDefinitionRegistry = mockStatic(ToolDefinitionRegistry.class)) {
@@ -260,7 +98,7 @@ class MCPToolSpecificationFactoryTest extends AbstractMCPToolSpecificationFactor
             mockToolDispatch(mockedToolDefinitionRegistry, toolDefinition, Map.of(), new MCPMapResponse(Map.of("status", "ok")));
             CallToolResult actual = callTool(createToolSpecification("stdio"), createExchange(), "fixture_ping", Map.of());
             assertThat(actual.structuredContent(), is(Map.of("status", "ok")));
-            assertThat(((TextContent) actual.content().getFirst()).text(), is("{\"status\":\"ok\"}"));
+            assertThat(actual.content().getFirst().type(), is("text"));
         }
     }
     
@@ -274,7 +112,7 @@ class MCPToolSpecificationFactoryTest extends AbstractMCPToolSpecificationFactor
             SyncToolSpecification actualSpecification = createToolSpecification(runtimeContext);
             McpSyncServerExchange exchange = createExchange();
             McpError actual = assertThrows(McpError.class, () -> callTool(actualSpecification, exchange, "database_gateway_search_metadata", Map.of()));
-            assertThat(actual.getJsonRpcError().code(), is(McpSchema.ErrorCodes.INVALID_PARAMS));
+            assertThat(actual.getJsonRpcError().code(), is(ErrorCodes.INVALID_PARAMS));
             assertThat(actual.getJsonRpcError().message(), is("Unsupported tool `database_gateway_search_metadata`."));
             @SuppressWarnings("unchecked")
             Map<String, Object> actualData = (Map<String, Object>) actual.getJsonRpcError().data();
@@ -313,447 +151,4 @@ class MCPToolSpecificationFactoryTest extends AbstractMCPToolSpecificationFactor
             assertTrue(actual.isError());
         }
     }
-    
-    @Test
-    void assertCreateToolSpecificationsHandleInteractiveElicitation() {
-        assertInteractiveElicitation(McpSchema.ClientCapabilities.builder().elicitation().build());
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsHandleInteractiveElicitationWithFormOnlyClient() {
-        assertInteractiveElicitation(createFormOnlyClientCapabilities());
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsHandleInteractiveElicitationWithFormAndUrlClient() {
-        assertInteractiveElicitation(createFormAndUrlClientCapabilities());
-    }
-    
-    private void assertInteractiveElicitation(final McpSchema.ClientCapabilities clientCapabilities) {
-        try (MockedStatic<ToolDefinitionRegistry> mockedToolDefinitionRegistry = mockStatic(ToolDefinitionRegistry.class)) {
-            String toolName = "database_gateway_plan_encrypt_rule";
-            MCPResponse clarifyingResponse = new MCPMapResponse(createClarifyingPayload());
-            MCPResponse plannedResponse = new MCPMapResponse(Map.of("status", "planned"));
-            MCPToolDefinition toolDefinition = mockSupportedTool(mockedToolDefinitionRegistry, createPlanningToolDescriptor(toolName));
-            mockedToolDefinitionRegistry.when(() -> ToolDefinitionRegistry.dispatch(any(MCPRequestScope.class), eq(toolDefinition), eq("session-id"), any()))
-                    .thenReturn(clarifyingResponse, plannedResponse);
-            McpSyncServerExchange exchange = createElicitationExchange(new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT,
-                    Map.of("field_1", "foo_display", "field_2", true)), clientCapabilities);
-            CallToolResult actual = callTool(createToolSpecification("stdio"), exchange, toolName, Map.of());
-            assertThat(actual.structuredContent(), is(Map.of("status", "planned")));
-            ArgumentCaptor<McpSchema.ElicitRequest> requestCaptor = ArgumentCaptor.forClass(McpSchema.ElicitRequest.class);
-            verify(exchange).createElicitation(requestCaptor.capture());
-            assertThat(requestCaptor.getValue().meta().get(MCPShardingSphereMetadataKeys.TOOL), is(toolName));
-            assertThat(requestCaptor.getValue().meta().get(MCPShardingSphereMetadataKeys.PLAN_ID), is("plan-1"));
-            assertThat(requestCaptor.getValue().meta().get(MCPShardingSphereMetadataKeys.FORM_REQUEST_ID), isA(String.class));
-            assertThat(requestCaptor.getValue().requestedSchema(), is(createExpectedElicitRequestedSchema()));
-            mockedToolDefinitionRegistry.verify(() -> ToolDefinitionRegistry.dispatch(any(MCPRequestScope.class), eq(toolDefinition), eq("session-id"), eq(createElicitedArguments())));
-        }
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsSkipElicitationWithSecretQuestion() {
-        assertCreateToolSpecificationsSkipUnsafeElicitation(createClarifyingQuestion("custom_properties.display-name", "string", true, "Provide display name."));
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsSkipElicitationWithSecretInputType() {
-        assertCreateToolSpecificationsSkipUnsafeElicitation(createClarifyingQuestion("custom_properties.display-name", "secret", false, "Provide display name."));
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsSkipElicitationWithSensitiveFieldName() {
-        assertCreateToolSpecificationsSkipUnsafeElicitation(createClarifyingQuestion("primary_algorithm_properties.access-token", "string", false, "Provide access token."));
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsFallbackWithUrlModeForSensitiveQuestion() {
-        try (MockedStatic<ToolDefinitionRegistry> mockedToolDefinitionRegistry = mockStatic(ToolDefinitionRegistry.class)) {
-            String toolName = "database_gateway_plan_encrypt_rule";
-            MCPResponse response = new MCPMapResponse(createClarifyingPayload(
-                    createClarifyingQuestion("primary_algorithm_properties.access-token", "string", false, "Provide access token.")));
-            MCPToolDefinition toolDefinition = mockSupportedTool(mockedToolDefinitionRegistry, createPlanningToolDescriptor(toolName));
-            mockToolDispatch(mockedToolDefinitionRegistry, toolDefinition, Map.of(), response);
-            McpSyncServerExchange exchange = createElicitationExchange(new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT, Map.of()), createFormAndUrlClientCapabilities());
-            CallToolResult actual = callTool(createToolSpecification("stdio"), exchange, toolName, Map.of());
-            assertStructuredFallback(actual, "url_mode_not_implemented", true, true, "url_fallback");
-            assertSanitizedSensitiveFallback(actual);
-            verify(exchange, never()).createElicitation(any());
-        }
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsSkipElicitationWithoutPlanId() {
-        assertCreateToolSpecificationsSkipUnsafeElicitationWithPayload(createClarifyingPayloadWithoutPlanId(), "missing_plan_id", "structured_fallback");
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsSkipElicitationWithUnknownAlgorithmSecretFlag() {
-        assertCreateToolSpecificationsSkipUnsafeElicitationWithPayload(Map.of(
-                "plan_id", "plan-1",
-                "status", "clarifying",
-                "clarification_questions", List.of(Map.of(
-                        "field", "primary_algorithm_properties.props",
-                        "input_type", "string",
-                        "display_message", "Provide props."))),
-                "sensitive_form_blocked", "url_fallback");
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsSkipElicitationWithAmbiguousFieldBinding() {
-        try (MockedStatic<ToolDefinitionRegistry> mockedToolDefinitionRegistry = mockStatic(ToolDefinitionRegistry.class)) {
-            String toolName = "database_gateway_plan_encrypt_rule";
-            Map<String, Object> expectedPayload = createClarifyingPayload(createClarifyingQuestion("requires_review", "boolean", false, "Require review?"));
-            MCPResponse response = new MCPMapResponse(expectedPayload);
-            MCPToolDefinition toolDefinition = mockSupportedTool(mockedToolDefinitionRegistry, createAmbiguousPlanningToolDescriptor(toolName));
-            mockToolDispatch(mockedToolDefinitionRegistry, toolDefinition, Map.of(), response);
-            McpSyncServerExchange exchange = createElicitationExchange(new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT, Map.of("field_1", true)));
-            CallToolResult actual = callTool(createToolSpecification("stdio"), exchange, toolName, Map.of());
-            assertStructuredFallback(actual, "ambiguous_field_binding", true, false, "structured_fallback");
-            verify(exchange, never()).createElicitation(any());
-        }
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsSkipElicitationForUrlOnlyClient() {
-        try (MockedStatic<ToolDefinitionRegistry> mockedToolDefinitionRegistry = mockStatic(ToolDefinitionRegistry.class)) {
-            String toolName = "database_gateway_plan_encrypt_rule";
-            Map<String, Object> expectedPayload = createClarifyingPayload();
-            MCPResponse response = new MCPMapResponse(expectedPayload);
-            MCPToolDefinition toolDefinition = mockSupportedTool(mockedToolDefinitionRegistry, createPlanningToolDescriptor(toolName));
-            mockToolDispatch(mockedToolDefinitionRegistry, toolDefinition, Map.of(), response);
-            McpSyncServerExchange exchange = createUrlOnlyElicitationExchange();
-            CallToolResult actual = callTool(createToolSpecification("stdio"), exchange, toolName, Map.of());
-            assertStructuredFallback(actual, "client_unsupported", false, true, "structured_fallback");
-            verify(exchange, never()).createElicitation(any());
-        }
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsFallbackForStreamableHttp() {
-        try (MockedStatic<ToolDefinitionRegistry> mockedToolDefinitionRegistry = mockStatic(ToolDefinitionRegistry.class)) {
-            String toolName = "database_gateway_plan_encrypt_rule";
-            MCPResponse response = new MCPMapResponse(createClarifyingPayload());
-            MCPToolDefinition toolDefinition = mockSupportedTool(mockedToolDefinitionRegistry, createPlanningToolDescriptor(toolName));
-            mockToolDispatch(mockedToolDefinitionRegistry, toolDefinition, Map.of(), response);
-            McpSyncServerExchange exchange = createElicitationExchange(new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT, Map.of("field_1", "foo_display")));
-            CallToolResult actual = callTool(createToolSpecification("http"), exchange, toolName, Map.of());
-            assertStructuredFallback(actual, "remote_identity_required", true, false, "structured_fallback");
-            verify(exchange, never()).createElicitation(any());
-        }
-    }
-    
-    private void assertCreateToolSpecificationsSkipUnsafeElicitation(final Map<String, Object> question) {
-        try (MockedStatic<ToolDefinitionRegistry> mockedToolDefinitionRegistry = mockStatic(ToolDefinitionRegistry.class)) {
-            String toolName = "database_gateway_plan_encrypt_rule";
-            Map<String, Object> expectedPayload = createClarifyingPayload(question);
-            MCPResponse response = new MCPMapResponse(expectedPayload);
-            MCPToolDefinition toolDefinition = mockSupportedTool(mockedToolDefinitionRegistry, createPlanningToolDescriptor(toolName));
-            mockToolDispatch(mockedToolDefinitionRegistry, toolDefinition, Map.of(), response);
-            McpSyncServerExchange exchange = createElicitationExchange(new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT, Map.of("custom_properties.display-name", "foo_display")));
-            CallToolResult actual = callTool(createToolSpecification("stdio"), exchange, toolName, Map.of());
-            assertStructuredFallback(actual, "sensitive_form_blocked", true, false, "url_fallback");
-            assertSanitizedSensitiveFallback(actual);
-            verify(exchange, never()).createElicitation(any());
-        }
-    }
-    
-    private void assertCreateToolSpecificationsSkipUnsafeElicitationWithPayload(final Map<String, Object> expectedPayload, final String expectedReason,
-                                                                                final String expectedInteraction) {
-        try (MockedStatic<ToolDefinitionRegistry> mockedToolDefinitionRegistry = mockStatic(ToolDefinitionRegistry.class)) {
-            String toolName = "database_gateway_plan_encrypt_rule";
-            MCPResponse response = new MCPMapResponse(expectedPayload);
-            MCPToolDefinition toolDefinition = mockSupportedTool(mockedToolDefinitionRegistry, createPlanningToolDescriptor(toolName));
-            mockToolDispatch(mockedToolDefinitionRegistry, toolDefinition, Map.of(), response);
-            McpSyncServerExchange exchange = createElicitationExchange(new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT, Map.of("field_1", "foo_display")));
-            CallToolResult actual = callTool(createToolSpecification("stdio"), exchange, toolName, Map.of());
-            assertStructuredFallback(actual, expectedReason, true, false, expectedInteraction);
-            if ("url_fallback".equals(expectedInteraction)) {
-                assertSanitizedSensitiveFallback(actual);
-            }
-            verify(exchange, never()).createElicitation(any());
-        }
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsSkipElicitationForNonPlanningTool() {
-        try (MockedStatic<ToolDefinitionRegistry> mockedToolDefinitionRegistry = mockStatic(ToolDefinitionRegistry.class)) {
-            Map<String, Object> expectedPayload = createClarifyingPayload();
-            MCPResponse response = new MCPMapResponse(expectedPayload);
-            MCPToolDefinition toolDefinition = mockSupportedTool(mockedToolDefinitionRegistry, createToolDescriptor("database_gateway_search_metadata"));
-            mockToolDispatch(mockedToolDefinitionRegistry, toolDefinition, Map.of(), response);
-            MCPRuntimeContext runtimeContext = mock(MCPRuntimeContext.class, RETURNS_DEEP_STUBS);
-            when(runtimeContext.getSessionManager().getTransactionResourceManager().getRuntimeDatabases()).thenReturn(Collections.emptyMap());
-            McpSyncServerExchange exchange = createElicitationExchange(new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT, Map.of()));
-            CallToolResult actual = callTool(createToolSpecification(runtimeContext), exchange, "database_gateway_search_metadata", Map.of());
-            assertThat(actual.structuredContent(), is(expectedPayload));
-            verify(exchange, never()).createElicitation(any());
-        }
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsSkipElicitationWithoutRuntimeDescriptor() {
-        try (MockedStatic<ToolDefinitionRegistry> mockedToolDefinitionRegistry = mockStatic(ToolDefinitionRegistry.class)) {
-            Map<String, Object> expectedPayload = createClarifyingPayload();
-            MCPResponse response = new MCPMapResponse(expectedPayload);
-            MCPToolDefinition toolDefinition = mockSupportedTool(mockedToolDefinitionRegistry, createToolDescriptor("fixture_ping"));
-            mockToolDispatch(mockedToolDefinitionRegistry, toolDefinition, Map.of(), response);
-            MCPRuntimeContext runtimeContext = mock(MCPRuntimeContext.class, RETURNS_DEEP_STUBS);
-            when(runtimeContext.getSessionManager().getTransactionResourceManager().getRuntimeDatabases()).thenReturn(Collections.emptyMap());
-            McpSyncServerExchange exchange = createElicitationExchange(new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT, Map.of()));
-            CallToolResult actual = callTool(createToolSpecification(runtimeContext), exchange, "fixture_ping", Map.of());
-            assertThat(actual.structuredContent(), is(expectedPayload));
-            verify(exchange, never()).createElicitation(any());
-        }
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsFallbackWithoutElicitation() {
-        try (MockedStatic<ToolDefinitionRegistry> mockedToolDefinitionRegistry = mockStatic(ToolDefinitionRegistry.class)) {
-            MCPResponse response = new MCPMapResponse(createClarifyingPayload());
-            MCPToolDefinition toolDefinition = mockSupportedTool(mockedToolDefinitionRegistry, createPlanningToolDescriptor("database_gateway_plan_encrypt_rule"));
-            mockToolDispatch(mockedToolDefinitionRegistry, toolDefinition, Map.of(), response);
-            McpSyncServerExchange exchange = createExchange();
-            CallToolResult actual = callTool(createToolSpecification("stdio"), exchange, "database_gateway_plan_encrypt_rule", Map.of());
-            assertStructuredFallback(actual, "client_unsupported", false, false, "structured_fallback");
-            verify(exchange, never()).createElicitation(any());
-        }
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsFallbackWithoutElicitationCapabilities() {
-        try (MockedStatic<ToolDefinitionRegistry> mockedToolDefinitionRegistry = mockStatic(ToolDefinitionRegistry.class)) {
-            MCPResponse response = new MCPMapResponse(createClarifyingPayload());
-            MCPToolDefinition toolDefinition = mockSupportedTool(mockedToolDefinitionRegistry, createPlanningToolDescriptor("database_gateway_plan_encrypt_rule"));
-            mockToolDispatch(mockedToolDefinitionRegistry, toolDefinition, Map.of(), response);
-            McpSchema.ClientCapabilities clientCapabilities = new McpSchema.ClientCapabilities(Collections.emptyMap(), null, null, null);
-            McpSyncServerExchange exchange = createElicitationExchange(new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT, Map.of()), clientCapabilities);
-            CallToolResult actual = callTool(createToolSpecification("stdio"), exchange, "database_gateway_plan_encrypt_rule", Map.of());
-            assertStructuredFallback(actual, "client_unsupported", false, false, "structured_fallback");
-            verify(exchange, never()).createElicitation(any());
-        }
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsFallbackWhenElicitationDeclined() {
-        assertCreateToolSpecificationsFallbackWhenElicitationAction(McpSchema.ElicitResult.Action.DECLINE);
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsFallbackWhenElicitationCancelled() {
-        assertCreateToolSpecificationsFallbackWhenElicitationAction(McpSchema.ElicitResult.Action.CANCEL);
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsFallbackWhenElicitedContentHasExtraField() {
-        assertCreateToolSpecificationsFallbackWhenElicitedContentInvalid(Map.of("field_1", "foo_display", "field_2", true, "field_3", "unexpected"));
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsFallbackWhenElicitedContentMissesField() {
-        assertCreateToolSpecificationsFallbackWhenElicitedContentInvalid(Map.of("field_1", "foo_display"));
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsFallbackWhenElicitedContentHasBlankRequiredValue() {
-        assertCreateToolSpecificationsFallbackWhenElicitedContentInvalid(Map.of("field_1", " ", "field_2", true));
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsFallbackWhenElicitedContentTypeMismatches() {
-        assertCreateToolSpecificationsFallbackWhenElicitedContentInvalid(Map.of("field_1", 1, "field_2", true));
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsFallbackWhenElicitedContentViolatesAllowedValues() {
-        Map<String, Object> expectedPayload = createClarifyingPayload(createClarifyingQuestion(
-                "custom_properties.display-name", "string", false, "Provide display name.", List.of("foo_display")));
-        assertCreateToolSpecificationsFallbackWhenElicitedContentInvalid(expectedPayload, Map.of("field_1", "bar_display"));
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsFallbackWhenElicitationFails() {
-        try (MockedStatic<ToolDefinitionRegistry> mockedToolDefinitionRegistry = mockStatic(ToolDefinitionRegistry.class)) {
-            MCPResponse response = new MCPMapResponse(createClarifyingPayload());
-            MCPToolDefinition toolDefinition = mockSupportedTool(mockedToolDefinitionRegistry, createPlanningToolDescriptor("database_gateway_plan_encrypt_rule"));
-            mockToolDispatch(mockedToolDefinitionRegistry, toolDefinition, Map.of(), response);
-            McpSyncServerExchange exchange = createThrowingElicitationExchange();
-            CallToolResult actual = callTool(createToolSpecification("stdio"), exchange, "database_gateway_plan_encrypt_rule", Map.of());
-            assertStructuredFallback(actual, "elicitation_failed", true, false, "structured_fallback");
-            verify(exchange).createElicitation(any());
-        }
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsFallbackWhenElicitationResultMalformed() {
-        assertCreateToolSpecificationsFallbackWhenElicitedContentInvalid("malformed_elicitation_result", new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT, null));
-    }
-    
-    @Test
-    void assertCreateToolSpecificationsFallbackWhenElicitationExpires() {
-        try (
-                MockedStatic<ToolDefinitionRegistry> mockedToolDefinitionRegistry = mockStatic(ToolDefinitionRegistry.class);
-                MockedStatic<Clock> mockedClock = mockStatic(Clock.class)) {
-            MutableClock clock = new MutableClock();
-            mockedClock.when(Clock::systemUTC).thenReturn(clock);
-            MCPResponse response = new MCPMapResponse(createClarifyingPayload());
-            MCPToolDefinition toolDefinition = mockSupportedTool(mockedToolDefinitionRegistry, createPlanningToolDescriptor("database_gateway_plan_encrypt_rule"));
-            mockToolDispatch(mockedToolDefinitionRegistry, toolDefinition, Map.of(), response);
-            McpSyncServerExchange exchange = createElicitationExchange(new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT, Map.of("field_1", "foo_display", "field_2", true)));
-            when(exchange.createElicitation(any())).thenAnswer(invocation -> {
-                clock.advance(Duration.ofMinutes(11L));
-                return new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT, Map.of("field_1", "foo_display", "field_2", true));
-            });
-            CallToolResult actual = callTool(createToolSpecification("stdio"), exchange, "database_gateway_plan_encrypt_rule", Map.of());
-            assertStructuredFallback(actual, "stale_elicitation", true, false, "structured_fallback");
-            mockedToolDefinitionRegistry.verify(() -> ToolDefinitionRegistry.dispatch(any(MCPRequestScope.class), eq(toolDefinition), eq("session-id"), eq(createElicitedArguments())), never());
-        }
-    }
-    
-    private void assertCreateToolSpecificationsFallbackWhenElicitedContentInvalid(final Map<String, Object> elicitedContent) {
-        assertCreateToolSpecificationsFallbackWhenElicitedContentInvalid(createClarifyingPayload(), elicitedContent);
-    }
-    
-    private void assertCreateToolSpecificationsFallbackWhenElicitedContentInvalid(final Map<String, Object> expectedPayload, final Map<String, Object> elicitedContent) {
-        try (MockedStatic<ToolDefinitionRegistry> mockedToolDefinitionRegistry = mockStatic(ToolDefinitionRegistry.class)) {
-            MCPResponse response = new MCPMapResponse(expectedPayload);
-            MCPToolDefinition toolDefinition = mockSupportedTool(mockedToolDefinitionRegistry, createPlanningToolDescriptor("database_gateway_plan_encrypt_rule"));
-            mockToolDispatch(mockedToolDefinitionRegistry, toolDefinition, Map.of(), response);
-            McpSyncServerExchange exchange = createElicitationExchange(new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT, elicitedContent));
-            CallToolResult actual = callTool(createToolSpecification("stdio"), exchange, "database_gateway_plan_encrypt_rule", Map.of());
-            assertStructuredFallback(actual, "invalid_elicited_content", true, false, "structured_fallback");
-            verify(exchange).createElicitation(any());
-            mockedToolDefinitionRegistry.verify(() -> ToolDefinitionRegistry.dispatch(any(MCPRequestScope.class), eq(toolDefinition), eq("session-id"), eq(createElicitedArguments())), never());
-        }
-    }
-    
-    private void assertCreateToolSpecificationsFallbackWhenElicitedContentInvalid(final String expectedReason, final McpSchema.ElicitResult elicitedResult) {
-        try (MockedStatic<ToolDefinitionRegistry> mockedToolDefinitionRegistry = mockStatic(ToolDefinitionRegistry.class)) {
-            MCPResponse response = new MCPMapResponse(createClarifyingPayload());
-            MCPToolDefinition toolDefinition = mockSupportedTool(mockedToolDefinitionRegistry, createPlanningToolDescriptor("database_gateway_plan_encrypt_rule"));
-            mockToolDispatch(mockedToolDefinitionRegistry, toolDefinition, Map.of(), response);
-            McpSyncServerExchange exchange = createElicitationExchange(elicitedResult);
-            CallToolResult actual = callTool(createToolSpecification("stdio"), exchange, "database_gateway_plan_encrypt_rule", Map.of());
-            assertStructuredFallback(actual, expectedReason, true, false, "structured_fallback");
-            verify(exchange).createElicitation(any());
-            mockedToolDefinitionRegistry.verify(() -> ToolDefinitionRegistry.dispatch(any(MCPRequestScope.class), eq(toolDefinition), eq("session-id"), eq(createElicitedArguments())), never());
-        }
-    }
-    
-    private CallToolResult createRealDescriptorCallToolResult(final String toolName, final MCPResponse response) {
-        MCPToolDescriptor descriptor = ToolDefinitionRegistry.getToolDefinition(toolName).getDescriptor();
-        return new MCPCallToolResultFactory().create(descriptor, response);
-    }
-    
-    private MCPResponse createMaskPlanResponse() throws ReflectiveOperationException {
-        PlanMaskRuleToolHandler handler = new PlanMaskRuleToolHandler();
-        MaskWorkflowPlanningService planningService = mock(MaskWorkflowPlanningService.class);
-        when(planningService.plan(any(), any(), any(), any(), any())).thenReturn(createMaskSnapshot());
-        setField(handler, "planningService", planningService);
-        return handler.handle(createWorkflowContext(), new MCPToolCall("session-id", Map.of("database", "logic_db", "table", "orders", "column", "phone")));
-    }
-    
-    private WorkflowContextSnapshot createMaskSnapshot() {
-        WorkflowRequest request = createWorkflowRequest();
-        request.getPrimaryAlgorithmProperties().put("first-n", "3");
-        request.getPrimaryAlgorithmProperties().put("last-m", "2");
-        request.getPrimaryAlgorithmProperties().put("replace-char", "*");
-        WorkflowContextSnapshot result = createWorkflowContextSnapshot(MaskFeatureDefinition.WORKFLOW_KIND, request);
-        result.getPropertyRequirements().add(new AlgorithmPropertyRequirement("primary", "first-n", true, false, "from", ""));
-        result.getRuleArtifacts().add(new RuleArtifact("create", "CREATE MASK RULE orders (TYPE(NAME='keep_first_n_last_m'))"));
-        return result;
-    }
-    
-    private MCPResponse createEncryptPlanResponse() throws ReflectiveOperationException {
-        PlanEncryptRuleToolHandler handler = new PlanEncryptRuleToolHandler();
-        EncryptWorkflowPlanningService planningService = mock(EncryptWorkflowPlanningService.class);
-        when(planningService.plan(any(), any(), any(), any(), any())).thenReturn(createEncryptSnapshot());
-        setField(handler, "planningService", planningService);
-        return handler.handle(createWorkflowContext(), new MCPToolCall("session-id", Map.of("database", "logic_db", "table", "orders", "column", "phone")));
-    }
-    
-    private WorkflowContextSnapshot createEncryptSnapshot() {
-        EncryptWorkflowRequest request = new EncryptWorkflowRequest();
-        request.setDatabase("logic_db");
-        request.setSchema("public");
-        request.setTable("orders");
-        request.getPrimaryAlgorithmProperties().put("aes-key-value", "123456");
-        WorkflowContextSnapshot result = createWorkflowContextSnapshot(EncryptFeatureDefinition.WORKFLOW_KIND, request);
-        result.getPropertyRequirements().add(new AlgorithmPropertyRequirement("primary", "aes-key-value", true, true, "key", ""));
-        result.getRuleArtifacts().add(new RuleArtifact("create", "CREATE ENCRYPT RULE orders (PROPERTIES('aes-key-value'='123456'))"));
-        return result;
-    }
-    
-    private WorkflowRequest createWorkflowRequest() {
-        WorkflowRequest result = new WorkflowRequest();
-        result.setDatabase("logic_db");
-        result.setSchema("public");
-        result.setTable("orders");
-        return result;
-    }
-    
-    private WorkflowContextSnapshot createWorkflowContextSnapshot(final WorkflowKind workflowKind, final WorkflowRequest request) {
-        WorkflowContextSnapshot result = new WorkflowContextSnapshot();
-        result.setPlanId("plan-1");
-        result.setWorkflowKind(workflowKind);
-        result.setStatus(WorkflowLifecycle.STATUS_PLANNED);
-        result.setRequest(request);
-        result.setClarifiedIntent(new ClarifiedIntent());
-        result.setInteractionPlan(createInteractionPlan());
-        return result;
-    }
-    
-    private InteractionPlan createInteractionPlan() {
-        InteractionPlan result = new InteractionPlan();
-        result.setCurrentStep("review");
-        result.setDeliveryMode("interactive");
-        result.setExecutionMode("review-then-execute");
-        return result;
-    }
-    
-    private MCPWorkflowHandlerContext createWorkflowContext() {
-        MCPWorkflowHandlerContext result = mock(MCPWorkflowHandlerContext.class);
-        MCPDatabaseHandlerContext databaseContext = mock(MCPDatabaseHandlerContext.class);
-        WorkflowSessionContext workflowSessionContext = new InMemoryWorkflowSessionContext();
-        when(result.getDatabaseContext()).thenReturn(databaseContext);
-        when(result.getWorkflowSessionContext()).thenReturn(workflowSessionContext);
-        when(databaseContext.getMetadataQueryFacade()).thenReturn(mock(MCPMetadataQueryFacade.class));
-        when(databaseContext.getQueryFacade()).thenReturn(mock(MCPFeatureQueryFacade.class));
-        return result;
-    }
-    
-    private void setField(final Object target, final String fieldName, final Object value) throws ReflectiveOperationException {
-        Field field = target.getClass().getDeclaredField(fieldName);
-        Plugins.getMemberAccessor().set(field, target, value);
-    }
-    
-    private void assertCreateToolSpecificationsFallbackWhenElicitationAction(final McpSchema.ElicitResult.Action action) {
-        try (MockedStatic<ToolDefinitionRegistry> mockedToolDefinitionRegistry = mockStatic(ToolDefinitionRegistry.class)) {
-            Map<String, Object> expectedPayload = createClarifyingPayload();
-            MCPResponse response = new MCPMapResponse(expectedPayload);
-            MCPToolDefinition toolDefinition = mockSupportedTool(mockedToolDefinitionRegistry, createPlanningToolDescriptor("database_gateway_plan_encrypt_rule"));
-            mockToolDispatch(mockedToolDefinitionRegistry, toolDefinition, Map.of(), response);
-            McpSyncServerExchange exchange = createElicitationExchange(new McpSchema.ElicitResult(action, Map.of()));
-            CallToolResult actual = callTool(createToolSpecification("stdio"), exchange, "database_gateway_plan_encrypt_rule", Map.of());
-            assertThat(actual.structuredContent(), is(expectedPayload));
-            verify(exchange).createElicitation(any());
-        }
-    }
-    
-    @Test
-    void assertToolOutputSchemaExamplesMatchSchemas() {
-        JsonSchemaValidator validator = new DefaultJsonSchemaValidator();
-        for (MCPToolDescriptor each : ToolDefinitionRegistry.getSupportedToolDescriptors()) {
-            for (Map<String, Object> example : getOutputSchemaExamples(each)) {
-                ValidationResponse actual = validator.validate(each.getOutputSchema(), example);
-                assertTrue(actual.valid(), () -> String.format("Invalid outputSchema example for `%s`: %s", each.getName(), actual.errorMessage()));
-            }
-        }
-    }
-    
-    @SuppressWarnings("unchecked")
-    private List<Map<String, Object>> getOutputSchemaExamples(final MCPToolDescriptor toolDescriptor) {
-        return toolDescriptor.getOutputSchema().containsKey("examples") ? (List<Map<String, Object>>) toolDescriptor.getOutputSchema().get("examples") : List.of();
-    }
-    
 }
diff --git a/mcp/features/sharding/src/main/java/org/apache/shardingsphere/mcp/feature/sharding/tool/service/ShardingWorkflowInputValidator.java b/mcp/features/sharding/src/main/java/org/apache/shardingsphere/mcp/feature/sharding/tool/service/ShardingWorkflowInputValidator.java
new file mode 100644
index 00000000000..60523a39dfc
--- /dev/null
+++ b/mcp/features/sharding/src/main/java/org/apache/shardingsphere/mcp/feature/sharding/tool/service/ShardingWorkflowInputValidator.java
@@ -0,0 +1,180 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.mcp.feature.sharding.tool.service;
+
+import org.apache.shardingsphere.mcp.feature.sharding.tool.model.ShardingWorkflowRequest;
+import org.apache.shardingsphere.mcp.support.workflow.model.WorkflowContextSnapshot;
+import org.apache.shardingsphere.mcp.support.workflow.model.WorkflowIssue;
+import org.apache.shardingsphere.mcp.support.workflow.model.WorkflowIssueCode;
+import org.apache.shardingsphere.mcp.support.workflow.model.WorkflowLifecycle;
+import org.apache.shardingsphere.mcp.support.workflow.service.WorkflowSQLUtils;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+final class ShardingWorkflowInputValidator {
+    
+    boolean hasDatabase(final ShardingWorkflowRequest request, final WorkflowContextSnapshot snapshot) {
+        if (!request.getDatabase().isEmpty()) {
+            return true;
+        }
+        snapshot.getClarifiedIntent().getClarificationMessages().add("Please provide logical database first.");
+        snapshot.setStatus(WorkflowLifecycle.STATUS_CLARIFYING);
+        return false;
+    }
+    
+    boolean ensureIdentifiers(final ShardingWorkflowRequest request, final WorkflowContextSnapshot snapshot) {
+        if (areSupportedIdentifiers(List.of(
+                request.getDatabase(), request.getTable(), request.getColumn(), request.getRuleName(), request.getKeyGeneratorName(), request.getKeyGenerateStrategyName()))
+                && areSupportedIdentifiers(splitCsv(request.getShardingColumns()))) {
+            return true;
+        }
+        snapshot.getIssues().add(new WorkflowIssue(WorkflowIssueCode.UNSUPPORTED_IDENTIFIER, "error", "intaking",
+                "A sharding identifier contains unsupported characters.", "Use reviewable DistSQL identifiers only.", false, Map.of()));
+        snapshot.setStatus(WorkflowLifecycle.STATUS_FAILED);
+        return false;
+    }
+    
+    boolean hasRequiredTableRuleInputs(final ShardingWorkflowRequest request) {
+        if (WorkflowLifecycle.OPERATION_DROP.equalsIgnoreCase(request.getOperationType())) {
+            return require(request, request.getTable(), "Please provide target logical table.");
+        }
+        if (!require(request, request.getTable(), "Please provide target logical table.")
+                || !require(request, request.getDataNodes().isEmpty() ? request.getStorageUnits() : request.getDataNodes(), "Please provide data nodes or storage units.")) {
+            return false;
+        }
+        return (request.getStorageUnits().isEmpty() ? hasRequiredStrategyInputs(request) : hasRequiredAutoTableRuleInputs(request))
+                && hasRequiredTableRuleKeyGenerateInputs(request);
+    }
+    
+    private boolean hasRequiredAutoTableRuleInputs(final ShardingWorkflowRequest request) {
+        return require(request, request.getColumn(), "Please provide auto table sharding column.")
+                && require(request, request.getAlgorithmType(), "Please provide auto table sharding algorithm type.");
+    }
+    
+    private boolean hasRequiredTableRuleKeyGenerateInputs(final ShardingWorkflowRequest request) {
+        if (request.getKeyGenerateColumn().isEmpty() || !request.getKeyGeneratorName().isEmpty()) {
+            return true;
+        }
+        return require(request, request.getKeyGeneratorType(), "Please provide key generator type or key generator name for key generate strategy.");
+    }
+    
+    boolean hasRequiredReferenceRuleInputs(final ShardingWorkflowRequest request) {
+        if (WorkflowLifecycle.OPERATION_DROP.equalsIgnoreCase(request.getOperationType())) {
+            return require(request, request.getRuleName(), "Please provide table reference rule name.");
+        }
+        return require(request, request.getRuleName(), "Please provide table reference rule name.")
+                && require(request, request.getReferenceTables().isEmpty() ? "" : "ok", "Please provide reference tables.");
+    }
+    
+    boolean hasRequiredDefaultStrategyInputs(final ShardingWorkflowRequest request) {
+        if (!require(request, request.getDefaultStrategyType(), "Please provide DATABASE or TABLE default strategy type.")) {
+            return false;
+        }
+        if (!isDefaultStrategyType(request.getDefaultStrategyType())) {
+            request.setFieldSemantics("Please provide default_strategy_type as DATABASE or TABLE.");
+            return false;
+        }
+        if (WorkflowLifecycle.OPERATION_DROP.equalsIgnoreCase(request.getOperationType()) || "none".equalsIgnoreCase(request.getStrategyType())) {
+            return true;
+        }
+        return hasRequiredStrategyInputs(request);
+    }
+    
+    private boolean isDefaultStrategyType(final String defaultStrategyType) {
+        return "DATABASE".equalsIgnoreCase(defaultStrategyType) || "TABLE".equalsIgnoreCase(defaultStrategyType);
+    }
+    
+    boolean hasRequiredStrategyInputs(final ShardingWorkflowRequest request) {
+        switch (normalizeStrategyType(request)) {
+            case "standard":
+                return require(request, request.getColumn(), "Please provide sharding column.")
+                        && require(request, request.getAlgorithmType(), "Please provide sharding algorithm type.");
+            case "complex":
+                return hasRequiredComplexStrategyInputs(request);
+            case "hint":
+                return require(request, request.getAlgorithmType(), "Please provide sharding algorithm type.");
+            case "none":
+                return true;
+            default:
+                request.setFieldSemantics("Please provide strategy_type as standard, complex, hint, or none.");
+                return false;
+        }
+    }
+    
+    private boolean hasRequiredComplexStrategyInputs(final ShardingWorkflowRequest request) {
+        if (!require(request, request.getShardingColumns(), "Please provide at least two sharding columns for complex strategy.")) {
+            return false;
+        }
+        if (splitCsv(request.getShardingColumns()).size() < 2) {
+            request.setFieldSemantics("Please provide at least two sharding columns for complex strategy.");
+            return false;
+        }
+        return require(request, request.getAlgorithmType(), "Please provide sharding algorithm type.");
+    }
+    
+    boolean hasRequiredKeyGeneratorInputs(final ShardingWorkflowRequest request) {
+        if (WorkflowLifecycle.OPERATION_DROP.equalsIgnoreCase(request.getOperationType())) {
+            return require(request, request.getKeyGeneratorName(), "Please provide key generator name.");
+        }
+        return require(request, request.getKeyGeneratorName(), "Please provide key generator name.")
+                && require(request, request.getKeyGeneratorType(), "Please provide key generator type.");
+    }
+    
+    boolean hasRequiredKeyGenerateStrategyInputs(final ShardingWorkflowRequest request) {
+        if (WorkflowLifecycle.OPERATION_DROP.equalsIgnoreCase(request.getOperationType())) {
+            return require(request, request.getKeyGenerateStrategyName(), "Please provide key generate strategy name.");
+        }
+        return require(request, request.getKeyGenerateStrategyName(), "Please provide key generate strategy name.")
+                && require(request, request.getSequenceName().isEmpty() ? request.getTable() : request.getSequenceName(), "Please provide table or sequence.")
+                && require(request, request.getSequenceName().isEmpty() ? request.getColumn() : "ok", "Please provide key generate column.")
+                && require(request, request.getKeyGeneratorName().isEmpty() ? request.getKeyGeneratorType() : request.getKeyGeneratorName(), "Please provide generator or key generator type.");
+    }
+    
+    boolean hasRequiredCleanupInputs(final ShardingWorkflowRequest request, final WorkflowContextSnapshot snapshot) {
+        if (!request.getComponentType().isEmpty() && !request.getComponentName().isEmpty()) {
+            return true;
+        }
+        snapshot.getClarifiedIntent().getClarificationMessages().add("Please provide component type and component name.");
+        snapshot.setStatus(WorkflowLifecycle.STATUS_CLARIFYING);
+        return false;
+    }
+    
+    private boolean require(final ShardingWorkflowRequest request, final String value, final String message) {
+        if (!value.isEmpty()) {
+            return true;
+        }
+        request.setFieldSemantics(message);
+        return false;
+    }
+    
+    List<String> splitCsv(final String value) {
+        return Arrays.stream(value.split(",")).map(String::trim).filter(each -> !each.isEmpty()).toList();
+    }
+    
+    String normalizeStrategyType(final ShardingWorkflowRequest request) {
+        return request.getStrategyType().isEmpty() ? "standard" : request.getStrategyType().trim().toLowerCase(Locale.ENGLISH);
+    }
+    
+    private boolean areSupportedIdentifiers(final Collection<String> identifiers) {
+        return identifiers.stream().allMatch(WorkflowSQLUtils::isSupportedIdentifier);
+    }
+}
diff --git a/mcp/features/sharding/src/main/java/org/apache/shardingsphere/mcp/feature/sharding/tool/service/ShardingWorkflowLifecycleSpec.java b/mcp/features/sharding/src/main/java/org/apache/shardingsphere/mcp/feature/sharding/tool/service/ShardingWorkflowLifecycleSpec.java
new file mode 100644
index 00000000000..d2ef957a67c
--- /dev/null
+++ b/mcp/features/sharding/src/main/java/org/apache/shardingsphere/mcp/feature/sharding/tool/service/ShardingWorkflowLifecycleSpec.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.mcp.feature.sharding.tool.service;
+
+import org.apache.shardingsphere.mcp.feature.sharding.tool.model.ShardingWorkflowRequest;
+import org.apache.shardingsphere.mcp.support.workflow.model.RuleArtifact;
+import org.apache.shardingsphere.mcp.support.workflow.model.WorkflowContextSnapshot;
+import org.apache.shardingsphere.mcp.support.workflow.model.WorkflowKind;
+
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+final class ShardingWorkflowLifecycleSpec {
+    
+    private final WorkflowKind workflowKind;
+    
+    private final String defaultOperationType;
+    
+    private final String summary;
+    
+    private final Function<ShardingWorkflowRequest, Boolean> existsSupplier;
+    
+    private final Function<ShardingWorkflowRequest, Boolean> requiredInputSupplier;
+    
+    private final BiFunction<ShardingWorkflowRequest, WorkflowContextSnapshot, Boolean> algorithmPlanSupplier;
+    
+    private final Function<ShardingWorkflowRequest, RuleArtifact> artifactSupplier;
+    
+    ShardingWorkflowLifecycleSpec(final WorkflowKind workflowKind, final String defaultOperationType, final String summary,
+                                  final Function<ShardingWorkflowRequest, Boolean> existsSupplier, final Function<ShardingWorkflowRequest, Boolean> requiredInputSupplier,
+                                  final BiFunction<ShardingWorkflowRequest, WorkflowContextSnapshot, Boolean> algorithmPlanSupplier,
+                                  final Function<ShardingWorkflowRequest, RuleArtifact> artifactSupplier) {
+        this.workflowKind = workflowKind;
+        this.defaultOperationType = defaultOperationType;
+        this.summary = summary;
+        this.existsSupplier = existsSupplier;
+        this.requiredInputSupplier = requiredInputSupplier;
+        this.algorithmPlanSupplier = algorithmPlanSupplier;
+        this.artifactSupplier = artifactSupplier;
+    }
+    
+    WorkflowKind getWorkflowKind() {
+        return workflowKind;
+    }
+    
+    String getDefaultOperationType() {
+        return defaultOperationType;
+    }
+    
+    String getSummary() {
+        return summary;
+    }
+    
+    Function<ShardingWorkflowRequest, Boolean> getExistsSupplier() {
+        return existsSupplier;
+    }
+    
+    Function<ShardingWorkflowRequest, Boolean> getRequiredInputSupplier() {
+        return requiredInputSupplier;
+    }
+    
+    BiFunction<ShardingWorkflowRequest, WorkflowContextSnapshot, Boolean> getAlgorithmPlanSupplier() {
+        return algorithmPlanSupplier;
+    }
+    
+    Function<ShardingWorkflowRequest, RuleArtifact> getArtifactSupplier() {
+        return artifactSupplier;
+    }
+}
diff --git a/mcp/features/sharding/src/main/java/org/apache/shardingsphere/mcp/feature/sharding/tool/service/ShardingWorkflowPlanningKernel.java b/mcp/features/sharding/src/main/java/org/apache/shardingsphere/mcp/feature/sharding/tool/service/ShardingWorkflowPlanningKernel.java
index 1ba48b384b7..a13295005b7 100644
--- a/mcp/features/sharding/src/main/java/org/apache/shardingsphere/mcp/feature/sharding/tool/service/ShardingWorkflowPlanningKernel.java
+++ b/mcp/features/sharding/src/main/java/org/apache/shardingsphere/mcp/feature/sharding/tool/service/ShardingWorkflowPlanningKernel.java
@@ -24,7 +24,6 @@ import org.apache.shardingsphere.mcp.support.workflow.WorkflowSessionContext;
 import org.apache.shardingsphere.mcp.support.workflow.model.AlgorithmCandidate;
 import org.apache.shardingsphere.mcp.support.workflow.model.AlgorithmPropertyRequirement;
 import org.apache.shardingsphere.mcp.support.workflow.model.ClarifiedIntent;
-import org.apache.shardingsphere.mcp.support.workflow.model.RuleArtifact;
 import org.apache.shardingsphere.mcp.support.workflow.model.WorkflowContextSnapshot;
 import org.apache.shardingsphere.mcp.support.workflow.model.WorkflowIssue;
 import org.apache.shardingsphere.mcp.support.workflow.model.WorkflowIssueCode;
@@ -35,13 +34,10 @@ 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.Arrays;
 import java.util.Collection;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
-import java.util.function.BiFunction;
-import java.util.function.Function;
 
 /**
  * Sharding workflow planning kernel.
@@ -60,6 +56,8 @@ final class ShardingWorkflowPlanningKernel {
     
     private final WorkflowPlanningSupport planningSupport = new WorkflowPlanningSupport();
     
+    private final ShardingWorkflowInputValidator inputValidator = new ShardingWorkflowInputValidator();
+    
     private final ShardingInspectionService inspectionService;
     
     private final ShardingAlgorithmRecommendationService algorithmRecommendationService;
@@ -75,13 +73,6 @@ final class ShardingWorkflowPlanningKernel {
         distSQLPlanningService = new ShardingDistSQLPlanningService();
     }
     
-    ShardingWorkflowPlanningKernel(final ShardingInspectionService inspectionService, final ShardingDistSQLPlanningService distSQLPlanningService) {
-        this.inspectionService = inspectionService;
-        algorithmRecommendationService = new ShardingAlgorithmRecommendationService();
-        algorithmPropertyTemplateService = new ShardingAlgorithmPropertyTemplateService();
-        this.distSQLPlanningService = distSQLPlanningService;
-    }
-    
     /**
      * Plan sharding table rule workflow.
      *
@@ -93,11 +84,12 @@ final class ShardingWorkflowPlanningKernel {
      */
     public WorkflowContextSnapshot planTableRule(final WorkflowSessionContext workflowSessionContext, final MCPFeatureQueryFacade queryFacade,
                                                  final String sessionId, final ShardingWorkflowRequest request) {
-        return planLifecycleWorkflow(workflowSessionContext, sessionId, request, ShardingFeatureDefinition.TABLE_RULE_WORKFLOW_KIND,
-                "create", "Sharding table rule workflow plan.", actual -> !inspectionService.queryTableRule(queryFacade, actual.getDatabase(), actual.getTable()).isEmpty(),
-                this::hasRequiredTableRuleInputs, (actual, snapshot) -> planAlgorithms(queryFacade, actual,
+        return planLifecycleWorkflow(workflowSessionContext, sessionId, request, new ShardingWorkflowLifecycleSpec(
+                ShardingFeatureDefinition.TABLE_RULE_WORKFLOW_KIND, "create", "Sharding table rule workflow plan.",
+                actual -> !inspectionService.queryTableRule(queryFacade, actual.getDatabase(), actual.getTable()).isEmpty(),
+                inputValidator::hasRequiredTableRuleInputs, (actual, snapshot) -> planAlgorithms(queryFacade, actual,
                         shouldPlanShardingAlgorithm(actual), shouldPlanTableRuleKeyGenerator(actual), snapshot),
-                actual -> distSQLPlanningService.planTableRule(actual, actual.getOperationType()));
+                actual -> distSQLPlanningService.planTableRule(actual, actual.getOperationType())));
     }
     
     /**
@@ -111,9 +103,10 @@ final class ShardingWorkflowPlanningKernel {
      */
     public WorkflowContextSnapshot planTableReferenceRule(final WorkflowSessionContext workflowSessionContext, final MCPFeatureQueryFacade queryFacade,
                                                           final String sessionId, final ShardingWorkflowRequest request) {
-        return planLifecycleWorkflow(workflowSessionContext, sessionId, request, ShardingFeatureDefinition.TABLE_REFERENCE_WORKFLOW_KIND,
-                "create", "Sharding table reference rule workflow plan.", actual -> !inspectionService.queryTableReferenceRule(queryFacade, actual.getDatabase(), actual.getRuleName()).isEmpty(),
-                this::hasRequiredReferenceRuleInputs, (actual, snapshot) -> true, actual -> distSQLPlanningService.planTableReferenceRule(actual, actual.getOperationType()));
+        return planLifecycleWorkflow(workflowSessionContext, sessionId, request, new ShardingWorkflowLifecycleSpec(
+                ShardingFeatureDefinition.TABLE_REFERENCE_WORKFLOW_KIND, "create", "Sharding table reference rule workflow plan.",
+                actual -> !inspectionService.queryTableReferenceRule(queryFacade, actual.getDatabase(), actual.getRuleName()).isEmpty(),
+                inputValidator::hasRequiredReferenceRuleInputs, (actual, snapshot) -> true, actual -> distSQLPlanningService.planTableReferenceRule(actual, actual.getOperationType())));
     }
     
     /**
@@ -127,12 +120,13 @@ final class ShardingWorkflowPlanningKernel {
      */
     public WorkflowContextSnapshot planDefaultStrategy(final WorkflowSessionContext workflowSessionContext, final MCPFeatureQueryFacade queryFacade,
                                                        final String sessionId, final ShardingWorkflowRequest request) {
-        return planLifecycleWorkflow(workflowSessionContext, sessionId, request, ShardingFeatureDefinition.DEFAULT_STRATEGY_WORKFLOW_KIND,
-                "create", "Default sharding strategy workflow plan.", actual -> containsDefaultStrategy(inspectionService.queryDefaultStrategy(queryFacade, actual.getDatabase()),
+        return planLifecycleWorkflow(workflowSessionContext, sessionId, request, new ShardingWorkflowLifecycleSpec(
+                ShardingFeatureDefinition.DEFAULT_STRATEGY_WORKFLOW_KIND, "create", "Default sharding strategy workflow plan.",
+                actual -> containsDefaultStrategy(inspectionService.queryDefaultStrategy(queryFacade, actual.getDatabase()),
                         queryFacade.getDatabaseType(actual.getDatabase()), actual.getDefaultStrategyType()),
-                this::hasRequiredDefaultStrategyInputs,
+                inputValidator::hasRequiredDefaultStrategyInputs,
                 (actual, snapshot) -> planAlgorithms(queryFacade, actual, shouldPlanShardingAlgorithm(actual), false, snapshot),
-                actual -> distSQLPlanningService.planDefaultStrategy(actual, actual.getOperationType()));
+                actual -> distSQLPlanningService.planDefaultStrategy(actual, actual.getOperationType())));
     }
     
     /**
@@ -146,10 +140,11 @@ final class ShardingWorkflowPlanningKernel {
      */
     public WorkflowContextSnapshot planKeyGenerator(final WorkflowSessionContext workflowSessionContext, final MCPFeatureQueryFacade queryFacade,
                                                     final String sessionId, final ShardingWorkflowRequest request) {
-        return planLifecycleWorkflow(workflowSessionContext, sessionId, request, ShardingFeatureDefinition.KEY_GENERATOR_WORKFLOW_KIND,
-                "create", "Sharding key generator workflow plan.", actual -> !inspectionService.queryKeyGenerator(queryFacade, actual.getDatabase(), actual.getKeyGeneratorName()).isEmpty(),
-                this::hasRequiredKeyGeneratorInputs, (actual, snapshot) -> planAlgorithms(queryFacade, actual, false, true, snapshot),
-                actual -> distSQLPlanningService.planKeyGenerator(actual, actual.getOperationType()));
+        return planLifecycleWorkflow(workflowSessionContext, sessionId, request, new ShardingWorkflowLifecycleSpec(
+                ShardingFeatureDefinition.KEY_GENERATOR_WORKFLOW_KIND, "create", "Sharding key generator workflow plan.",
+                actual -> !inspectionService.queryKeyGenerator(queryFacade, actual.getDatabase(), actual.getKeyGeneratorName()).isEmpty(),
+                inputValidator::hasRequiredKeyGeneratorInputs, (actual, snapshot) -> planAlgorithms(queryFacade, actual, false, true, snapshot),
+                actual -> distSQLPlanningService.planKeyGenerator(actual, actual.getOperationType())));
     }
     
     /**
@@ -163,12 +158,12 @@ final class ShardingWorkflowPlanningKernel {
      */
     public WorkflowContextSnapshot planKeyGenerateStrategy(final WorkflowSessionContext workflowSessionContext, final MCPFeatureQueryFacade queryFacade,
                                                            final String sessionId, final ShardingWorkflowRequest request) {
-        return planLifecycleWorkflow(workflowSessionContext, sessionId, request, ShardingFeatureDefinition.KEY_GENERATE_STRATEGY_WORKFLOW_KIND,
-                "create", "Sharding key generate strategy workflow plan.",
+        return planLifecycleWorkflow(workflowSessionContext, sessionId, request, new ShardingWorkflowLifecycleSpec(
+                ShardingFeatureDefinition.KEY_GENERATE_STRATEGY_WORKFLOW_KIND, "create", "Sharding key generate strategy workflow plan.",
                 actual -> !inspectionService.queryKeyGenerateStrategy(queryFacade, actual.getDatabase(), actual.getKeyGenerateStrategyName()).isEmpty(),
-                this::hasRequiredKeyGenerateStrategyInputs, (actual, snapshot) -> planAlgorithms(queryFacade, actual, false,
+                inputValidator::hasRequiredKeyGenerateStrategyInputs, (actual, snapshot) -> planAlgorithms(queryFacade, actual, false,
                         shouldPlanKeyGenerateStrategyGenerator(actual), snapshot),
-                actual -> distSQLPlanningService.planKeyGenerateStrategy(actual, actual.getOperationType()));
+                actual -> distSQLPlanningService.planKeyGenerateStrategy(actual, actual.getOperationType())));
     }
     
     /**
@@ -185,7 +180,7 @@ final class ShardingWorkflowPlanningKernel {
         WorkflowContextSnapshot result = prepareSnapshot(workflowSessionContext, sessionId, request, ShardingFeatureDefinition.COMPONENT_CLEANUP_WORKFLOW_KIND,
                 WorkflowLifecycle.OPERATION_DROP, "Sharding rule component cleanup workflow plan.");
         ShardingWorkflowRequest mergedRequest = (ShardingWorkflowRequest) result.getRequest();
-        if (!hasDatabase(mergedRequest, result) || !hasRequiredCleanupInputs(mergedRequest, result)) {
+        if (!inputValidator.hasDatabase(mergedRequest, result) || !inputValidator.hasRequiredCleanupInputs(mergedRequest, result)) {
             return workflowSessionContext.persist(result, WorkflowLifecycle.STEP_CLARIFYING, result.getStatus());
         }
         if (!ensureCleanupDropOnly(mergedRequest, result)) {
@@ -213,31 +208,28 @@ final class ShardingWorkflowPlanningKernel {
     }
     
     private WorkflowContextSnapshot planLifecycleWorkflow(final WorkflowSessionContext workflowSessionContext, final String sessionId, final ShardingWorkflowRequest request,
-                                                          final WorkflowKind workflowKind, final String defaultOperationType, final String summary,
-                                                          final Function<ShardingWorkflowRequest, Boolean> existsSupplier, final Function<ShardingWorkflowRequest, Boolean> requiredInputSupplier,
-                                                          final BiFunction<ShardingWorkflowRequest, WorkflowContextSnapshot, Boolean> algorithmPlanSupplier,
-                                                          final Function<ShardingWorkflowRequest, RuleArtifact> artifactSupplier) {
-        WorkflowContextSnapshot result = prepareSnapshot(workflowSessionContext, sessionId, request, workflowKind, defaultOperationType, summary);
+                                                          final ShardingWorkflowLifecycleSpec spec) {
+        WorkflowContextSnapshot result = prepareSnapshot(workflowSessionContext, sessionId, request, spec.getWorkflowKind(), spec.getDefaultOperationType(), spec.getSummary());
         ShardingWorkflowRequest mergedRequest = (ShardingWorkflowRequest) result.getRequest();
-        if (!hasDatabase(mergedRequest, result)) {
+        if (!inputValidator.hasDatabase(mergedRequest, result)) {
             return workflowSessionContext.persist(result, WorkflowLifecycle.STEP_CLARIFYING, result.getStatus());
         }
-        if (!ensureIdentifiers(mergedRequest, result)) {
+        if (!inputValidator.ensureIdentifiers(mergedRequest, result)) {
             return workflowSessionContext.persist(result, WorkflowLifecycle.STEP_FAILED, result.getStatus());
         }
-        if (!requiredInputSupplier.apply(mergedRequest)) {
+        if (!spec.getRequiredInputSupplier().apply(mergedRequest)) {
             result.getClarifiedIntent().getClarificationMessages().add(
                     mergedRequest.getFieldSemantics().isEmpty() ? "Please provide the missing sharding planning inputs." : mergedRequest.getFieldSemantics());
             result.setStatus(WorkflowLifecycle.STATUS_CLARIFYING);
             return workflowSessionContext.persist(result, WorkflowLifecycle.STEP_CLARIFYING, result.getStatus());
         }
-        if (!planningSupport.ensureLifecycleState(summary, result.getClarifiedIntent(), existsSupplier.apply(mergedRequest), result)) {
+        if (!planningSupport.ensureLifecycleState(spec.getSummary(), result.getClarifiedIntent(), spec.getExistsSupplier().apply(mergedRequest), result)) {
             return workflowSessionContext.persist(result, WorkflowLifecycle.STEP_FAILED, WorkflowLifecycle.STATUS_FAILED);
         }
-        if (!algorithmPlanSupplier.apply(mergedRequest, result)) {
+        if (!spec.getAlgorithmPlanSupplier().apply(mergedRequest, result)) {
             return workflowSessionContext.persist(result, WorkflowLifecycle.STEP_CLARIFYING, WorkflowLifecycle.STATUS_CLARIFYING);
         }
-        result.getRuleArtifacts().add(artifactSupplier.apply(mergedRequest));
+        result.getRuleArtifacts().add(spec.getArtifactSupplier().apply(mergedRequest));
         return workflowSessionContext.persist(result, WorkflowLifecycle.STEP_REVIEW, WorkflowLifecycle.STATUS_PLANNED);
     }
     
@@ -258,140 +250,6 @@ final class ShardingWorkflowPlanningKernel {
         return result;
     }
     
-    private boolean hasDatabase(final ShardingWorkflowRequest request, final WorkflowContextSnapshot snapshot) {
-        if (!request.getDatabase().isEmpty()) {
-            return true;
-        }
-        snapshot.getClarifiedIntent().getClarificationMessages().add("Please provide logical database first.");
-        snapshot.setStatus(WorkflowLifecycle.STATUS_CLARIFYING);
-        return false;
-    }
-    
-    private boolean ensureIdentifiers(final ShardingWorkflowRequest request, final WorkflowContextSnapshot snapshot) {
-        if (areSupportedIdentifiers(List.of(
-                request.getDatabase(), request.getTable(), request.getColumn(), request.getRuleName(), request.getKeyGeneratorName(), request.getKeyGenerateStrategyName()))
-                && areSupportedIdentifiers(splitCsv(request.getShardingColumns()))) {
-            return true;
-        }
-        snapshot.getIssues().add(new WorkflowIssue(WorkflowIssueCode.UNSUPPORTED_IDENTIFIER, "error", "intaking",
-                "A sharding identifier contains unsupported characters.", "Use reviewable DistSQL identifiers only.", false, Map.of()));
-        snapshot.setStatus(WorkflowLifecycle.STATUS_FAILED);
-        return false;
-    }
-    
-    private boolean hasRequiredTableRuleInputs(final ShardingWorkflowRequest request) {
-        if (WorkflowLifecycle.OPERATION_DROP.equalsIgnoreCase(request.getOperationType())) {
-            return require(request, request.getTable(), "Please provide target logical table.");
-        }
-        if (!require(request, request.getTable(), "Please provide target logical table.")
-                || !require(request, request.getDataNodes().isEmpty() ? request.getStorageUnits() : request.getDataNodes(), "Please provide data nodes or storage units.")) {
-            return false;
-        }
-        return (request.getStorageUnits().isEmpty() ? hasRequiredStrategyInputs(request) : hasRequiredAutoTableRuleInputs(request))
-                && hasRequiredTableRuleKeyGenerateInputs(request);
-    }
-    
-    private boolean hasRequiredAutoTableRuleInputs(final ShardingWorkflowRequest request) {
-        return require(request, request.getColumn(), "Please provide auto table sharding column.")
-                && require(request, request.getAlgorithmType(), "Please provide auto table sharding algorithm type.");
-    }
-    
-    private boolean hasRequiredTableRuleKeyGenerateInputs(final ShardingWorkflowRequest request) {
-        if (request.getKeyGenerateColumn().isEmpty() || !request.getKeyGeneratorName().isEmpty()) {
-            return true;
-        }
-        return require(request, request.getKeyGeneratorType(), "Please provide key generator type or key generator name for key generate strategy.");
-    }
-    
-    private boolean hasRequiredReferenceRuleInputs(final ShardingWorkflowRequest request) {
-        if (WorkflowLifecycle.OPERATION_DROP.equalsIgnoreCase(request.getOperationType())) {
-            return require(request, request.getRuleName(), "Please provide table reference rule name.");
-        }
-        return require(request, request.getRuleName(), "Please provide table reference rule name.")
-                && require(request, request.getReferenceTables().isEmpty() ? "" : "ok", "Please provide reference tables.");
-    }
-    
-    private boolean hasRequiredDefaultStrategyInputs(final ShardingWorkflowRequest request) {
-        if (!require(request, request.getDefaultStrategyType(), "Please provide DATABASE or TABLE default strategy type.")) {
-            return false;
-        }
-        if (!isDefaultStrategyType(request.getDefaultStrategyType())) {
-            request.setFieldSemantics("Please provide default_strategy_type as DATABASE or TABLE.");
-            return false;
-        }
-        if (WorkflowLifecycle.OPERATION_DROP.equalsIgnoreCase(request.getOperationType()) || "none".equalsIgnoreCase(request.getStrategyType())) {
-            return true;
-        }
-        return hasRequiredStrategyInputs(request);
-    }
-    
-    private boolean isDefaultStrategyType(final String defaultStrategyType) {
-        return "DATABASE".equalsIgnoreCase(defaultStrategyType) || "TABLE".equalsIgnoreCase(defaultStrategyType);
-    }
-    
-    private boolean hasRequiredStrategyInputs(final ShardingWorkflowRequest request) {
-        switch (normalizeStrategyType(request)) {
-            case "standard":
-                return require(request, request.getColumn(), "Please provide sharding column.")
-                        && require(request, request.getAlgorithmType(), "Please provide sharding algorithm type.");
-            case "complex":
-                return hasRequiredComplexStrategyInputs(request);
-            case "hint":
-                return require(request, request.getAlgorithmType(), "Please provide sharding algorithm type.");
-            case "none":
-                return true;
-            default:
-                request.setFieldSemantics("Please provide strategy_type as standard, complex, hint, or none.");
-                return false;
-        }
-    }
-    
-    private boolean hasRequiredComplexStrategyInputs(final ShardingWorkflowRequest request) {
-        if (!require(request, request.getShardingColumns(), "Please provide at least two sharding columns for complex strategy.")) {
-            return false;
-        }
-        if (splitCsv(request.getShardingColumns()).size() < 2) {
-            request.setFieldSemantics("Please provide at least two sharding columns for complex strategy.");
-            return false;
-        }
-        return require(request, request.getAlgorithmType(), "Please provide sharding algorithm type.");
-    }
-    
-    private boolean hasRequiredKeyGeneratorInputs(final ShardingWorkflowRequest request) {
-        if (WorkflowLifecycle.OPERATION_DROP.equalsIgnoreCase(request.getOperationType())) {
-            return require(request, request.getKeyGeneratorName(), "Please provide key generator name.");
-        }
-        return require(request, request.getKeyGeneratorName(), "Please provide key generator name.")
-                && require(request, request.getKeyGeneratorType(), "Please provide key generator type.");
-    }
-    
-    private boolean hasRequiredKeyGenerateStrategyInputs(final ShardingWorkflowRequest request) {
-        if (WorkflowLifecycle.OPERATION_DROP.equalsIgnoreCase(request.getOperationType())) {
-            return require(request, request.getKeyGenerateStrategyName(), "Please provide key generate strategy name.");
-        }
-        return require(request, request.getKeyGenerateStrategyName(), "Please provide key generate strategy name.")
-                && require(request, request.getSequenceName().isEmpty() ? request.getTable() : request.getSequenceName(), "Please provide table or sequence.")
-                && require(request, request.getSequenceName().isEmpty() ? request.getColumn() : "ok", "Please provide key generate column.")
-                && require(request, request.getKeyGeneratorName().isEmpty() ? request.getKeyGeneratorType() : request.getKeyGeneratorName(), "Please provide generator or key generator type.");
-    }
-    
-    private boolean hasRequiredCleanupInputs(final ShardingWorkflowRequest request, final WorkflowContextSnapshot snapshot) {
-        if (!request.getComponentType().isEmpty() && !request.getComponentName().isEmpty()) {
-            return true;
-        }
-        snapshot.getClarifiedIntent().getClarificationMessages().add("Please provide component type and component name.");
-        snapshot.setStatus(WorkflowLifecycle.STATUS_CLARIFYING);
-        return false;
-    }
-    
-    private boolean require(final ShardingWorkflowRequest request, final String value, final String message) {
-        if (!value.isEmpty()) {
-            return true;
-        }
-        request.setFieldSemantics(message);
-        return false;
-    }
-    
     private boolean containsDefaultStrategy(final List<Map<String, Object>> rows, final String databaseType, final String defaultStrategyType) {
         return rows.stream().anyMatch(each -> WorkflowSQLUtils.isSameIdentifier(
                 databaseType, defaultStrategyType, WorkflowRuleValueUtils.getRuleValue(each, "name")) && !WorkflowRuleValueUtils.getRuleValue(each, "type").isEmpty());
@@ -433,7 +291,7 @@ final class ShardingWorkflowPlanningKernel {
     }
     
     private boolean shouldPlanShardingAlgorithm(final ShardingWorkflowRequest request) {
-        return !WorkflowLifecycle.OPERATION_DROP.equalsIgnoreCase(request.getOperationType()) && !"none".equalsIgnoreCase(normalizeStrategyType(request));
+        return !WorkflowLifecycle.OPERATION_DROP.equalsIgnoreCase(request.getOperationType()) && !"none".equalsIgnoreCase(inputValidator.normalizeStrategyType(request));
     }
     
     private boolean shouldPlanTableRuleKeyGenerator(final ShardingWorkflowRequest request) {
@@ -474,18 +332,6 @@ final class ShardingWorkflowPlanningKernel {
         return rows.stream().anyMatch(each -> WorkflowSQLUtils.isSameIdentifier(databaseType, expected, WorkflowRuleValueUtils.getRuleValue(each, fieldName)));
     }
     
-    private boolean areSupportedIdentifiers(final Collection<String> identifiers) {
-        return identifiers.stream().allMatch(WorkflowSQLUtils::isSupportedIdentifier);
-    }
-    
-    private List<String> splitCsv(final String value) {
-        return Arrays.stream(value.split(",")).map(String::trim).filter(each -> !each.isEmpty()).toList();
-    }
-    
-    private String normalizeStrategyType(final ShardingWorkflowRequest request) {
-        return request.getStrategyType().isEmpty() ? "standard" : request.getStrategyType().trim().toLowerCase(Locale.ENGLISH);
-    }
-    
     private String normalizeComponentType(final String componentType) {
         return componentType.trim().toLowerCase(Locale.ENGLISH).replace('_', '-');
     }
diff --git a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPCompletionTargetDescriptorValidator.java b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPCompletionTargetDescriptorValidator.java
new file mode 100644
index 00000000000..e914ba5eb92
--- /dev/null
+++ b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPCompletionTargetDescriptorValidator.java
@@ -0,0 +1,127 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.mcp.support.descriptor;
+
+import org.apache.shardingsphere.infra.exception.ShardingSpherePreconditions;
+import org.apache.shardingsphere.mcp.api.prompt.descriptor.MCPPromptArgumentDescriptor;
+import org.apache.shardingsphere.mcp.api.prompt.descriptor.MCPPromptDescriptor;
+import org.apache.shardingsphere.mcp.api.resource.descriptor.MCPResourceDescriptor;
+import org.apache.shardingsphere.mcp.support.resource.MCPUriTemplate;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Validator for MCP completion targets.
+ */
+public final class MCPCompletionTargetDescriptorValidator {
+    
+    private MCPCompletionTargetDescriptorValidator() {
+    }
+    
+    /**
+     * Validate completion targets against declared prompt and resource descriptors.
+     *
+     * @param descriptors completion target descriptors
+     * @param prompts prompt descriptors
+     * @param resources resource descriptors
+     */
+    public static void validate(final Collection<MCPCompletionTargetDescriptor> descriptors, final Collection<MCPPromptDescriptor> prompts,
+                                final Collection<MCPResourceDescriptor> resources) {
+        Map<String, Set<String>> promptArguments = prompts.stream().collect(Collectors.toMap(MCPPromptDescriptor::getName,
+                each -> each.getArguments().stream().map(MCPPromptArgumentDescriptor::getName).collect(Collectors.toSet())));
+        Set<String> promptNames = promptArguments.keySet();
+        Map<String, MCPResourceDescriptor> resourceDescriptors = resources.stream().collect(Collectors.toMap(MCPResourceDescriptor::getUriTemplate, each -> each));
+        Map<String, MCPCompletionTargetDescriptor> registered = new LinkedHashMap<>(descriptors.size(), 1F);
+        for (MCPCompletionTargetDescriptor each : descriptors) {
+            validateCompletionReference(each, promptNames, resourceDescriptors.keySet());
+            validatePromptCompletionArguments(each, promptArguments);
+            validateResourceCompletionArguments(each, resourceDescriptors);
+            validateCompletionRequiredContextArguments(each);
+            ShardingSpherePreconditions.checkState(null == registered.putIfAbsent(each.getReferenceType() + ":" + each.getReference(), each),
+                    () -> new IllegalStateException(String.format("Duplicate MCP completion target `%s:%s`.", each.getReferenceType(), each.getReference())));
+        }
+    }
+    
+    private static void validateCompletionRequiredContextArguments(final MCPCompletionTargetDescriptor descriptor) {
+        Object value = descriptor.getMeta().get(MCPShardingSphereMetadataKeys.REQUIRED_CONTEXT_ARGUMENTS);
+        if (null == value) {
+            return;
+        }
+        ShardingSpherePreconditions.checkState(value instanceof Map,
+                () -> new IllegalStateException(String.format("Completion target `%s:%s` metadata `%s` must be an object.",
+                        descriptor.getReferenceType(), descriptor.getReference(), MCPShardingSphereMetadataKeys.REQUIRED_CONTEXT_ARGUMENTS)));
+        Set<String> argumentNames = new HashSet<>(descriptor.getArguments());
+        for (Entry<?, ?> entry : ((Map<?, ?>) value).entrySet()) {
+            String argumentName = String.valueOf(entry.getKey());
+            ShardingSpherePreconditions.checkState(argumentNames.contains(argumentName),
+                    () -> new IllegalStateException(String.format("Completion target `%s:%s` required context argument `%s` is not declared by the target.",
+                            descriptor.getReferenceType(), descriptor.getReference(), argumentName)));
+            ShardingSpherePreconditions.checkState(entry.getValue() instanceof Collection,
+                    () -> new IllegalStateException(String.format("Completion target `%s:%s` required context for `%s` must be a list.",
+                            descriptor.getReferenceType(), descriptor.getReference(), argumentName)));
+            for (Object each : (Collection<?>) entry.getValue()) {
+                ShardingSpherePreconditions.checkState(argumentNames.contains(String.valueOf(each)),
+                        () -> new IllegalStateException(String.format("Completion target `%s:%s` context argument `%s` for `%s` is not declared by the target.",
+                                descriptor.getReferenceType(), descriptor.getReference(), each, argumentName)));
+            }
+        }
+    }
+    
+    private static void validatePromptCompletionArguments(final MCPCompletionTargetDescriptor descriptor, final Map<String, Set<String>> promptArguments) {
+        if (!"prompt".equals(descriptor.getReferenceType())) {
+            return;
+        }
+        Set<String> argumentNames = promptArguments.getOrDefault(descriptor.getReference(), Set.of());
+        for (String each : descriptor.getArguments()) {
+            ShardingSpherePreconditions.checkState(argumentNames.contains(each), () -> new IllegalStateException(
+                    String.format("Completion target `prompt:%s` argument `%s` is not declared by prompt `%s`.", descriptor.getReference(), each, descriptor.getReference())));
+        }
+    }
+    
+    private static void validateResourceCompletionArguments(final MCPCompletionTargetDescriptor descriptor, final Map<String, MCPResourceDescriptor> resources) {
+        if (!"resource".equals(descriptor.getReferenceType())) {
+            return;
+        }
+        MCPResourceDescriptor resource = resources.get(descriptor.getReference());
+        ShardingSpherePreconditions.checkState(resource.isTemplated(),
+                () -> new IllegalStateException(String.format("Completion target `resource:%s` must reference a resource template.", descriptor.getReference())));
+        Collection<String> templateVariables = new MCPUriTemplate(resource.getUriTemplate()).getVariableNames();
+        for (String each : descriptor.getArguments()) {
+            ShardingSpherePreconditions.checkState(templateVariables.contains(each), () -> new IllegalStateException(
+                    String.format("Completion target `resource:%s` argument `%s` is not a URI template variable.", descriptor.getReference(), each)));
+        }
+    }
+    
+    private static void validateCompletionReference(final MCPCompletionTargetDescriptor descriptor, final Set<String> promptNames, final Set<String> resourceUris) {
+        if ("prompt".equals(descriptor.getReferenceType())) {
+            ShardingSpherePreconditions.checkState(promptNames.contains(descriptor.getReference()),
+                    () -> new IllegalStateException(String.format("Completion target references unknown prompt `%s`.", descriptor.getReference())));
+            return;
+        }
+        ShardingSpherePreconditions.checkState("resource".equals(descriptor.getReferenceType()),
+                () -> new IllegalStateException(String.format("Unsupported completion reference type `%s`.", descriptor.getReferenceType())));
+        ShardingSpherePreconditions.checkState(resourceUris.contains(descriptor.getReference()),
+                () -> new IllegalStateException(String.format("Completion target references unknown resource `%s`.", descriptor.getReference())));
+    }
+}
diff --git a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPDescriptorCatalogValidator.java b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPDescriptorCatalogValidator.java
index 9dc9a6a8361..fbc4315a14e 100644
--- a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPDescriptorCatalogValidator.java
+++ b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPDescriptorCatalogValidator.java
@@ -17,611 +17,16 @@
 
 package org.apache.shardingsphere.mcp.support.descriptor;
 
-import org.apache.shardingsphere.infra.exception.ShardingSpherePreconditions;
-import org.apache.shardingsphere.mcp.api.prompt.descriptor.MCPPromptArgumentDescriptor;
-import org.apache.shardingsphere.mcp.api.prompt.descriptor.MCPPromptDescriptor;
-import org.apache.shardingsphere.mcp.api.resource.descriptor.MCPResourceDescriptor;
-import org.apache.shardingsphere.mcp.api.tool.descriptor.MCPToolDescriptor;
-import org.apache.shardingsphere.mcp.support.protocol.MCPPayloadFieldNames;
-import org.apache.shardingsphere.mcp.support.protocol.MCPResponseMode;
-import org.apache.shardingsphere.mcp.support.resource.MCPUriTemplate;
-
-import java.util.Collection;
-import java.util.HashSet;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Optional;
-import java.util.Set;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import java.util.stream.Collectors;
-
 final class MCPDescriptorCatalogValidator {
     
-    private static final Collection<String> REMOVED_MODEL_FACING_FIELDS = Set.of(
-            "target_tool", "target_resource", "required_arguments", "action_kind", "suggested_next_tool", "suggested_next_tools", "recommended_next_tool",
-            "recommended_recovery", "suggested_next_action", "approved_by_user", "requires_user_approval", "approval_required", "user_overrides");
-    
-    private static final Collection<String> SUPPORTED_INPUT_SCHEMA_TOP_LEVEL_FIELDS = Set.of("type", "properties", "required", "additionalProperties");
-    
-    private static final Map<String, Collection<String>> NEXT_ACTION_ALLOWED_FIELDS = createNextActionAllowedFields();
-    
-    private static final Collection<String> NEXT_ACTION_SCHEMA_ALLOWED_FIELDS = createNextActionSchemaAllowedFields();
-    
-    private static final Collection<String> MODEL_CRITICAL_HINT_FIELDS = List.of(
-            MCPPayloadFieldNames.NEXT_ACTIONS, MCPPayloadFieldNames.RESOURCES_TO_READ, MCPPayloadFieldNames.RESOURCE, MCPPayloadFieldNames.PARENT_RESOURCE,
-            MCPPayloadFieldNames.NEXT_RESOURCES, "manual_artifact_summary", "manual_follow_up", "empty_state", "ambiguity_state", MCPPayloadFieldNames.RECOVERY, "recovery_guidance",
-            "remediation");
-    
-    private static final Collection<String> CONTINUATION_MODES = List.of("none", "pagination", "metadata_search");
-    
-    private static final Collection<String> RECOVERY_CATEGORIES = List.of("not_found", "ambiguous", "empty_scope", "missing_context", "validation", "terminal",
-            "unsupported_target", "invalid_enum", "unsafe_sql", "stale_workflow", "unavailable_runtime", "terminal_operator_action");
-    
-    private static final String CLIENT_FORM_ONLY_ARGUMENTS = "org.apache.shardingsphere/client-form-only-arguments";
-    
-    private static final Pattern SINGLE_BRACE_PLACEHOLDER_PATTERN = Pattern.compile("(?<!\\{)\\{\\s*([a-zA-Z0-9_.-]+)\\s*}(?!})");
-    
     private MCPDescriptorCatalogValidator() {
     }
     
-    private static Map<String, Collection<String>> createNextActionAllowedFields() {
-        return Map.of(
-                "resource_read", Set.of("order", "type", "title", "resource_uri", "reason", "depends_on"),
-                "tool_call", Set.of("order", "type", "title", "tool_name", "arguments", "reason", "depends_on"),
-                "completion", Set.of("order", "type", "title", "ref", "argument", "context", "missing_context_arguments", "resume_ref", "resume_arguments", "reason", "depends_on"),
-                "ask_user", Set.of("order", "type", "title", "question", "required_inputs", "reason", "depends_on"),
-                "terminal", Set.of("order", "type", "title", "reason", "depends_on"));
-    }
-    
-    private static Collection<String> createNextActionSchemaAllowedFields() {
-        Set<String> result = new HashSet<>();
-        for (Collection<String> each : NEXT_ACTION_ALLOWED_FIELDS.values()) {
-            result.addAll(each);
-        }
-        return result;
-    }
-    
     static void validate(final MCPDescriptorCatalog catalog) {
-        validateResourceDescriptors(catalog);
-        validateToolDescriptors(catalog);
-        validatePromptDescriptors(catalog.getPromptDescriptors(), catalog.getPromptTemplateBindings());
-        validateCompletionTargetDescriptors(catalog.getCompletionTargetDescriptors(), catalog.getPromptDescriptors(), catalog.getAllResourceDescriptors());
-        validateResourceNavigationDescriptors(catalog.getResourceNavigationDescriptors(), catalog);
-    }
-    
-    private static void validateResourceDescriptors(final MCPDescriptorCatalog catalog) {
-        Map<String, MCPResourceDescriptor> registered = new LinkedHashMap<>(catalog.getResourceDescriptors().size() + catalog.getResourceTemplateDescriptors().size(), 1F);
-        for (MCPResourceDescriptor each : catalog.getResourceDescriptors()) {
-            checkNotBlank(each.getUriTemplate(), "Resource URI");
-            ShardingSpherePreconditions.checkState(!each.isTemplated(),
-                    () -> new IllegalStateException(String.format("Fixed resource `%s` must not contain template variables.", each.getUriTemplate())));
-            validateResourceDescriptor(each, registered);
-        }
-        for (MCPResourceDescriptor each : catalog.getResourceTemplateDescriptors()) {
-            checkNotBlank(each.getUriTemplate(), "Resource template URI template");
-            ShardingSpherePreconditions.checkState(each.isTemplated(),
-                    () -> new IllegalStateException(String.format("Resource template `%s` must contain template variables.", each.getUriTemplate())));
-            validateResourceDescriptor(each, registered);
-            validateResourceVariables(each, findShardingSphereResourceMetadata(catalog, each.getUriTemplate()).map(ShardingSphereMCPResourceMetadata::getUriVariables).orElse(List.of()));
-        }
-    }
-    
-    private static void validateResourceDescriptor(final MCPResourceDescriptor descriptor, final Map<String, MCPResourceDescriptor> registered) {
-        ShardingSpherePreconditions.checkState(null == registered.putIfAbsent(descriptor.getUriTemplate(), descriptor),
-                () -> new IllegalStateException(String.format("Duplicate MCP resource descriptor `%s`.", descriptor.getUriTemplate())));
-    }
-    
-    private static Optional<ShardingSphereMCPResourceMetadata> findShardingSphereResourceMetadata(final MCPDescriptorCatalog catalog, final String uriOrTemplate) {
-        return catalog.getShardingSphereResourceMetadata().stream().filter(each -> uriOrTemplate.equals(each.getUriOrTemplate())).findFirst();
-    }
-    
-    private static void validateResourceVariables(final MCPResourceDescriptor descriptor, final Collection<MCPUriVariableDescriptor> uriVariables) {
-        List<String> templateVariables = new MCPUriTemplate(descriptor.getUriTemplate()).getVariableNames();
-        Set<String> registeredTemplateVariables = new HashSet<>();
-        for (String each : templateVariables) {
-            ShardingSpherePreconditions.checkState(registeredTemplateVariables.add(each),
-                    () -> new IllegalStateException(String.format("Duplicate URI template variable `%s` in resource descriptor `%s`.", each, descriptor.getUriTemplate())));
-        }
-        Map<String, MCPUriVariableDescriptor> declaredParameters = new LinkedHashMap<>(uriVariables.size(), 1F);
-        for (MCPUriVariableDescriptor each : uriVariables) {
-            ShardingSpherePreconditions.checkState(each.isRequired(), () -> new IllegalStateException(
-                    String.format("Resource parameter `%s.%s` must be required because URI template variables are required.", descriptor.getUriTemplate(), each.getName())));
-            ShardingSpherePreconditions.checkState(registeredTemplateVariables.contains(each.getName()),
-                    () -> new IllegalStateException(String.format("Resource descriptor `%s` declares non-template parameter `%s`.", descriptor.getUriTemplate(), each.getName())));
-            ShardingSpherePreconditions.checkState(null == declaredParameters.putIfAbsent(each.getName(), each),
-                    () -> new IllegalStateException(String.format("Duplicate MCP resource parameter `%s.%s`.", descriptor.getUriTemplate(), each.getName())));
-        }
-        for (String variableName : templateVariables) {
-            ShardingSpherePreconditions.checkState(declaredParameters.containsKey(variableName),
-                    () -> new IllegalStateException(String.format("Resource descriptor `%s` must describe URI template variable `%s`.", descriptor.getUriTemplate(), variableName)));
-        }
-    }
-    
-    private static void validateToolDescriptors(final MCPDescriptorCatalog catalog) {
-        Collection<MCPToolDescriptor> descriptors = catalog.getToolDescriptors();
-        Collection<MCPToolRuntimeDescriptor> runtimeDescriptors = catalog.getToolRuntimeDescriptors();
-        Collection<MCPToolDescriptorValidator> descriptorValidators = MCPToolDescriptorValidatorLoader.load();
-        Map<String, MCPToolDescriptor> registered = new LinkedHashMap<>(descriptors.size(), 1F);
-        Map<String, MCPToolRuntimeDescriptor> runtimes = runtimeDescriptors.stream()
-                .collect(Collectors.toMap(MCPToolRuntimeDescriptor::getToolName, each -> each));
-        Set<String> resourceIdentifiers = catalog.getAllResourceDescriptors().stream().map(MCPResourceDescriptor::getUriTemplate).collect(Collectors.toSet());
-        Set<String> shardingSphereResourceIdentifiers = catalog.getShardingSphereResourceMetadata().stream()
-                .map(ShardingSphereMCPResourceMetadata::getUriOrTemplate).collect(Collectors.toSet());
-        for (MCPToolDescriptor each : descriptors) {
-            ShardingSpherePreconditions.checkState(null == registered.putIfAbsent(each.getName(), each),
-                    () -> new IllegalStateException(String.format("Duplicate MCP tool descriptor `%s`.", each.getName())));
-            validateToolInputSchema(each);
-            validateToolOutputSchema(each, descriptorValidators);
-            validateDestructiveToolDescriptor(each, runtimes.get(each.getName()));
-            validatePlanningExecutionMode(each);
-            validateRelatedResourceUris(each, resourceIdentifiers, shardingSphereResourceIdentifiers);
-        }
-        validatePlanningToolRuntimeDescriptors(registered, runtimeDescriptors);
-    }
-    
-    private static void validateToolInputSchema(final MCPToolDescriptor descriptor) {
-        Map<String, Object> inputSchema = descriptor.getInputSchema();
-        for (String each : inputSchema.keySet()) {
-            ShardingSpherePreconditions.checkState(SUPPORTED_INPUT_SCHEMA_TOP_LEVEL_FIELDS.contains(each),
-                    () -> new IllegalStateException(String.format("Tool `%s` inputSchema contains unsupported top-level field `%s`.", descriptor.getName(), each)));
-        }
-        ShardingSpherePreconditions.checkState("object".equals(inputSchema.get("type")),
-                () -> new IllegalStateException(String.format("Tool `%s` inputSchema must be an object.", descriptor.getName())));
-        Object properties = inputSchema.get("properties");
-        ShardingSpherePreconditions.checkState(properties instanceof Map, () -> new IllegalStateException(String.format("Tool `%s` inputSchema must declare properties.", descriptor.getName())));
-        validateNoRemovedModelFacingFields(descriptor, inputSchema);
-    }
-    
-    private static void validateRelatedResourceUris(final MCPToolDescriptor descriptor, final Set<String> resourceIdentifiers, final Set<String> shardingSphereResourceIdentifiers) {
-        Object value = descriptor.getMeta().get(MCPShardingSphereMetadataKeys.RELATED_RESOURCE_URIS);
-        if (null == value) {
-            return;
-        }
-        ShardingSpherePreconditions.checkState(value instanceof Collection,
-                () -> new IllegalStateException(String.format("Tool `%s` metadata `%s` must be a list.", descriptor.getName(), MCPShardingSphereMetadataKeys.RELATED_RESOURCE_URIS)));
-        for (Object each : (Collection<?>) value) {
-            String uri = String.valueOf(each);
-            ShardingSpherePreconditions.checkState(resourceIdentifiers.contains(uri),
-                    () -> new IllegalStateException(String.format("Tool `%s` metadata `%s` references unknown resource `%s`.",
-                            descriptor.getName(), MCPShardingSphereMetadataKeys.RELATED_RESOURCE_URIS, uri)));
-            ShardingSpherePreconditions.checkState(shardingSphereResourceIdentifiers.contains(uri),
-                    () -> new IllegalStateException(String.format("Tool `%s` metadata `%s` references resource `%s` without ShardingSphere metadata.",
-                            descriptor.getName(), MCPShardingSphereMetadataKeys.RELATED_RESOURCE_URIS, uri)));
-        }
-    }
-    
-    private static void validatePlanningToolRuntimeDescriptors(final Map<String, MCPToolDescriptor> descriptors, final Collection<MCPToolRuntimeDescriptor> runtimeDescriptors) {
-        Map<String, String> workflowKinds = new LinkedHashMap<>(runtimeDescriptors.size(), 1F);
-        for (MCPToolRuntimeDescriptor each : runtimeDescriptors) {
-            if (!"plan".equals(each.getWorkflowRole())) {
-                continue;
-            }
-            MCPToolDescriptor descriptor = descriptors.get(each.getToolName());
-            ShardingSpherePreconditions.checkState(null != descriptor,
-                    () -> new IllegalStateException(String.format("Planning runtime tool `%s` must reference a registered tool descriptor.", each.getToolName())));
-            Object workflowKind = descriptor.getMeta().get(MCPShardingSphereMetadataKeys.WORKFLOW_KIND);
-            ShardingSpherePreconditions.checkState(null != workflowKind && !workflowKind.toString().isBlank(),
-                    () -> new IllegalStateException(String.format("Planning tool `%s` metadata must declare `%s`.", each.getToolName(), MCPShardingSphereMetadataKeys.WORKFLOW_KIND)));
-            String previousToolName = workflowKinds.putIfAbsent(workflowKind.toString(), each.getToolName());
-            ShardingSpherePreconditions.checkState(null == previousToolName, () -> new IllegalStateException(
-                    String.format("Planning workflow kind `%s` is used by both `%s` and `%s`.", workflowKind, previousToolName, each.getToolName())));
-        }
-    }
-    
-    private static void validateToolOutputSchema(final MCPToolDescriptor descriptor, final Collection<MCPToolDescriptorValidator> descriptorValidators) {
-        Map<String, Object> outputSchema = descriptor.getOutputSchema();
-        ShardingSpherePreconditions.checkState("object".equals(outputSchema.get("type")),
-                () -> new IllegalStateException(String.format("Tool `%s` outputSchema must be an object.", descriptor.getName())));
-        Object properties = outputSchema.get("properties");
-        ShardingSpherePreconditions.checkState(properties instanceof Map && !((Map<?, ?>) properties).isEmpty(),
-                () -> new IllegalStateException(String.format("Tool `%s` outputSchema must declare properties.", descriptor.getName())));
-        validateNoRemovedModelFacingFields(descriptor, outputSchema);
-        validateOutputExamples(descriptor, outputSchema);
-        validateOutputExampleContractValues(descriptor, outputSchema);
-        validateModelCriticalOutputHints(descriptor, (Map<?, ?>) properties);
-        validateToolDescriptorExtensions(descriptor, descriptorValidators);
-    }
-    
-    private static void validateToolDescriptorExtensions(final MCPToolDescriptor descriptor, final Collection<MCPToolDescriptorValidator> descriptorValidators) {
-        for (MCPToolDescriptorValidator each : descriptorValidators) {
-            if (each.supports(descriptor)) {
-                each.validate(descriptor);
-            }
-        }
-    }
-    
-    private static void validateOutputExamples(final MCPToolDescriptor descriptor, final Map<String, Object> outputSchema) {
-        ShardingSpherePreconditions.checkState(isNonEmptyCollection(outputSchema.get("examples")),
-                () -> new IllegalStateException(String.format("Tool `%s` outputSchema must declare examples.", descriptor.getName())));
-    }
-    
-    private static void validateOutputExampleContractValues(final MCPToolDescriptor descriptor, final Map<String, Object> outputSchema) {
-        Object examples = outputSchema.get("examples");
-        if (!(examples instanceof Collection)) {
-            return;
-        }
-        for (Object each : (Collection<?>) examples) {
-            validateExampleContractValue(descriptor, each);
-        }
-    }
-    
-    private static void validateExampleContractValue(final MCPToolDescriptor descriptor, final Object value) {
-        if (value instanceof Map) {
-            validateExampleContractMap(descriptor, (Map<?, ?>) value);
-        } else if (value instanceof Collection) {
-            for (Object each : (Collection<?>) value) {
-                validateExampleContractValue(descriptor, each);
-            }
-        }
-    }
-    
-    private static void validateExampleContractMap(final MCPToolDescriptor descriptor, final Map<?, ?> value) {
-        Object responseMode = value.get("response_mode");
-        if (null != responseMode) {
-            ShardingSpherePreconditions.checkState(MCPResponseMode.isAllowed(responseMode.toString()),
-                    () -> new IllegalStateException(String.format("Tool `%s` output example uses unknown response_mode `%s`.", descriptor.getName(), responseMode)));
-        }
-        Object continuationMode = value.get("continuation_mode");
-        if (null != continuationMode) {
-            ShardingSpherePreconditions.checkState(CONTINUATION_MODES.contains(continuationMode.toString()),
-                    () -> new IllegalStateException(String.format("Tool `%s` output example uses unknown continuation_mode `%s`.", descriptor.getName(), continuationMode)));
-        }
-        Object recoveryCategory = value.get("recovery_category");
-        if (null != recoveryCategory) {
-            ShardingSpherePreconditions.checkState(RECOVERY_CATEGORIES.contains(recoveryCategory.toString()),
-                    () -> new IllegalStateException(String.format("Tool `%s` output example uses unknown recovery_category `%s`.", descriptor.getName(), recoveryCategory)));
-        }
-        for (Object each : value.values()) {
-            validateExampleContractValue(descriptor, each);
-        }
-        Object nextActions = value.get(MCPPayloadFieldNames.NEXT_ACTIONS);
-        if (null != nextActions) {
-            validateConcreteNextActions(descriptor, nextActions);
-        }
-    }
-    
-    private static void validateNoRemovedModelFacingFields(final MCPToolDescriptor descriptor, final Object value) {
-        if (value instanceof Map) {
-            validateNoRemovedModelFacingFieldMap(descriptor, (Map<?, ?>) value);
-        } else if (value instanceof Collection) {
-            for (Object each : (Collection<?>) value) {
-                validateNoRemovedModelFacingFields(descriptor, each);
-            }
-        }
-    }
-    
-    private static void validateNoRemovedModelFacingFieldMap(final MCPToolDescriptor descriptor, final Map<?, ?> value) {
-        for (Entry<?, ?> entry : value.entrySet()) {
-            String key = String.valueOf(entry.getKey());
-            validateNoRemovedModelFacingField(descriptor, key);
-            if ("required".equals(key)) {
-                validateNoRemovedModelFacingRequiredFields(descriptor, entry.getValue());
-            }
-            validateNoRemovedModelFacingFields(descriptor, entry.getValue());
-        }
-    }
-    
-    private static void validateNoRemovedModelFacingRequiredFields(final MCPToolDescriptor descriptor, final Object value) {
-        if (!(value instanceof Collection)) {
-            return;
-        }
-        for (Object each : (Collection<?>) value) {
-            validateNoRemovedModelFacingField(descriptor, String.valueOf(each));
-        }
-    }
-    
-    private static void validateNoRemovedModelFacingField(final MCPToolDescriptor descriptor, final String fieldName) {
-        ShardingSpherePreconditions.checkState(!REMOVED_MODEL_FACING_FIELDS.contains(fieldName),
-                () -> new IllegalStateException(String.format("Tool `%s` model-facing contract must use canonical fields instead of removed `%s`.", descriptor.getName(), fieldName)));
-    }
-    
-    private static void validateConcreteNextActions(final MCPToolDescriptor descriptor, final Object value) {
-        ShardingSpherePreconditions.checkState(value instanceof Collection,
-                () -> new IllegalStateException(String.format("Tool `%s` next_actions example must be an array.", descriptor.getName())));
-        for (Object each : (Collection<?>) value) {
-            ShardingSpherePreconditions.checkState(each instanceof Map,
-                    () -> new IllegalStateException(String.format("Tool `%s` next_actions example item must be an object.", descriptor.getName())));
-            validateConcreteNextAction(descriptor, (Map<?, ?>) each);
-        }
-    }
-    
-    private static void validateConcreteNextAction(final MCPToolDescriptor descriptor, final Map<?, ?> action) {
-        String type = String.valueOf(action.get("type"));
-        Collection<String> allowedFields = NEXT_ACTION_ALLOWED_FIELDS.get(type);
-        ShardingSpherePreconditions.checkState(null != allowedFields,
-                () -> new IllegalStateException(String.format("Tool `%s` next_actions example uses unknown type `%s`.", descriptor.getName(), type)));
-        for (Object each : action.keySet()) {
-            String fieldName = String.valueOf(each);
-            ShardingSpherePreconditions.checkState(allowedFields.contains(fieldName),
-                    () -> new IllegalStateException(String.format("Tool `%s` next_actions example `%s` contains unsupported field `%s`.", descriptor.getName(), type, fieldName)));
-        }
-    }
-    
-    private static void validateModelCriticalOutputHints(final MCPToolDescriptor descriptor, final Map<?, ?> properties) {
-        for (Entry<?, ?> entry : properties.entrySet()) {
-            String fieldName = String.valueOf(entry.getKey());
-            if (MODEL_CRITICAL_HINT_FIELDS.contains(fieldName)) {
-                validateModelCriticalOutputHint(descriptor, fieldName, entry.getValue());
-            }
-            validateNestedModelCriticalOutputHints(descriptor, entry.getValue());
-        }
-    }
-    
-    private static void validateNestedModelCriticalOutputHints(final MCPToolDescriptor descriptor, final Object value) {
-        if (!(value instanceof Map)) {
-            return;
-        }
-        Object properties = ((Map<?, ?>) value).get("properties");
-        if (properties instanceof Map) {
-            validateModelCriticalOutputHints(descriptor, (Map<?, ?>) properties);
-        }
-        Object items = ((Map<?, ?>) value).get("items");
-        if (items instanceof Map) {
-            validateNestedModelCriticalOutputHints(descriptor, items);
-        }
-    }
-    
-    private static void validateModelCriticalOutputHint(final MCPToolDescriptor descriptor, final String fieldName, final Object property) {
-        ShardingSpherePreconditions.checkState(property instanceof Map,
-                () -> new IllegalStateException(String.format("Tool `%s` model-critical output field `%s` must be an object.", descriptor.getName(), fieldName)));
-        Object description = ((Map<?, ?>) property).get("description");
-        checkDescription(null == description ? "" : description.toString(), String.format("Tool model-critical output field `%s.%s` description", descriptor.getName(), fieldName));
-        if (MCPPayloadFieldNames.NEXT_ACTIONS.equals(fieldName)) {
-            validateNextActionsSchema(descriptor, (Map<?, ?>) property);
-        }
-    }
-    
-    private static void validateNextActionsSchema(final MCPToolDescriptor descriptor, final Map<?, ?> property) {
-        ShardingSpherePreconditions.checkState("array".equals(property.get("type")), () -> new IllegalStateException(String.format("Tool `%s` next_actions must be an array.", descriptor.getName())));
-        Object items = property.get("items");
-        ShardingSpherePreconditions.checkState(items instanceof Map, () -> new IllegalStateException(String.format("Tool `%s` next_actions items must be an object.", descriptor.getName())));
-        Object properties = ((Map<?, ?>) items).get("properties");
-        ShardingSpherePreconditions.checkState(properties instanceof Map,
-                () -> new IllegalStateException(String.format("Tool `%s` next_actions items must declare properties.", descriptor.getName())));
-        for (String each : List.of("order", "type", "title")) {
-            ShardingSpherePreconditions.checkState(((Map<?, ?>) properties).containsKey(each),
-                    () -> new IllegalStateException(String.format("Tool `%s` next_actions item must declare `%s`.", descriptor.getName(), each)));
-            Object field = ((Map<?, ?>) properties).get(each);
-            ShardingSpherePreconditions.checkState(field instanceof Map,
-                    () -> new IllegalStateException(String.format("Tool `%s` next_actions item field `%s` must be an object.", descriptor.getName(), each)));
-            Object description = ((Map<?, ?>) field).get("description");
-            checkDescription(null == description ? "" : description.toString(), String.format("Tool next_actions item field `%s.%s` description", descriptor.getName(), each));
-        }
-        for (Object each : ((Map<?, ?>) properties).keySet()) {
-            String fieldName = String.valueOf(each);
-            ShardingSpherePreconditions.checkState(NEXT_ACTION_SCHEMA_ALLOWED_FIELDS.contains(fieldName),
-                    () -> new IllegalStateException(String.format("Tool `%s` next_actions item contains unsupported field `%s`.", descriptor.getName(), fieldName)));
-        }
-    }
-    
-    private static void validatePromptDescriptors(final Collection<MCPPromptDescriptor> descriptors, final Collection<MCPPromptTemplateBinding> templateBindings) {
-        Map<String, MCPPromptDescriptor> registered = new LinkedHashMap<>(descriptors.size(), 1F);
-        Map<String, MCPPromptTemplateBinding> bindings = templateBindings.stream()
-                .collect(Collectors.toMap(MCPPromptTemplateBinding::getPromptName, each -> each));
-        for (MCPPromptDescriptor each : descriptors) {
-            MCPPromptTemplateBinding binding = bindings.get(each.getName());
-            ShardingSpherePreconditions.checkState(null != binding, () -> new IllegalStateException(String.format("Prompt `%s` must declare an internal template binding.", each.getName())));
-            ShardingSpherePreconditions.checkState(null == registered.putIfAbsent(each.getName(), each),
-                    () -> new IllegalStateException(String.format("Duplicate MCP prompt descriptor `%s`.", each.getName())));
-            validatePromptTemplate(each, binding);
-        }
-    }
-    
-    private static void validatePromptTemplate(final MCPPromptDescriptor descriptor, final MCPPromptTemplateBinding binding) {
-        String template = MCPPromptTemplateLoader.load(binding.getTemplateResource());
-        validateNoUnsupportedModelFacingPlaceholders(binding, template);
-        Set<String> declaredArguments = descriptor.getArguments().stream().map(MCPPromptArgumentDescriptor::getName).collect(Collectors.toSet());
-        Set<String> renderedArguments = MCPPromptTemplateLoader.extractPlaceholders(template);
-        for (String each : renderedArguments) {
-            ShardingSpherePreconditions.checkState(declaredArguments.contains(each),
-                    () -> new IllegalStateException(String.format("Prompt template `%s` has undeclared placeholder `%s`.", binding.getTemplateResource(), each)));
-        }
-        validateDeclaredPromptArgumentsRendered(descriptor, binding, declaredArguments, renderedArguments);
-    }
-    
-    private static void validateNoUnsupportedModelFacingPlaceholders(final MCPPromptTemplateBinding binding, final String template) {
-        for (String each : template.lines().toList()) {
-            validateNoUnsupportedModelFacingPlaceholderInLine(binding, each);
-        }
-    }
-    
-    private static void validateNoUnsupportedModelFacingPlaceholderInLine(final MCPPromptTemplateBinding binding, final String line) {
-        Matcher matcher = SINGLE_BRACE_PLACEHOLDER_PATTERN.matcher(line);
-        while (matcher.find()) {
-            if (!isResourceUriTemplateVariable(line, matcher.start())) {
-                throw new IllegalStateException(String.format("Prompt template `%s` contains unsupported model-facing placeholder `{%s}`.",
-                        binding.getTemplateResource(), matcher.group(1)));
-            }
-        }
-    }
-    
-    private static boolean isResourceUriTemplateVariable(final String line, final int placeholderStartIndex) {
-        int tokenStartIndex = findTokenStartIndex(line, placeholderStartIndex);
-        int resourceUriStartIndex = line.lastIndexOf("shardingsphere://", placeholderStartIndex);
-        return resourceUriStartIndex >= tokenStartIndex;
-    }
-    
-    private static int findTokenStartIndex(final String line, final int index) {
-        for (int i = index - 1; i >= 0; i--) {
-            if (Character.isWhitespace(line.charAt(i))) {
-                return i + 1;
-            }
-        }
-        return 0;
-    }
-    
-    private static void validateDeclaredPromptArgumentsRendered(final MCPPromptDescriptor descriptor, final MCPPromptTemplateBinding binding,
-                                                                final Set<String> declaredArguments, final Set<String> renderedArguments) {
-        Set<String> clientFormOnlyArguments = getClientFormOnlyArguments(descriptor);
-        for (String each : declaredArguments) {
-            ShardingSpherePreconditions.checkState(renderedArguments.contains(each) || clientFormOnlyArguments.contains(each),
-                    () -> new IllegalStateException(String.format("Prompt `%s` declares argument `%s` but template `%s` does not render it.",
-                            descriptor.getName(), each, binding.getTemplateResource())));
-        }
-    }
-    
-    private static Set<String> getClientFormOnlyArguments(final MCPPromptDescriptor descriptor) {
-        Object value = descriptor.getMeta().get(CLIENT_FORM_ONLY_ARGUMENTS);
-        if (null == value) {
-            return Set.of();
-        }
-        ShardingSpherePreconditions.checkState(value instanceof Collection,
-                () -> new IllegalStateException(String.format("Prompt `%s` metadata `%s` must be a list.", descriptor.getName(), CLIENT_FORM_ONLY_ARGUMENTS)));
-        return ((Collection<?>) value).stream().map(String::valueOf).collect(Collectors.toSet());
-    }
-    
-    private static void validateCompletionTargetDescriptors(final Collection<MCPCompletionTargetDescriptor> descriptors, final Collection<MCPPromptDescriptor> prompts,
-                                                            final Collection<MCPResourceDescriptor> resources) {
-        Map<String, Set<String>> promptArguments = prompts.stream().collect(Collectors.toMap(MCPPromptDescriptor::getName,
-                each -> each.getArguments().stream().map(MCPPromptArgumentDescriptor::getName).collect(Collectors.toSet())));
-        Set<String> promptNames = promptArguments.keySet();
-        Map<String, MCPResourceDescriptor> resourceDescriptors = resources.stream().collect(Collectors.toMap(MCPResourceDescriptor::getUriTemplate, each -> each));
-        Map<String, MCPCompletionTargetDescriptor> registered = new LinkedHashMap<>(descriptors.size(), 1F);
-        for (MCPCompletionTargetDescriptor each : descriptors) {
-            validateCompletionReference(each, promptNames, resourceDescriptors.keySet());
-            validatePromptCompletionArguments(each, promptArguments);
-            validateResourceCompletionArguments(each, resourceDescriptors);
-            validateCompletionRequiredContextArguments(each);
-            ShardingSpherePreconditions.checkState(null == registered.putIfAbsent(each.getReferenceType() + ":" + each.getReference(), each),
-                    () -> new IllegalStateException(String.format("Duplicate MCP completion target `%s:%s`.", each.getReferenceType(), each.getReference())));
-        }
-    }
-    
-    private static void validateCompletionRequiredContextArguments(final MCPCompletionTargetDescriptor descriptor) {
-        Object value = descriptor.getMeta().get(MCPShardingSphereMetadataKeys.REQUIRED_CONTEXT_ARGUMENTS);
-        if (null == value) {
-            return;
-        }
-        ShardingSpherePreconditions.checkState(value instanceof Map,
-                () -> new IllegalStateException(String.format("Completion target `%s:%s` metadata `%s` must be an object.",
-                        descriptor.getReferenceType(), descriptor.getReference(), MCPShardingSphereMetadataKeys.REQUIRED_CONTEXT_ARGUMENTS)));
-        Set<String> argumentNames = new HashSet<>(descriptor.getArguments());
-        for (Entry<?, ?> entry : ((Map<?, ?>) value).entrySet()) {
-            String argumentName = String.valueOf(entry.getKey());
-            ShardingSpherePreconditions.checkState(argumentNames.contains(argumentName),
-                    () -> new IllegalStateException(String.format("Completion target `%s:%s` required context argument `%s` is not declared by the target.",
-                            descriptor.getReferenceType(), descriptor.getReference(), argumentName)));
-            ShardingSpherePreconditions.checkState(entry.getValue() instanceof Collection,
-                    () -> new IllegalStateException(String.format("Completion target `%s:%s` required context for `%s` must be a list.",
-                            descriptor.getReferenceType(), descriptor.getReference(), argumentName)));
-            for (Object each : (Collection<?>) entry.getValue()) {
-                ShardingSpherePreconditions.checkState(argumentNames.contains(String.valueOf(each)),
-                        () -> new IllegalStateException(String.format("Completion target `%s:%s` context argument `%s` for `%s` is not declared by the target.",
-                                descriptor.getReferenceType(), descriptor.getReference(), each, argumentName)));
-            }
-        }
-    }
-    
-    private static void validatePromptCompletionArguments(final MCPCompletionTargetDescriptor descriptor, final Map<String, Set<String>> promptArguments) {
-        if (!"prompt".equals(descriptor.getReferenceType())) {
-            return;
-        }
-        Set<String> argumentNames = promptArguments.getOrDefault(descriptor.getReference(), Set.of());
-        for (String each : descriptor.getArguments()) {
-            ShardingSpherePreconditions.checkState(argumentNames.contains(each), () -> new IllegalStateException(
-                    String.format("Completion target `prompt:%s` argument `%s` is not declared by prompt `%s`.", descriptor.getReference(), each, descriptor.getReference())));
-        }
-    }
-    
-    private static void validateResourceCompletionArguments(final MCPCompletionTargetDescriptor descriptor, final Map<String, MCPResourceDescriptor> resources) {
-        if (!"resource".equals(descriptor.getReferenceType())) {
-            return;
-        }
-        MCPResourceDescriptor resource = resources.get(descriptor.getReference());
-        ShardingSpherePreconditions.checkState(resource.isTemplated(),
-                () -> new IllegalStateException(String.format("Completion target `resource:%s` must reference a resource template.", descriptor.getReference())));
-        Collection<String> templateVariables = new MCPUriTemplate(resource.getUriTemplate()).getVariableNames();
-        for (String each : descriptor.getArguments()) {
-            ShardingSpherePreconditions.checkState(templateVariables.contains(each), () -> new IllegalStateException(
-                    String.format("Completion target `resource:%s` argument `%s` is not a URI template variable.", descriptor.getReference(), each)));
-        }
-    }
-    
-    private static void validateCompletionReference(final MCPCompletionTargetDescriptor descriptor, final Set<String> promptNames, final Set<String> resourceUris) {
-        if ("prompt".equals(descriptor.getReferenceType())) {
-            ShardingSpherePreconditions.checkState(promptNames.contains(descriptor.getReference()),
-                    () -> new IllegalStateException(String.format("Completion target references unknown prompt `%s`.", descriptor.getReference())));
-            return;
-        }
-        ShardingSpherePreconditions.checkState("resource".equals(descriptor.getReferenceType()),
-                () -> new IllegalStateException(String.format("Unsupported completion reference type `%s`.", descriptor.getReferenceType())));
-        ShardingSpherePreconditions.checkState(resourceUris.contains(descriptor.getReference()),
-                () -> new IllegalStateException(String.format("Completion target references unknown resource `%s`.", descriptor.getReference())));
-    }
-    
-    private static void validateResourceNavigationDescriptors(final Collection<MCPResourceNavigationDescriptor> descriptors, final MCPDescriptorCatalog catalog) {
-        Set<String> publicIdentifiers = createPublicIdentifiers(catalog);
-        Set<String> registered = new HashSet<>();
-        for (MCPResourceNavigationDescriptor each : descriptors) {
-            ShardingSpherePreconditions.checkState(publicIdentifiers.contains(each.getFrom()),
-                    () -> new IllegalStateException(String.format("Resource navigation references unknown source `%s`.", each.getFrom())));
-            ShardingSpherePreconditions.checkState(publicIdentifiers.contains(each.getTo()),
-                    () -> new IllegalStateException(String.format("Resource navigation references unknown target `%s`.", each.getTo())));
-            ShardingSpherePreconditions.checkState(registered.add(each.getFrom() + "->" + each.getTo()),
-                    () -> new IllegalStateException(String.format("Duplicate MCP resource navigation `%s` to `%s`.", each.getFrom(), each.getTo())));
-        }
-    }
-    
-    private static Set<String> createPublicIdentifiers(final MCPDescriptorCatalog catalog) {
-        Set<String> result = new HashSet<>();
-        catalog.getAllResourceDescriptors().stream().map(MCPResourceDescriptor::getUriTemplate).forEach(result::add);
-        catalog.getToolDescriptors().stream().map(MCPToolDescriptor::getName).forEach(result::add);
-        catalog.getPromptDescriptors().stream().map(MCPPromptDescriptor::getName).forEach(result::add);
-        return result;
-    }
-    
-    private static void validateDestructiveToolDescriptor(final MCPToolDescriptor descriptor, final MCPToolRuntimeDescriptor runtimeDescriptor) {
-        if (!descriptor.getAnnotations().isDestructiveHint()) {
-            return;
-        }
-        Map<?, ?> executionMode = MCPToolDescriptorValidationUtils.findToolInputProperty(descriptor, MCPPayloadFieldNames.EXECUTION_MODE).orElseThrow(
-                () -> new IllegalStateException(String.format("Destructive tool `%s` must declare execution_mode.", descriptor.getName())));
-        ShardingSpherePreconditions.checkState(MCPToolDescriptorValidationUtils.isRequiredToolInput(descriptor, MCPPayloadFieldNames.EXECUTION_MODE),
-                () -> new IllegalStateException(String.format("Destructive tool `%s` execution_mode must be required.", descriptor.getName())));
-        Collection<?> executionModes = executionMode.get("enum") instanceof Collection ? (Collection<?>) executionMode.get("enum") : List.of();
-        ShardingSpherePreconditions.checkState(executionModes.contains("preview"),
-                () -> new IllegalStateException(String.format("Destructive tool `%s` execution_mode must allow preview.", descriptor.getName())));
-        ShardingSpherePreconditions.checkState(!executionModes.contains("auto-execute"),
-                () -> new IllegalStateException(String.format("Destructive tool `%s` execution_mode must not expose auto-execute.", descriptor.getName())));
-        ShardingSpherePreconditions.checkState(null != runtimeDescriptor && !runtimeDescriptor.getSideEffectScope().isEmpty(),
-                () -> new IllegalStateException(String.format("Destructive tool `%s` must declare sideEffectScope in internal runtime.", descriptor.getName())));
-    }
-    
-    private static void validatePlanningExecutionMode(final MCPToolDescriptor descriptor) {
-        if (descriptor.getAnnotations().isDestructiveHint()) {
-            return;
-        }
-        Optional<Map<?, ?>> executionMode = MCPToolDescriptorValidationUtils.findToolInputProperty(descriptor, MCPPayloadFieldNames.EXECUTION_MODE);
-        if (executionMode.isEmpty()) {
-            return;
-        }
-        Collection<?> executionModes = executionMode.get().get("enum") instanceof Collection ? (Collection<?>) executionMode.get().get("enum") : List.of();
-        ShardingSpherePreconditions.checkState(!executionModes.contains("preview"),
-                () -> new IllegalStateException(String.format("Planning tool `%s` execution_mode must not expose preview.", descriptor.getName())));
-        ShardingSpherePreconditions.checkState(!executionModes.contains("auto-execute"),
-                () -> new IllegalStateException(String.format("Planning tool `%s` execution_mode must not expose auto-execute.", descriptor.getName())));
-    }
-    
-    private static boolean isNonEmptyCollection(final Object value) {
-        return value instanceof Collection && !((Collection<?>) value).isEmpty();
-    }
-    
-    private static void checkDescription(final String value, final String label) {
-        checkNotBlank(value, label);
-        ShardingSpherePreconditions.checkState(!value.startsWith(createPlaceholderPrefix("resource:")),
-                () -> new IllegalStateException(String.format("%s must not be a placeholder description.", label)));
-        ShardingSpherePreconditions.checkState(!value.startsWith(createPlaceholderPrefix("resource template:")),
-                () -> new IllegalStateException(String.format("%s must not be a placeholder description.", label)));
-    }
-    
-    private static String createPlaceholderPrefix(final String suffix) {
-        return "ShardingSphere MCP " + suffix;
-    }
-    
-    private static void checkNotBlank(final String value, final String label) {
-        ShardingSpherePreconditions.checkState(null != value && !value.isBlank(), () -> new IllegalStateException(String.format("%s is required.", label)));
+        MCPResourceDescriptorValidator.validate(catalog);
+        MCPToolDescriptorCatalogValidator.validate(catalog);
+        MCPPromptDescriptorValidator.validate(catalog.getPromptDescriptors(), catalog.getPromptTemplateBindings());
+        MCPCompletionTargetDescriptorValidator.validate(catalog.getCompletionTargetDescriptors(), catalog.getPromptDescriptors(), catalog.getAllResourceDescriptors());
+        MCPResourceNavigationDescriptorValidator.validate(catalog.getResourceNavigationDescriptors(), catalog);
     }
 }
diff --git a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPPromptDescriptorValidator.java b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPPromptDescriptorValidator.java
new file mode 100644
index 00000000000..2b6c771663e
--- /dev/null
+++ b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPPromptDescriptorValidator.java
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.mcp.support.descriptor;
+
+import org.apache.shardingsphere.infra.exception.ShardingSpherePreconditions;
+import org.apache.shardingsphere.mcp.api.prompt.descriptor.MCPPromptArgumentDescriptor;
+import org.apache.shardingsphere.mcp.api.prompt.descriptor.MCPPromptDescriptor;
+
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+/**
+ * Validator for MCP prompt descriptors and their internal templates.
+ */
+public final class MCPPromptDescriptorValidator {
+    
+    private static final String CLIENT_FORM_ONLY_ARGUMENTS = "org.apache.shardingsphere/client-form-only-arguments";
+    
+    private static final Pattern SINGLE_BRACE_PLACEHOLDER_PATTERN = Pattern.compile("(?<!\\{)\\{\\s*([a-zA-Z0-9_.-]+)\\s*}(?!})");
+    
+    private MCPPromptDescriptorValidator() {
+    }
+    
+    /**
+     * Validate prompt descriptors against their internal templates.
+     *
+     * @param descriptors prompt descriptors
+     * @param templateBindings prompt template bindings
+     */
+    public static void validate(final Collection<MCPPromptDescriptor> descriptors, final Collection<MCPPromptTemplateBinding> templateBindings) {
+        Map<String, MCPPromptDescriptor> registered = new LinkedHashMap<>(descriptors.size(), 1F);
+        Map<String, MCPPromptTemplateBinding> bindings = templateBindings.stream()
+                .collect(Collectors.toMap(MCPPromptTemplateBinding::getPromptName, each -> each));
+        for (MCPPromptDescriptor each : descriptors) {
+            MCPPromptTemplateBinding binding = bindings.get(each.getName());
+            ShardingSpherePreconditions.checkState(null != binding, () -> new IllegalStateException(String.format("Prompt `%s` must declare an internal template binding.", each.getName())));
+            ShardingSpherePreconditions.checkState(null == registered.putIfAbsent(each.getName(), each),
+                    () -> new IllegalStateException(String.format("Duplicate MCP prompt descriptor `%s`.", each.getName())));
+            validatePromptTemplate(each, binding);
+        }
+    }
+    
+    private static void validatePromptTemplate(final MCPPromptDescriptor descriptor, final MCPPromptTemplateBinding binding) {
+        String template = MCPPromptTemplateLoader.load(binding.getTemplateResource());
+        validateNoUnsupportedModelFacingPlaceholders(binding, template);
+        Set<String> declaredArguments = descriptor.getArguments().stream().map(MCPPromptArgumentDescriptor::getName).collect(Collectors.toSet());
+        Set<String> renderedArguments = MCPPromptTemplateLoader.extractPlaceholders(template);
+        for (String each : renderedArguments) {
+            ShardingSpherePreconditions.checkState(declaredArguments.contains(each),
+                    () -> new IllegalStateException(String.format("Prompt template `%s` has undeclared placeholder `%s`.", binding.getTemplateResource(), each)));
+        }
+        validateDeclaredPromptArgumentsRendered(descriptor, binding, declaredArguments, renderedArguments);
+    }
+    
+    private static void validateNoUnsupportedModelFacingPlaceholders(final MCPPromptTemplateBinding binding, final String template) {
+        for (String each : template.lines().toList()) {
+            validateNoUnsupportedModelFacingPlaceholderInLine(binding, each);
+        }
+    }
+    
+    private static void validateNoUnsupportedModelFacingPlaceholderInLine(final MCPPromptTemplateBinding binding, final String line) {
+        Matcher matcher = SINGLE_BRACE_PLACEHOLDER_PATTERN.matcher(line);
+        while (matcher.find()) {
+            if (!isResourceUriTemplateVariable(line, matcher.start())) {
+                throw new IllegalStateException(String.format("Prompt template `%s` contains unsupported model-facing placeholder `{%s}`.",
+                        binding.getTemplateResource(), matcher.group(1)));
+            }
+        }
+    }
+    
+    private static boolean isResourceUriTemplateVariable(final String line, final int placeholderStartIndex) {
+        int tokenStartIndex = findTokenStartIndex(line, placeholderStartIndex);
+        int resourceUriStartIndex = line.lastIndexOf("shardingsphere://", placeholderStartIndex);
+        return resourceUriStartIndex >= tokenStartIndex;
+    }
+    
+    private static int findTokenStartIndex(final String line, final int index) {
+        for (int i = index - 1; i >= 0; i--) {
+            if (Character.isWhitespace(line.charAt(i))) {
+                return i + 1;
+            }
+        }
+        return 0;
+    }
+    
+    private static void validateDeclaredPromptArgumentsRendered(final MCPPromptDescriptor descriptor, final MCPPromptTemplateBinding binding,
+                                                                final Set<String> declaredArguments, final Set<String> renderedArguments) {
+        Set<String> clientFormOnlyArguments = getClientFormOnlyArguments(descriptor);
+        for (String each : declaredArguments) {
+            ShardingSpherePreconditions.checkState(renderedArguments.contains(each) || clientFormOnlyArguments.contains(each),
+                    () -> new IllegalStateException(String.format("Prompt `%s` declares argument `%s` but template `%s` does not render it.",
+                            descriptor.getName(), each, binding.getTemplateResource())));
+        }
+    }
+    
+    private static Set<String> getClientFormOnlyArguments(final MCPPromptDescriptor descriptor) {
+        Object value = descriptor.getMeta().get(CLIENT_FORM_ONLY_ARGUMENTS);
+        if (null == value) {
+            return Set.of();
+        }
+        ShardingSpherePreconditions.checkState(value instanceof Collection,
+                () -> new IllegalStateException(String.format("Prompt `%s` metadata `%s` must be a list.", descriptor.getName(), CLIENT_FORM_ONLY_ARGUMENTS)));
+        return ((Collection<?>) value).stream().map(String::valueOf).collect(Collectors.toSet());
+    }
+}
diff --git a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPResourceDescriptorValidator.java b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPResourceDescriptorValidator.java
new file mode 100644
index 00000000000..4a9bc28fcb5
--- /dev/null
+++ b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPResourceDescriptorValidator.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.mcp.support.descriptor;
+
+import org.apache.shardingsphere.infra.exception.ShardingSpherePreconditions;
+import org.apache.shardingsphere.mcp.api.resource.descriptor.MCPResourceDescriptor;
+import org.apache.shardingsphere.mcp.support.resource.MCPUriTemplate;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * Validator for MCP resource descriptors.
+ */
+public final class MCPResourceDescriptorValidator {
+    
+    private MCPResourceDescriptorValidator() {
+    }
+    
+    /**
+     * Validate fixed and templated resources in one descriptor catalog.
+     *
+     * @param catalog descriptor catalog
+     */
+    public static void validate(final MCPDescriptorCatalog catalog) {
+        Map<String, MCPResourceDescriptor> registered = new LinkedHashMap<>(catalog.getResourceDescriptors().size() + catalog.getResourceTemplateDescriptors().size(), 1F);
+        for (MCPResourceDescriptor each : catalog.getResourceDescriptors()) {
+            checkNotBlank(each.getUriTemplate(), "Resource URI");
+            ShardingSpherePreconditions.checkState(!each.isTemplated(),
+                    () -> new IllegalStateException(String.format("Fixed resource `%s` must not contain template variables.", each.getUriTemplate())));
+            validateResourceDescriptor(each, registered);
+        }
+        for (MCPResourceDescriptor each : catalog.getResourceTemplateDescriptors()) {
+            checkNotBlank(each.getUriTemplate(), "Resource template URI template");
+            ShardingSpherePreconditions.checkState(each.isTemplated(),
+                    () -> new IllegalStateException(String.format("Resource template `%s` must contain template variables.", each.getUriTemplate())));
+            validateResourceDescriptor(each, registered);
+            validateResourceVariables(each, findShardingSphereResourceMetadata(catalog, each.getUriTemplate()).map(ShardingSphereMCPResourceMetadata::getUriVariables).orElse(List.of()));
+        }
+    }
+    
+    private static void validateResourceDescriptor(final MCPResourceDescriptor descriptor, final Map<String, MCPResourceDescriptor> registered) {
+        ShardingSpherePreconditions.checkState(null == registered.putIfAbsent(descriptor.getUriTemplate(), descriptor),
+                () -> new IllegalStateException(String.format("Duplicate MCP resource descriptor `%s`.", descriptor.getUriTemplate())));
+    }
+    
+    private static Optional<ShardingSphereMCPResourceMetadata> findShardingSphereResourceMetadata(final MCPDescriptorCatalog catalog, final String uriOrTemplate) {
+        return catalog.getShardingSphereResourceMetadata().stream().filter(each -> uriOrTemplate.equals(each.getUriOrTemplate())).findFirst();
+    }
+    
+    private static void validateResourceVariables(final MCPResourceDescriptor descriptor, final Collection<MCPUriVariableDescriptor> uriVariables) {
+        List<String> templateVariables = new MCPUriTemplate(descriptor.getUriTemplate()).getVariableNames();
+        Set<String> registeredTemplateVariables = new HashSet<>();
+        for (String each : templateVariables) {
+            ShardingSpherePreconditions.checkState(registeredTemplateVariables.add(each),
+                    () -> new IllegalStateException(String.format("Duplicate URI template variable `%s` in resource descriptor `%s`.", each, descriptor.getUriTemplate())));
+        }
+        Map<String, MCPUriVariableDescriptor> declaredParameters = new LinkedHashMap<>(uriVariables.size(), 1F);
+        for (MCPUriVariableDescriptor each : uriVariables) {
+            ShardingSpherePreconditions.checkState(each.isRequired(), () -> new IllegalStateException(
+                    String.format("Resource parameter `%s.%s` must be required because URI template variables are required.", descriptor.getUriTemplate(), each.getName())));
+            ShardingSpherePreconditions.checkState(registeredTemplateVariables.contains(each.getName()),
+                    () -> new IllegalStateException(String.format("Resource descriptor `%s` declares non-template parameter `%s`.", descriptor.getUriTemplate(), each.getName())));
+            ShardingSpherePreconditions.checkState(null == declaredParameters.putIfAbsent(each.getName(), each),
+                    () -> new IllegalStateException(String.format("Duplicate MCP resource parameter `%s.%s`.", descriptor.getUriTemplate(), each.getName())));
+        }
+        for (String variableName : templateVariables) {
+            ShardingSpherePreconditions.checkState(declaredParameters.containsKey(variableName),
+                    () -> new IllegalStateException(String.format("Resource descriptor `%s` must describe URI template variable `%s`.", descriptor.getUriTemplate(), variableName)));
+        }
+    }
+    
+    private static void checkNotBlank(final String value, final String label) {
+        ShardingSpherePreconditions.checkState(null != value && !value.isBlank(), () -> new IllegalStateException(String.format("%s is required.", label)));
+    }
+}
diff --git a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPResourceNavigationDescriptorValidator.java b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPResourceNavigationDescriptorValidator.java
new file mode 100644
index 00000000000..f10a3535cd4
--- /dev/null
+++ b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPResourceNavigationDescriptorValidator.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.mcp.support.descriptor;
+
+import org.apache.shardingsphere.infra.exception.ShardingSpherePreconditions;
+import org.apache.shardingsphere.mcp.api.prompt.descriptor.MCPPromptDescriptor;
+import org.apache.shardingsphere.mcp.api.resource.descriptor.MCPResourceDescriptor;
+import org.apache.shardingsphere.mcp.api.tool.descriptor.MCPToolDescriptor;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Validator for MCP resource navigation descriptors.
+ */
+public final class MCPResourceNavigationDescriptorValidator {
+    
+    private MCPResourceNavigationDescriptorValidator() {
+    }
+    
+    /**
+     * Validate resource navigation endpoints against public descriptor identifiers.
+     *
+     * @param descriptors resource navigation descriptors
+     * @param catalog descriptor catalog
+     */
+    public static void validate(final Collection<MCPResourceNavigationDescriptor> descriptors, final MCPDescriptorCatalog catalog) {
+        Set<String> publicIdentifiers = createPublicIdentifiers(catalog);
+        Set<String> registered = new HashSet<>();
+        for (MCPResourceNavigationDescriptor each : descriptors) {
+            ShardingSpherePreconditions.checkState(publicIdentifiers.contains(each.getFrom()),
+                    () -> new IllegalStateException(String.format("Resource navigation references unknown source `%s`.", each.getFrom())));
+            ShardingSpherePreconditions.checkState(publicIdentifiers.contains(each.getTo()),
+                    () -> new IllegalStateException(String.format("Resource navigation references unknown target `%s`.", each.getTo())));
+            ShardingSpherePreconditions.checkState(registered.add(each.getFrom() + "->" + each.getTo()),
+                    () -> new IllegalStateException(String.format("Duplicate MCP resource navigation `%s` to `%s`.", each.getFrom(), each.getTo())));
+        }
+    }
+    
+    private static Set<String> createPublicIdentifiers(final MCPDescriptorCatalog catalog) {
+        Set<String> result = new HashSet<>();
+        catalog.getAllResourceDescriptors().stream().map(MCPResourceDescriptor::getUriTemplate).forEach(result::add);
+        catalog.getToolDescriptors().stream().map(MCPToolDescriptor::getName).forEach(result::add);
+        catalog.getPromptDescriptors().stream().map(MCPPromptDescriptor::getName).forEach(result::add);
+        return result;
+    }
+}
diff --git a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPToolDescriptorCatalogValidator.java b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPToolDescriptorCatalogValidator.java
new file mode 100644
index 00000000000..48cc6955399
--- /dev/null
+++ b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPToolDescriptorCatalogValidator.java
@@ -0,0 +1,159 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.mcp.support.descriptor;
+
+import org.apache.shardingsphere.infra.exception.ShardingSpherePreconditions;
+import org.apache.shardingsphere.mcp.api.resource.descriptor.MCPResourceDescriptor;
+import org.apache.shardingsphere.mcp.api.tool.descriptor.MCPToolDescriptor;
+import org.apache.shardingsphere.mcp.support.protocol.MCPPayloadFieldNames;
+
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Validator for MCP tool descriptors and their runtime bindings.
+ */
+public final class MCPToolDescriptorCatalogValidator {
+    
+    private static final Collection<String> SUPPORTED_INPUT_SCHEMA_TOP_LEVEL_FIELDS = Set.of("type", "properties", "required", "additionalProperties");
+    
+    private MCPToolDescriptorCatalogValidator() {
+    }
+    
+    /**
+     * Validate tool descriptors in one descriptor catalog.
+     *
+     * @param catalog descriptor catalog
+     */
+    public static void validate(final MCPDescriptorCatalog catalog) {
+        Collection<MCPToolDescriptor> descriptors = catalog.getToolDescriptors();
+        Collection<MCPToolRuntimeDescriptor> runtimeDescriptors = catalog.getToolRuntimeDescriptors();
+        Collection<MCPToolDescriptorValidator> descriptorValidators = MCPToolDescriptorValidatorLoader.load();
+        Map<String, MCPToolDescriptor> registered = new LinkedHashMap<>(descriptors.size(), 1F);
+        Map<String, MCPToolRuntimeDescriptor> runtimes = runtimeDescriptors.stream()
+                .collect(Collectors.toMap(MCPToolRuntimeDescriptor::getToolName, each -> each));
+        Set<String> resourceIdentifiers = catalog.getAllResourceDescriptors().stream().map(MCPResourceDescriptor::getUriTemplate).collect(Collectors.toSet());
+        Set<String> shardingSphereResourceIdentifiers = catalog.getShardingSphereResourceMetadata().stream()
+                .map(ShardingSphereMCPResourceMetadata::getUriOrTemplate).collect(Collectors.toSet());
+        for (MCPToolDescriptor each : descriptors) {
+            ShardingSpherePreconditions.checkState(null == registered.putIfAbsent(each.getName(), each),
+                    () -> new IllegalStateException(String.format("Duplicate MCP tool descriptor `%s`.", each.getName())));
+            validateToolInputSchema(each);
+            MCPToolOutputSchemaValidator.validate(each);
+            validateToolDescriptorExtensions(each, descriptorValidators);
+            validateDestructiveToolDescriptor(each, runtimes.get(each.getName()));
+            validatePlanningExecutionMode(each);
+            validateRelatedResourceUris(each, resourceIdentifiers, shardingSphereResourceIdentifiers);
+        }
+        validatePlanningToolRuntimeDescriptors(registered, runtimeDescriptors);
+    }
+    
+    private static void validateToolInputSchema(final MCPToolDescriptor descriptor) {
+        Map<String, Object> inputSchema = descriptor.getInputSchema();
+        for (String each : inputSchema.keySet()) {
+            ShardingSpherePreconditions.checkState(SUPPORTED_INPUT_SCHEMA_TOP_LEVEL_FIELDS.contains(each),
+                    () -> new IllegalStateException(String.format("Tool `%s` inputSchema contains unsupported top-level field `%s`.", descriptor.getName(), each)));
+        }
+        ShardingSpherePreconditions.checkState("object".equals(inputSchema.get("type")),
+                () -> new IllegalStateException(String.format("Tool `%s` inputSchema must be an object.", descriptor.getName())));
+        Object properties = inputSchema.get("properties");
+        ShardingSpherePreconditions.checkState(properties instanceof Map, () -> new IllegalStateException(String.format("Tool `%s` inputSchema must declare properties.", descriptor.getName())));
+        MCPToolOutputSchemaValidator.validateInputSchemaFields(descriptor);
+    }
+    
+    private static void validateRelatedResourceUris(final MCPToolDescriptor descriptor, final Set<String> resourceIdentifiers, final Set<String> shardingSphereResourceIdentifiers) {
+        Object value = descriptor.getMeta().get(MCPShardingSphereMetadataKeys.RELATED_RESOURCE_URIS);
+        if (null == value) {
+            return;
+        }
+        ShardingSpherePreconditions.checkState(value instanceof Collection,
+                () -> new IllegalStateException(String.format("Tool `%s` metadata `%s` must be a list.", descriptor.getName(), MCPShardingSphereMetadataKeys.RELATED_RESOURCE_URIS)));
+        for (Object each : (Collection<?>) value) {
+            String uri = String.valueOf(each);
+            ShardingSpherePreconditions.checkState(resourceIdentifiers.contains(uri),
+                    () -> new IllegalStateException(String.format("Tool `%s` metadata `%s` references unknown resource `%s`.",
+                            descriptor.getName(), MCPShardingSphereMetadataKeys.RELATED_RESOURCE_URIS, uri)));
+            ShardingSpherePreconditions.checkState(shardingSphereResourceIdentifiers.contains(uri),
+                    () -> new IllegalStateException(String.format("Tool `%s` metadata `%s` references resource `%s` without ShardingSphere metadata.",
+                            descriptor.getName(), MCPShardingSphereMetadataKeys.RELATED_RESOURCE_URIS, uri)));
+        }
+    }
+    
+    private static void validatePlanningToolRuntimeDescriptors(final Map<String, MCPToolDescriptor> descriptors, final Collection<MCPToolRuntimeDescriptor> runtimeDescriptors) {
+        Map<String, String> workflowKinds = new LinkedHashMap<>(runtimeDescriptors.size(), 1F);
+        for (MCPToolRuntimeDescriptor each : runtimeDescriptors) {
+            if (!"plan".equals(each.getWorkflowRole())) {
+                continue;
+            }
+            MCPToolDescriptor descriptor = descriptors.get(each.getToolName());
+            ShardingSpherePreconditions.checkState(null != descriptor,
+                    () -> new IllegalStateException(String.format("Planning runtime tool `%s` must reference a registered tool descriptor.", each.getToolName())));
+            Object workflowKind = descriptor.getMeta().get(MCPShardingSphereMetadataKeys.WORKFLOW_KIND);
+            ShardingSpherePreconditions.checkState(null != workflowKind && !workflowKind.toString().isBlank(),
+                    () -> new IllegalStateException(String.format("Planning tool `%s` metadata must declare `%s`.", each.getToolName(), MCPShardingSphereMetadataKeys.WORKFLOW_KIND)));
+            String previousToolName = workflowKinds.putIfAbsent(workflowKind.toString(), each.getToolName());
+            ShardingSpherePreconditions.checkState(null == previousToolName, () -> new IllegalStateException(
+                    String.format("Planning workflow kind `%s` is used by both `%s` and `%s`.", workflowKind, previousToolName, each.getToolName())));
+        }
+    }
+    
+    private static void validateDestructiveToolDescriptor(final MCPToolDescriptor descriptor, final MCPToolRuntimeDescriptor runtimeDescriptor) {
+        if (!descriptor.getAnnotations().isDestructiveHint()) {
+            return;
+        }
+        Map<?, ?> executionMode = MCPToolDescriptorValidationUtils.findToolInputProperty(descriptor, MCPPayloadFieldNames.EXECUTION_MODE).orElseThrow(
+                () -> new IllegalStateException(String.format("Destructive tool `%s` must declare execution_mode.", descriptor.getName())));
+        ShardingSpherePreconditions.checkState(MCPToolDescriptorValidationUtils.isRequiredToolInput(descriptor, MCPPayloadFieldNames.EXECUTION_MODE),
+                () -> new IllegalStateException(String.format("Destructive tool `%s` execution_mode must be required.", descriptor.getName())));
+        Collection<?> executionModes = executionMode.get("enum") instanceof Collection ? (Collection<?>) executionMode.get("enum") : List.of();
+        ShardingSpherePreconditions.checkState(executionModes.contains("preview"),
+                () -> new IllegalStateException(String.format("Destructive tool `%s` execution_mode must allow preview.", descriptor.getName())));
+        ShardingSpherePreconditions.checkState(!executionModes.contains("auto-execute"),
+                () -> new IllegalStateException(String.format("Destructive tool `%s` execution_mode must not expose auto-execute.", descriptor.getName())));
+        ShardingSpherePreconditions.checkState(null != runtimeDescriptor && !runtimeDescriptor.getSideEffectScope().isEmpty(),
+                () -> new IllegalStateException(String.format("Destructive tool `%s` must declare sideEffectScope in internal runtime.", descriptor.getName())));
+    }
+    
+    private static void validatePlanningExecutionMode(final MCPToolDescriptor descriptor) {
+        if (descriptor.getAnnotations().isDestructiveHint()) {
+            return;
+        }
+        Optional<Map<?, ?>> executionMode = MCPToolDescriptorValidationUtils.findToolInputProperty(descriptor, MCPPayloadFieldNames.EXECUTION_MODE);
+        if (executionMode.isEmpty()) {
+            return;
+        }
+        Collection<?> executionModes = executionMode.get().get("enum") instanceof Collection ? (Collection<?>) executionMode.get().get("enum") : List.of();
+        ShardingSpherePreconditions.checkState(!executionModes.contains("preview"),
+                () -> new IllegalStateException(String.format("Planning tool `%s` execution_mode must not expose preview.", descriptor.getName())));
+        ShardingSpherePreconditions.checkState(!executionModes.contains("auto-execute"),
+                () -> new IllegalStateException(String.format("Planning tool `%s` execution_mode must not expose auto-execute.", descriptor.getName())));
+    }
+    
+    private static void validateToolDescriptorExtensions(final MCPToolDescriptor descriptor, final Collection<MCPToolDescriptorValidator> descriptorValidators) {
+        for (MCPToolDescriptorValidator each : descriptorValidators) {
+            if (each.supports(descriptor)) {
+                each.validate(descriptor);
+            }
+        }
+    }
+}
diff --git a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPToolOutputSchemaValidator.java b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPToolOutputSchemaValidator.java
new file mode 100644
index 00000000000..f70177f19e6
--- /dev/null
+++ b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPToolOutputSchemaValidator.java
@@ -0,0 +1,285 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.mcp.support.descriptor;
+
+import org.apache.shardingsphere.infra.exception.ShardingSpherePreconditions;
+import org.apache.shardingsphere.mcp.api.tool.descriptor.MCPToolDescriptor;
+import org.apache.shardingsphere.mcp.support.protocol.MCPPayloadFieldNames;
+import org.apache.shardingsphere.mcp.support.protocol.MCPResponseMode;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+/**
+ * Validator for model-facing MCP tool output schema contracts.
+ */
+public final class MCPToolOutputSchemaValidator {
+    
+    private static final Collection<String> REMOVED_MODEL_FACING_FIELDS = Set.of(
+            "target_tool", "target_resource", "required_arguments", "action_kind", "suggested_next_tool", "suggested_next_tools", "recommended_next_tool",
+            "recommended_recovery", "suggested_next_action", "approved_by_user", "requires_user_approval", "approval_required", "user_overrides");
+    
+    private static final Map<String, Collection<String>> NEXT_ACTION_ALLOWED_FIELDS = createNextActionAllowedFields();
+    
+    private static final Collection<String> NEXT_ACTION_SCHEMA_ALLOWED_FIELDS = createNextActionSchemaAllowedFields();
+    
+    private static final Collection<String> MODEL_CRITICAL_HINT_FIELDS = List.of(
+            MCPPayloadFieldNames.NEXT_ACTIONS, MCPPayloadFieldNames.RESOURCES_TO_READ, MCPPayloadFieldNames.RESOURCE, MCPPayloadFieldNames.PARENT_RESOURCE,
+            MCPPayloadFieldNames.NEXT_RESOURCES, "manual_artifact_summary", "manual_follow_up", "empty_state", "ambiguity_state", MCPPayloadFieldNames.RECOVERY, "recovery_guidance",
+            "remediation");
+    
+    private static final Collection<String> CONTINUATION_MODES = List.of("none", "pagination", "metadata_search");
+    
+    private static final Collection<String> RECOVERY_CATEGORIES = List.of("not_found", "ambiguous", "empty_scope", "missing_context", "validation", "terminal",
+            "unsupported_target", "invalid_enum", "unsafe_sql", "stale_workflow", "unavailable_runtime", "terminal_operator_action");
+    
+    private MCPToolOutputSchemaValidator() {
+    }
+    
+    private static Map<String, Collection<String>> createNextActionAllowedFields() {
+        return Map.of(
+                "resource_read", Set.of("order", "type", "title", "resource_uri", "reason", "depends_on"),
+                "tool_call", Set.of("order", "type", "title", "tool_name", "arguments", "reason", "depends_on"),
+                "completion", Set.of("order", "type", "title", "ref", "argument", "context", "missing_context_arguments", "resume_ref", "resume_arguments", "reason", "depends_on"),
+                "ask_user", Set.of("order", "type", "title", "question", "required_inputs", "reason", "depends_on"),
+                "terminal", Set.of("order", "type", "title", "reason", "depends_on"));
+    }
+    
+    private static Collection<String> createNextActionSchemaAllowedFields() {
+        Set<String> result = new HashSet<>();
+        for (Collection<String> each : NEXT_ACTION_ALLOWED_FIELDS.values()) {
+            result.addAll(each);
+        }
+        return result;
+    }
+    
+    /**
+     * Validate one tool output schema.
+     *
+     * @param descriptor tool descriptor
+     */
+    public static void validate(final MCPToolDescriptor descriptor) {
+        Map<String, Object> outputSchema = descriptor.getOutputSchema();
+        ShardingSpherePreconditions.checkState("object".equals(outputSchema.get("type")),
+                () -> new IllegalStateException(String.format("Tool `%s` outputSchema must be an object.", descriptor.getName())));
+        Object properties = outputSchema.get("properties");
+        ShardingSpherePreconditions.checkState(properties instanceof Map && !((Map<?, ?>) properties).isEmpty(),
+                () -> new IllegalStateException(String.format("Tool `%s` outputSchema must declare properties.", descriptor.getName())));
+        validateNoRemovedModelFacingFields(descriptor, outputSchema);
+        validateOutputExamples(descriptor, outputSchema);
+        validateOutputExampleContractValues(descriptor, outputSchema);
+        validateModelCriticalOutputHints(descriptor, (Map<?, ?>) properties);
+    }
+    
+    /**
+     * Validate that model-facing input schema fields do not use removed aliases.
+     *
+     * @param descriptor tool descriptor
+     */
+    public static void validateInputSchemaFields(final MCPToolDescriptor descriptor) {
+        validateNoRemovedModelFacingFields(descriptor, descriptor.getInputSchema());
+    }
+    
+    private static void validateOutputExamples(final MCPToolDescriptor descriptor, final Map<String, Object> outputSchema) {
+        ShardingSpherePreconditions.checkState(isNonEmptyCollection(outputSchema.get("examples")),
+                () -> new IllegalStateException(String.format("Tool `%s` outputSchema must declare examples.", descriptor.getName())));
+    }
+    
+    private static void validateOutputExampleContractValues(final MCPToolDescriptor descriptor, final Map<String, Object> outputSchema) {
+        Object examples = outputSchema.get("examples");
+        if (!(examples instanceof Collection)) {
+            return;
+        }
+        for (Object each : (Collection<?>) examples) {
+            validateExampleContractValue(descriptor, each);
+        }
+    }
+    
+    private static void validateExampleContractValue(final MCPToolDescriptor descriptor, final Object value) {
+        if (value instanceof Map) {
+            validateExampleContractMap(descriptor, (Map<?, ?>) value);
+        } else if (value instanceof Collection) {
+            for (Object each : (Collection<?>) value) {
+                validateExampleContractValue(descriptor, each);
+            }
+        }
+    }
+    
+    private static void validateExampleContractMap(final MCPToolDescriptor descriptor, final Map<?, ?> value) {
+        Object responseMode = value.get("response_mode");
+        if (null != responseMode) {
+            ShardingSpherePreconditions.checkState(MCPResponseMode.isAllowed(responseMode.toString()),
+                    () -> new IllegalStateException(String.format("Tool `%s` output example uses unknown response_mode `%s`.", descriptor.getName(), responseMode)));
+        }
+        Object continuationMode = value.get("continuation_mode");
+        if (null != continuationMode) {
+            ShardingSpherePreconditions.checkState(CONTINUATION_MODES.contains(continuationMode.toString()),
+                    () -> new IllegalStateException(String.format("Tool `%s` output example uses unknown continuation_mode `%s`.", descriptor.getName(), continuationMode)));
+        }
+        Object recoveryCategory = value.get("recovery_category");
+        if (null != recoveryCategory) {
+            ShardingSpherePreconditions.checkState(RECOVERY_CATEGORIES.contains(recoveryCategory.toString()),
+                    () -> new IllegalStateException(String.format("Tool `%s` output example uses unknown recovery_category `%s`.", descriptor.getName(), recoveryCategory)));
+        }
+        for (Object each : value.values()) {
+            validateExampleContractValue(descriptor, each);
+        }
+        Object nextActions = value.get(MCPPayloadFieldNames.NEXT_ACTIONS);
+        if (null != nextActions) {
+            validateConcreteNextActions(descriptor, nextActions);
+        }
+    }
+    
+    private static void validateNoRemovedModelFacingFields(final MCPToolDescriptor descriptor, final Object value) {
+        if (value instanceof Map) {
+            validateNoRemovedModelFacingFieldMap(descriptor, (Map<?, ?>) value);
+        } else if (value instanceof Collection) {
+            for (Object each : (Collection<?>) value) {
+                validateNoRemovedModelFacingFields(descriptor, each);
+            }
+        }
+    }
+    
+    private static void validateNoRemovedModelFacingFieldMap(final MCPToolDescriptor descriptor, final Map<?, ?> value) {
+        for (Entry<?, ?> entry : value.entrySet()) {
+            String key = String.valueOf(entry.getKey());
+            validateNoRemovedModelFacingField(descriptor, key);
+            if ("required".equals(key)) {
+                validateNoRemovedModelFacingRequiredFields(descriptor, entry.getValue());
+            }
+            validateNoRemovedModelFacingFields(descriptor, entry.getValue());
+        }
+    }
+    
+    private static void validateNoRemovedModelFacingRequiredFields(final MCPToolDescriptor descriptor, final Object value) {
+        if (!(value instanceof Collection)) {
+            return;
+        }
+        for (Object each : (Collection<?>) value) {
+            validateNoRemovedModelFacingField(descriptor, String.valueOf(each));
+        }
+    }
+    
+    private static void validateNoRemovedModelFacingField(final MCPToolDescriptor descriptor, final String fieldName) {
+        ShardingSpherePreconditions.checkState(!REMOVED_MODEL_FACING_FIELDS.contains(fieldName),
+                () -> new IllegalStateException(String.format("Tool `%s` model-facing contract must use canonical fields instead of removed `%s`.", descriptor.getName(), fieldName)));
+    }
+    
+    private static void validateConcreteNextActions(final MCPToolDescriptor descriptor, final Object value) {
+        ShardingSpherePreconditions.checkState(value instanceof Collection,
+                () -> new IllegalStateException(String.format("Tool `%s` next_actions example must be an array.", descriptor.getName())));
+        for (Object each : (Collection<?>) value) {
+            ShardingSpherePreconditions.checkState(each instanceof Map,
+                    () -> new IllegalStateException(String.format("Tool `%s` next_actions example item must be an object.", descriptor.getName())));
+            validateConcreteNextAction(descriptor, (Map<?, ?>) each);
+        }
+    }
+    
+    private static void validateConcreteNextAction(final MCPToolDescriptor descriptor, final Map<?, ?> action) {
+        String type = String.valueOf(action.get("type"));
+        Collection<String> allowedFields = NEXT_ACTION_ALLOWED_FIELDS.get(type);
+        ShardingSpherePreconditions.checkState(null != allowedFields,
+                () -> new IllegalStateException(String.format("Tool `%s` next_actions example uses unknown type `%s`.", descriptor.getName(), type)));
+        for (Object each : action.keySet()) {
+            String fieldName = String.valueOf(each);
+            ShardingSpherePreconditions.checkState(allowedFields.contains(fieldName),
+                    () -> new IllegalStateException(String.format("Tool `%s` next_actions example `%s` contains unsupported field `%s`.", descriptor.getName(), type, fieldName)));
+        }
+    }
+    
+    private static void validateModelCriticalOutputHints(final MCPToolDescriptor descriptor, final Map<?, ?> properties) {
+        for (Entry<?, ?> entry : properties.entrySet()) {
+            String fieldName = String.valueOf(entry.getKey());
+            if (MODEL_CRITICAL_HINT_FIELDS.contains(fieldName)) {
+                validateModelCriticalOutputHint(descriptor, fieldName, entry.getValue());
+            }
+            validateNestedModelCriticalOutputHints(descriptor, entry.getValue());
+        }
+    }
+    
+    private static void validateNestedModelCriticalOutputHints(final MCPToolDescriptor descriptor, final Object value) {
+        if (!(value instanceof Map)) {
+            return;
+        }
+        Object properties = ((Map<?, ?>) value).get("properties");
+        if (properties instanceof Map) {
+            validateModelCriticalOutputHints(descriptor, (Map<?, ?>) properties);
+        }
+        Object items = ((Map<?, ?>) value).get("items");
+        if (items instanceof Map) {
+            validateNestedModelCriticalOutputHints(descriptor, items);
+        }
+    }
+    
+    private static void validateModelCriticalOutputHint(final MCPToolDescriptor descriptor, final String fieldName, final Object property) {
+        ShardingSpherePreconditions.checkState(property instanceof Map,
+                () -> new IllegalStateException(String.format("Tool `%s` model-critical output field `%s` must be an object.", descriptor.getName(), fieldName)));
+        Object description = ((Map<?, ?>) property).get("description");
+        checkDescription(null == description ? "" : description.toString(), String.format("Tool model-critical output field `%s.%s` description", descriptor.getName(), fieldName));
+        if (MCPPayloadFieldNames.NEXT_ACTIONS.equals(fieldName)) {
+            validateNextActionsSchema(descriptor, (Map<?, ?>) property);
+        }
+    }
+    
+    private static void validateNextActionsSchema(final MCPToolDescriptor descriptor, final Map<?, ?> property) {
+        ShardingSpherePreconditions.checkState("array".equals(property.get("type")), () -> new IllegalStateException(String.format("Tool `%s` next_actions must be an array.", descriptor.getName())));
+        Object items = property.get("items");
+        ShardingSpherePreconditions.checkState(items instanceof Map, () -> new IllegalStateException(String.format("Tool `%s` next_actions items must be an object.", descriptor.getName())));
+        Object properties = ((Map<?, ?>) items).get("properties");
+        ShardingSpherePreconditions.checkState(properties instanceof Map,
+                () -> new IllegalStateException(String.format("Tool `%s` next_actions items must declare properties.", descriptor.getName())));
+        for (String each : List.of("order", "type", "title")) {
+            ShardingSpherePreconditions.checkState(((Map<?, ?>) properties).containsKey(each),
+                    () -> new IllegalStateException(String.format("Tool `%s` next_actions item must declare `%s`.", descriptor.getName(), each)));
+            Object field = ((Map<?, ?>) properties).get(each);
+            ShardingSpherePreconditions.checkState(field instanceof Map,
+                    () -> new IllegalStateException(String.format("Tool `%s` next_actions item field `%s` must be an object.", descriptor.getName(), each)));
+            Object description = ((Map<?, ?>) field).get("description");
+            checkDescription(null == description ? "" : description.toString(), String.format("Tool next_actions item field `%s.%s` description", descriptor.getName(), each));
+        }
+        for (Object each : ((Map<?, ?>) properties).keySet()) {
+            String fieldName = String.valueOf(each);
+            ShardingSpherePreconditions.checkState(NEXT_ACTION_SCHEMA_ALLOWED_FIELDS.contains(fieldName),
+                    () -> new IllegalStateException(String.format("Tool `%s` next_actions item contains unsupported field `%s`.", descriptor.getName(), fieldName)));
+        }
+    }
+    
+    private static boolean isNonEmptyCollection(final Object value) {
+        return value instanceof Collection && !((Collection<?>) value).isEmpty();
+    }
+    
+    private static void checkDescription(final String value, final String label) {
+        checkNotBlank(value, label);
+        ShardingSpherePreconditions.checkState(!value.startsWith(createPlaceholderPrefix("resource:")),
+                () -> new IllegalStateException(String.format("%s must not be a placeholder description.", label)));
+        ShardingSpherePreconditions.checkState(!value.startsWith(createPlaceholderPrefix("resource template:")),
+                () -> new IllegalStateException(String.format("%s must not be a placeholder description.", label)));
+    }
+    
+    private static String createPlaceholderPrefix(final String suffix) {
+        return "ShardingSphere MCP " + suffix;
+    }
+    
+    private static void checkNotBlank(final String value, final String label) {
+        ShardingSpherePreconditions.checkState(null != value && !value.isBlank(), () -> new IllegalStateException(String.format("%s is required.", label)));
+    }
+}
diff --git a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/AbstractMCPJdbcMetadataLoaderTest.java b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/AbstractMCPJdbcMetadataLoaderTest.java
new file mode 100644
index 00000000000..42bd90f7466
--- /dev/null
+++ b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/AbstractMCPJdbcMetadataLoaderTest.java
@@ -0,0 +1,454 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.mcp.support.database.metadata.jdbc;
+
+import lombok.AccessLevel;
+import lombok.RequiredArgsConstructor;
+import org.apache.shardingsphere.database.connector.core.type.DatabaseTypeFactory;
+import org.apache.shardingsphere.mcp.support.database.capability.SupportedMCPMetadataObjectType;
+import org.apache.shardingsphere.mcp.support.database.metadata.model.MCPColumnMetadata;
+import org.apache.shardingsphere.mcp.support.database.metadata.model.MCPDatabaseMetadata;
+import org.apache.shardingsphere.mcp.support.database.metadata.model.MCPIndexMetadata;
+import org.apache.shardingsphere.mcp.support.database.metadata.model.MCPSchemaMetadata;
+import org.apache.shardingsphere.mcp.support.database.metadata.model.MCPSequenceMetadata;
+import org.apache.shardingsphere.mcp.support.database.metadata.model.MCPTableMetadata;
+import org.apache.shardingsphere.mcp.support.database.metadata.model.MCPViewMetadata;
+import org.apache.shardingsphere.mcp.support.fixture.SupportDatabaseTypeFactoryMocker;
+import org.junit.jupiter.params.provider.Arguments;
+import org.mockito.MockedStatic;
+
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.Driver;
+import java.sql.DriverManager;
+import java.sql.DriverPropertyInfo;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.SQLFeatureNotSupportedException;
+import java.sql.Statement;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Optional;
+import java.util.Properties;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.logging.Logger;
+import java.util.stream.Stream;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+abstract class AbstractMCPJdbcMetadataLoaderTest {
+    
+    protected static final Map<String, String> METADATA_JDBC_URLS = Map.of(
+            "MySQL", "jdbc:mysql://metadata-loader/test",
+            "PostgreSQL", "jdbc:postgresql://metadata-loader/test",
+            "openGauss", "jdbc:opengauss://metadata-loader/test",
+            "SQLServer", "jdbc:sqlserver://metadata-loader",
+            "Oracle", "jdbc:oracle:thin:@metadata-loader",
+            "MariaDB", "jdbc:mariadb://metadata-loader/test",
+            "Firebird", "jdbc:firebirdsql://metadata-loader/test");
+    
+    protected LoadedMetadataCatalog load(final Map<String, RuntimeDatabaseConfiguration> runtimeDatabases) {
+        try (MockedStatic<DatabaseTypeFactory> ignored = SupportDatabaseTypeFactoryMocker.mockByConnectionMetadata()) {
+            MCPJdbcDatabaseProfileLoader databaseProfileLoader = new MCPJdbcDatabaseProfileLoader();
+            MCPJdbcMetadataLoader metadataLoader = new MCPJdbcMetadataLoader();
+            Map<String, RuntimeDatabaseProfile> databaseProfiles = databaseProfileLoader.load(runtimeDatabases);
+            Map<String, MCPDatabaseMetadata> result = new LinkedHashMap<>(runtimeDatabases.size(), 1F);
+            for (Entry<String, RuntimeDatabaseConfiguration> entry : runtimeDatabases.entrySet()) {
+                result.put(entry.getKey(), metadataLoader.load(entry.getKey(), entry.getValue(), databaseProfiles.get(entry.getKey())));
+            }
+            return new LoadedMetadataCatalog(result);
+        }
+    }
+    
+    protected static Stream<Arguments> loadTypedMetadataArguments() {
+        return Stream.of(
+                Arguments.of("table orders", SupportedMCPMetadataObjectType.TABLE, "orders"),
+                Arguments.of("table order_items", SupportedMCPMetadataObjectType.TABLE, "order_items"),
+                Arguments.of("view active_orders", SupportedMCPMetadataObjectType.VIEW, "active_orders"),
+                Arguments.of("column status", SupportedMCPMetadataObjectType.COLUMN, "status"),
+                Arguments.of("index idx_orders_status", SupportedMCPMetadataObjectType.INDEX, "idx_orders_status"),
+                Arguments.of("sequence order_seq", SupportedMCPMetadataObjectType.SEQUENCE, "order_seq"));
+    }
+    
+    protected static Stream<Arguments> loadSequenceDialectArguments() {
+        return Stream.of(
+                Arguments.of("postgresql", "PostgreSQL", "public", "order_seq",
+                        "SELECT sequence_schema AS SEQUENCE_SCHEMA, sequence_name AS SEQUENCE_NAME FROM information_schema.sequences"),
+                Arguments.of("open gauss", "openGauss", "public", "order_seq",
+                        "SELECT sequence_schema AS SEQUENCE_SCHEMA, sequence_name AS SEQUENCE_NAME FROM information_schema.sequences"),
+                Arguments.of("sql server", "SQLServer", "dbo", "order_seq",
+                        "SELECT schemas.name AS SEQUENCE_SCHEMA, seq.name AS SEQUENCE_NAME FROM sys.sequences seq INNER JOIN sys.schemas schemas ON seq.schema_id = schemas.schema_id"),
+                Arguments.of("oracle", "Oracle", "APP", "ORDER_SEQ",
+                        "SELECT USER AS SEQUENCE_SCHEMA, sequence_name AS SEQUENCE_NAME FROM USER_SEQUENCES"),
+                Arguments.of("mariadb", "MariaDB", "logic_db", "order_seq",
+                        "SELECT TABLE_SCHEMA AS SEQUENCE_SCHEMA, TABLE_NAME AS SEQUENCE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'SEQUENCE'"),
+                Arguments.of("firebird", "Firebird", "", "ORDER_SEQ",
+                        "SELECT '' AS SEQUENCE_SCHEMA, TRIM(RDB$GENERATOR_NAME) AS SEQUENCE_NAME FROM RDB$GENERATORS WHERE COALESCE(RDB$SYSTEM_FLAG, 0) = 0"));
+    }
+    
+    protected static Stream<Arguments> loadWithoutSequenceQueryArguments() {
+        return Stream.of(Arguments.of("unsupported database type", "MySQL"));
+    }
+    
+    protected Connection createConnectionWithoutSchema(final String databaseType) throws SQLException {
+        return createConnectionWithoutSchemaByURL(getMetadataJdbcUrl(databaseType));
+    }
+    
+    protected Connection createConnectionWithoutSchemaByURL(final String jdbcUrl) throws SQLException {
+        Connection result = mock(Connection.class);
+        DatabaseMetaData databaseMetaData = mock(DatabaseMetaData.class);
+        Statement statement = mock(Statement.class);
+        ResultSet tableResultSet = mockResultSet("TABLE_NAME", "orders");
+        ResultSet viewResultSet = mockResultSet("TABLE_NAME");
+        ResultSet columnResultSet = mockResultSet("COLUMN_NAME", "order_id");
+        ResultSet indexResultSet = mockResultSet("INDEX_NAME");
+        ResultSet emptyQueryResultSet = mockMultiRowResultSet(List.of());
+        when(result.getMetaData()).thenReturn(databaseMetaData);
+        when(result.createStatement()).thenReturn(statement);
+        when(databaseMetaData.getURL()).thenReturn(jdbcUrl);
+        when(statement.executeQuery(anyString())).thenReturn(emptyQueryResultSet);
+        when(databaseMetaData.getTables(isNull(), isNull(), eq("%"), any(String[].class))).thenAnswer(invocation -> {
+            String[] tableTypes = invocation.getArgument(3);
+            return "TABLE".equals(tableTypes[0]) ? tableResultSet : viewResultSet;
+        });
+        when(databaseMetaData.getColumns(isNull(), isNull(), eq("orders"), eq("%"))).thenReturn(columnResultSet);
+        when(databaseMetaData.getIndexInfo(isNull(), isNull(), eq("orders"), eq(false), eq(false))).thenReturn(indexResultSet);
+        return result;
+    }
+    
+    protected Connection createConnectionWithSequenceMetadata(final String databaseType, final String sequenceSchema,
+                                                              final String sequenceName, final String sequenceQuery) throws SQLException {
+        Connection result = mock(Connection.class);
+        DatabaseMetaData databaseMetaData = mock(DatabaseMetaData.class);
+        Statement statement = mock(Statement.class);
+        ResultSet tableResultSet = mockResultSet("TABLE_NAME");
+        ResultSet viewResultSet = mockResultSet("TABLE_NAME");
+        ResultSet sequenceResultSet = mockSingleRowResultSet(Map.of("SEQUENCE_SCHEMA", sequenceSchema, "SEQUENCE_NAME", sequenceName));
+        ResultSet emptyQueryResultSet = mockMultiRowResultSet(List.of());
+        when(result.getMetaData()).thenReturn(databaseMetaData);
+        when(result.createStatement()).thenReturn(statement);
+        when(databaseMetaData.getDatabaseProductVersion()).thenReturn("");
+        when(databaseMetaData.getURL()).thenReturn(getMetadataJdbcUrl(databaseType));
+        when(statement.executeQuery(anyString())).thenReturn(emptyQueryResultSet);
+        when(databaseMetaData.getTables(isNull(), isNull(), eq("%"), any(String[].class))).thenAnswer(invocation -> {
+            String[] tableTypes = invocation.getArgument(3);
+            return "TABLE".equals(tableTypes[0]) ? tableResultSet : viewResultSet;
+        });
+        when(statement.executeQuery(sequenceQuery)).thenReturn(sequenceResultSet);
+        return result;
+    }
+    
+    protected Connection createConnectionWithFailedSequenceMetadataQuery() throws SQLException {
+        Connection result = mock(Connection.class);
+        DatabaseMetaData databaseMetaData = mock(DatabaseMetaData.class);
+        Statement statement = mock(Statement.class);
+        ResultSet tableResultSet = mockResultSet("TABLE_NAME");
+        ResultSet viewResultSet = mockResultSet("TABLE_NAME");
+        when(result.getMetaData()).thenReturn(databaseMetaData);
+        when(result.createStatement()).thenReturn(statement);
+        when(databaseMetaData.getDatabaseProductVersion()).thenReturn("");
+        when(databaseMetaData.getURL()).thenReturn(getMetadataJdbcUrl("PostgreSQL"));
+        when(databaseMetaData.getTables(isNull(), isNull(), eq("%"), any(String[].class))).thenAnswer(invocation -> {
+            String[] tableTypes = invocation.getArgument(3);
+            return "TABLE".equals(tableTypes[0]) ? tableResultSet : viewResultSet;
+        });
+        when(statement.executeQuery("SELECT sequence_schema AS SEQUENCE_SCHEMA, sequence_name AS SEQUENCE_NAME FROM information_schema.sequences"))
+                .thenThrow(new SQLException("sequence metadata query failed"));
+        return result;
+    }
+    
+    protected Connection createStandardPostgreSQLMetadataConnection() throws SQLException {
+        Connection result = createConnectionWithMetadata(
+                List.of(
+                        Map.of("TABLE_SCHEM", "PUBLIC", "TABLE_NAME", "orders"),
+                        Map.of("TABLE_SCHEM", "PUBLIC", "TABLE_NAME", "order_items")),
+                List.of(Map.of("TABLE_SCHEM", "PUBLIC", "TABLE_NAME", "active_orders")),
+                Map.of(
+                        "orders", List.of("order_id", "status", "amount"),
+                        "order_items", List.of("item_id", "order_id", "sku"),
+                        "active_orders", List.of("order_id", "status")),
+                Map.of(
+                        "orders", List.of("PRIMARY_KEY_C", "idx_orders_status"),
+                        "order_items", List.of("PRIMARY_KEY_C")),
+                List.of(Map.of("SEQUENCE_SCHEMA", "PUBLIC", "SEQUENCE_NAME", "order_seq")));
+        DatabaseMetaData databaseMetaData = result.getMetaData();
+        when(databaseMetaData.getDatabaseProductVersion()).thenReturn("16.2");
+        when(databaseMetaData.getURL()).thenReturn("jdbc:postgresql://metadata-loader/test");
+        return result;
+    }
+    
+    protected RuntimeDatabaseConfiguration createMockRuntimeDatabaseConfiguration(final Connection connection) throws SQLException {
+        RuntimeDatabaseConfiguration result = mock(RuntimeDatabaseConfiguration.class);
+        when(result.openConnection(anyString())).thenReturn(connection);
+        return result;
+    }
+    
+    protected Connection createConnectionWithMetadata(final List<Map<String, String>> tableRows, final List<Map<String, String>> viewRows, final Map<String, List<String>> columns,
+                                                      final Map<String, List<String>> indexes, final List<Map<String, String>> sequenceRows) throws SQLException {
+        return createConnectionWithMetadata("PostgreSQL", tableRows, viewRows, columns, indexes, sequenceRows);
+    }
+    
+    protected Connection createConnectionWithMetadata(final String databaseType, final List<Map<String, String>> tableRows, final List<Map<String, String>> viewRows,
+                                                      final Map<String, List<String>> columns, final Map<String, List<String>> indexes,
+                                                      final List<Map<String, String>> sequenceRows) throws SQLException {
+        Connection result = mock(Connection.class);
+        DatabaseMetaData databaseMetaData = mock(DatabaseMetaData.class);
+        Statement statement = mock(Statement.class);
+        ResultSet sequenceResultSet = mockMultiRowResultSet(sequenceRows);
+        ResultSet emptyQueryResultSet = mockMultiRowResultSet(List.of());
+        when(result.getMetaData()).thenReturn(databaseMetaData);
+        when(result.createStatement()).thenReturn(statement);
+        when(databaseMetaData.getDatabaseProductVersion()).thenReturn("");
+        when(databaseMetaData.getURL()).thenReturn(getMetadataJdbcUrl(databaseType));
+        when(statement.executeQuery(anyString())).thenReturn(emptyQueryResultSet);
+        when(databaseMetaData.getTables(isNull(), isNull(), eq("%"), any(String[].class))).thenAnswer(invocation -> {
+            String[] tableTypes = invocation.getArgument(3);
+            return mockMultiRowResultSet("TABLE".equals(tableTypes[0]) ? tableRows : viewRows);
+        });
+        when(databaseMetaData.getColumns(isNull(), nullable(String.class), anyString(), eq("%"))).thenAnswer(invocation -> {
+            String objectName = invocation.getArgument(2);
+            return mockResultSet("COLUMN_NAME", columns.getOrDefault(objectName, List.of()).toArray(new String[0]));
+        });
+        when(databaseMetaData.getIndexInfo(isNull(), nullable(String.class), anyString(), eq(false), eq(false))).thenAnswer(invocation -> {
+            String tableName = invocation.getArgument(2);
+            return mockResultSet("INDEX_NAME", indexes.getOrDefault(tableName, List.of()).toArray(new String[0]));
+        });
+        when(statement.executeQuery(getSequenceQuery(databaseType))).thenReturn(sequenceResultSet);
+        return result;
+    }
+    
+    protected void mockEmptyScalarQueries(final Connection connection) throws SQLException {
+        Statement statement = mock(Statement.class);
+        ResultSet resultSet = mockMultiRowResultSet(List.of());
+        when(connection.createStatement()).thenReturn(statement);
+        when(statement.executeQuery(anyString())).thenReturn(resultSet);
+    }
+    
+    protected String getSequenceQuery(final String databaseType) {
+        switch (databaseType) {
+            case "PostgreSQL":
+            case "openGauss":
+                return "SELECT sequence_schema AS SEQUENCE_SCHEMA, sequence_name AS SEQUENCE_NAME FROM information_schema.sequences";
+            case "SQLServer":
+                return "SELECT schemas.name AS SEQUENCE_SCHEMA, seq.name AS SEQUENCE_NAME FROM sys.sequences seq INNER JOIN sys.schemas schemas ON seq.schema_id = schemas.schema_id";
+            case "Oracle":
+                return "SELECT USER AS SEQUENCE_SCHEMA, sequence_name AS SEQUENCE_NAME FROM USER_SEQUENCES";
+            case "MariaDB":
+                return "SELECT TABLE_SCHEMA AS SEQUENCE_SCHEMA, TABLE_NAME AS SEQUENCE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'SEQUENCE'";
+            case "Firebird":
+                return "SELECT '' AS SEQUENCE_SCHEMA, TRIM(RDB$GENERATOR_NAME) AS SEQUENCE_NAME FROM RDB$GENERATORS WHERE COALESCE(RDB$SYSTEM_FLAG, 0) = 0";
+            default:
+                return "";
+        }
+    }
+    
+    protected ResultSet mockResultSet(final String columnName, final String... values) throws SQLException {
+        ResultSet result = mock(ResultSet.class);
+        AtomicInteger nextIndex = new AtomicInteger();
+        AtomicInteger valueIndex = new AtomicInteger();
+        when(result.next()).thenAnswer(invocation -> nextIndex.getAndIncrement() < values.length);
+        when(result.getString(columnName)).thenAnswer(invocation -> values[valueIndex.getAndIncrement()]);
+        return result;
+    }
+    
+    protected ResultSet mockSingleRowResultSet(final Map<String, String> values) throws SQLException {
+        ResultSet result = mock(ResultSet.class);
+        when(result.next()).thenReturn(true, false);
+        for (Entry<String, String> entry : values.entrySet()) {
+            when(result.getString(entry.getKey())).thenReturn(entry.getValue());
+        }
+        return result;
+    }
+    
+    protected ResultSet mockMultiRowResultSet(final List<Map<String, String>> values) throws SQLException {
+        ResultSet result = mock(ResultSet.class);
+        AtomicInteger rowIndex = new AtomicInteger(-1);
+        when(result.next()).thenAnswer(invocation -> rowIndex.incrementAndGet() < values.size());
+        when(result.getString(anyString())).thenAnswer(invocation -> {
+            int currentRowIndex = rowIndex.get();
+            return 0 <= currentRowIndex && currentRowIndex < values.size() ? values.get(currentRowIndex).get(invocation.getArgument(0)) : null;
+        });
+        return result;
+    }
+    
+    protected String getMetadataJdbcUrl(final String databaseType) {
+        return METADATA_JDBC_URLS.getOrDefault(databaseType, METADATA_JDBC_URLS.get("PostgreSQL"));
+    }
+    
+    protected int countMetadata(final MCPDatabaseMetadata databaseMetadata, final SupportedMCPMetadataObjectType objectType, final String objectName) {
+        int result = 0;
+        for (MCPSchemaMetadata each : databaseMetadata.getSchemas()) {
+            result += countSchemaMetadata(each, objectType, objectName);
+        }
+        return result;
+    }
+    
+    private int countSchemaMetadata(final MCPSchemaMetadata schemaMetadata, final SupportedMCPMetadataObjectType objectType, final String objectName) {
+        int result = SupportedMCPMetadataObjectType.SCHEMA == objectType && objectName.equals(schemaMetadata.getSchema()) ? 1 : 0;
+        result += countTableMetadata(schemaMetadata.getTables(), objectType, objectName);
+        result += countViewMetadata(schemaMetadata.getViews(), objectType, objectName);
+        result += countSequenceMetadata(schemaMetadata.getSequences(), objectType, objectName);
+        return result;
+    }
+    
+    private int countTableMetadata(final Collection<MCPTableMetadata> tables, final SupportedMCPMetadataObjectType objectType, final String objectName) {
+        int result = 0;
+        for (MCPTableMetadata each : tables) {
+            if (SupportedMCPMetadataObjectType.TABLE == objectType && objectName.equals(each.getTable())) {
+                result++;
+            }
+            result += countColumnMetadata(each.getColumns(), objectType, objectName);
+            result += countIndexMetadata(each.getIndexes(), objectType, objectName);
+        }
+        return result;
+    }
+    
+    private int countViewMetadata(final Collection<MCPViewMetadata> views, final SupportedMCPMetadataObjectType objectType, final String objectName) {
+        int result = 0;
+        for (MCPViewMetadata each : views) {
+            if (SupportedMCPMetadataObjectType.VIEW == objectType && objectName.equals(each.getView())) {
+                result++;
+            }
+            result += countColumnMetadata(each.getColumns(), objectType, objectName);
+        }
+        return result;
+    }
+    
+    private int countColumnMetadata(final Collection<MCPColumnMetadata> columns, final SupportedMCPMetadataObjectType objectType, final String objectName) {
+        int result = 0;
+        for (MCPColumnMetadata each : columns) {
+            if (SupportedMCPMetadataObjectType.COLUMN == objectType && objectName.equals(each.getColumn())) {
+                result++;
+            }
+        }
+        return result;
+    }
+    
+    private int countIndexMetadata(final Collection<MCPIndexMetadata> indexes, final SupportedMCPMetadataObjectType objectType, final String objectName) {
+        int result = 0;
+        for (MCPIndexMetadata each : indexes) {
+            if (SupportedMCPMetadataObjectType.INDEX == objectType && objectName.equals(each.getIndex())) {
+                result++;
+            }
+        }
+        return result;
+    }
+    
+    private int countSequenceMetadata(final Collection<MCPSequenceMetadata> sequences, final SupportedMCPMetadataObjectType objectType, final String objectName) {
+        int result = 0;
+        for (MCPSequenceMetadata each : sequences) {
+            if (SupportedMCPMetadataObjectType.SEQUENCE == objectType && objectName.equals(each.getSequence())) {
+                result++;
+            }
+        }
+        return result;
+    }
+    
+    protected boolean containsMetadata(final MCPDatabaseMetadata databaseMetadata, final SupportedMCPMetadataObjectType objectType, final String objectName) {
+        return 0 < countMetadata(databaseMetadata, objectType, objectName);
+    }
+    
+    @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
+    protected static final class LoadedMetadataCatalog {
+        
+        private final Map<String, MCPDatabaseMetadata> databaseMetadataMap;
+        
+        protected Optional<MCPDatabaseMetadata> findMetadata(final String databaseName) {
+            return Optional.ofNullable(databaseMetadataMap.get(databaseName));
+        }
+        
+        protected Map<String, MCPDatabaseMetadata> getDatabaseMetadataMap() {
+            return databaseMetadataMap;
+        }
+    }
+    
+    protected static final class MockDriverRegistration implements AutoCloseable {
+        
+        private final Driver driver;
+        
+        private MockDriverRegistration(final Driver driver) throws SQLException {
+            this.driver = driver;
+            DriverManager.registerDriver(driver);
+        }
+        
+        protected static MockDriverRegistration register(final Driver driver) throws SQLException {
+            return new MockDriverRegistration(driver);
+        }
+        
+        @Override
+        public void close() throws SQLException {
+            DriverManager.deregisterDriver(driver);
+        }
+    }
+    
+    protected static final class MockDriver implements Driver {
+        
+        private final String jdbcUrl;
+        
+        private final Connection connection;
+        
+        protected MockDriver(final String jdbcUrl, final Connection connection) {
+            this.jdbcUrl = jdbcUrl;
+            this.connection = connection;
+        }
+        
+        @Override
+        public Connection connect(final String url, final Properties info) {
+            return acceptsURL(url) ? connection : null;
+        }
+        
+        @Override
+        public boolean acceptsURL(final String url) {
+            return jdbcUrl.equals(url);
+        }
+        
+        @Override
+        public DriverPropertyInfo[] getPropertyInfo(final String url, final Properties info) {
+            return new DriverPropertyInfo[0];
+        }
+        
+        @Override
+        public int getMajorVersion() {
+            return 1;
+        }
+        
+        @Override
+        public int getMinorVersion() {
+            return 0;
+        }
+        
+        @Override
+        public boolean jdbcCompliant() {
+            return false;
+        }
+        
+        @Override
+        public Logger getParentLogger() throws SQLFeatureNotSupportedException {
+            throw new SQLFeatureNotSupportedException("Mock driver does not expose a parent logger.");
+        }
+    }
+}
diff --git a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcMetadataLoaderFailureTest.java b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcMetadataLoaderFailureTest.java
new file mode 100644
index 00000000000..1290a2e096a
--- /dev/null
+++ b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcMetadataLoaderFailureTest.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.mcp.support.database.metadata.jdbc;
+
+import org.junit.jupiter.api.Test;
+
+import java.sql.SQLException;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class MCPJdbcMetadataLoaderFailureTest {
+    
+    @Test
+    void assertLoadWithFailedConnection() throws SQLException {
+        RuntimeDatabaseConfiguration runtimeDatabaseConfiguration = mock(RuntimeDatabaseConfiguration.class);
+        SQLException expected = new SQLException("connection failed");
+        when(runtimeDatabaseConfiguration.openConnection("logic_db")).thenThrow(expected);
+        MCPJdbcMetadataLoader metadataLoader = new MCPJdbcMetadataLoader();
+        RuntimeDatabaseConnectionException actual = assertThrows(RuntimeDatabaseConnectionException.class,
+                () -> metadataLoader.load("logic_db", runtimeDatabaseConfiguration, new RuntimeDatabaseProfile("logic_db", "PostgreSQL", "")));
+        assertThat(actual.getMessage(), is("Runtime database `logic_db` connection failed: connection_failed."));
+        assertThat(actual.getCause(), is(expected));
+    }
+}
diff --git a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcMetadataLoaderSchemaFilteringTest.java b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcMetadataLoaderSchemaFilteringTest.java
new file mode 100644
index 00000000000..e457fe88eee
--- /dev/null
+++ b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcMetadataLoaderSchemaFilteringTest.java
@@ -0,0 +1,174 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.mcp.support.database.metadata.jdbc;
+
+import org.apache.shardingsphere.mcp.support.database.capability.SupportedMCPMetadataObjectType;
+import org.apache.shardingsphere.mcp.support.database.metadata.model.MCPDatabaseMetadata;
+import org.junit.jupiter.api.Test;
+
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.Driver;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.List;
+import java.util.Map;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class MCPJdbcMetadataLoaderSchemaFilteringTest extends AbstractMCPJdbcMetadataLoaderTest {
+    
+    @Test
+    void assertLoadWithoutSchemaObjects() throws SQLException {
+        Driver mockDriver = new MockDriver("jdbc:mock:no-schema", createConnectionWithoutSchema("MySQL"));
+        try (MockDriverRegistration registration = MockDriverRegistration.register(mockDriver)) {
+            LoadedMetadataCatalog actual = load(Map.of("logic_db", new RuntimeDatabaseConfiguration("jdbc:mock:no-schema", "", "", MockDriver.class.getName())));
+            MCPDatabaseMetadata databaseMetadata = actual.findMetadata("logic_db").orElseThrow();
+            assertThat(databaseMetadata.getSchemas().size(), is(1));
+            assertThat(databaseMetadata.getSchemas().get(0).getSchema(), is("logic_db"));
+            assertTrue(containsMetadata(databaseMetadata, SupportedMCPMetadataObjectType.TABLE, "orders"));
+            assertTrue(containsMetadata(databaseMetadata, SupportedMCPMetadataObjectType.COLUMN, "order_id"));
+            assertThat(databaseMetadata.getDatabaseVersion(), is(""));
+        }
+    }
+    
+    @Test
+    void assertLoadWithoutSystemCatalogForDatabaseAsSchema() throws SQLException {
+        Connection connection = mock(Connection.class);
+        DatabaseMetaData databaseMetaData = mock(DatabaseMetaData.class);
+        when(connection.getMetaData()).thenReturn(databaseMetaData);
+        when(databaseMetaData.getDatabaseProductVersion()).thenReturn("");
+        when(databaseMetaData.getURL()).thenReturn(getMetadataJdbcUrl("MySQL"));
+        mockEmptyScalarQueries(connection);
+        when(databaseMetaData.getTables(isNull(), isNull(), eq("%"), any(String[].class))).thenAnswer(invocation -> {
+            String[] tableTypes = invocation.getArgument(3);
+            return mockMultiRowResultSet("TABLE".equals(tableTypes[0])
+                    ? List.of(
+                            Map.of("TABLE_CAT", "information_schema", "TABLE_SCHEM", "", "TABLE_NAME", "GLOBAL_STATUS"),
+                            Map.of("TABLE_CAT", "logic_db", "TABLE_SCHEM", "", "TABLE_NAME", "orders"))
+                    : List.of());
+        });
+        ResultSet orderColumns = mockResultSet("COLUMN_NAME", "order_id");
+        when(databaseMetaData.getColumns(eq("logic_db"), isNull(), eq("orders"), eq("%"))).thenReturn(orderColumns);
+        ResultSet orderIndexes = mockResultSet("INDEX_NAME", "idx_orders_status");
+        when(databaseMetaData.getIndexInfo(eq("logic_db"), isNull(), eq("orders"), eq(false), eq(false))).thenReturn(orderIndexes);
+        when(databaseMetaData.getColumns(eq("information_schema"), isNull(), eq("GLOBAL_STATUS"), eq("%")))
+                .thenThrow(new SQLException("system catalog should be skipped"));
+        RuntimeDatabaseConfiguration runtimeDatabaseConfiguration = createMockRuntimeDatabaseConfiguration(connection);
+        MCPDatabaseMetadata actual = load(Map.of("logic_db", runtimeDatabaseConfiguration)).findMetadata("logic_db").orElseThrow();
+        assertTrue(containsMetadata(actual, SupportedMCPMetadataObjectType.TABLE, "orders"));
+        assertFalse(containsMetadata(actual, SupportedMCPMetadataObjectType.TABLE, "GLOBAL_STATUS"));
+        assertThat(actual.getSchemas().get(0).getSchema(), is("logic_db"));
+    }
+    
+    @Test
+    void assertLoadWithCatalogBackedMetadataUsingLogicalSchemaName() throws SQLException {
+        Connection connection = mock(Connection.class);
+        DatabaseMetaData databaseMetaData = mock(DatabaseMetaData.class);
+        when(connection.getMetaData()).thenReturn(databaseMetaData);
+        when(databaseMetaData.getDatabaseProductVersion()).thenReturn("");
+        when(databaseMetaData.getURL()).thenReturn(getMetadataJdbcUrl("MySQL"));
+        mockEmptyScalarQueries(connection);
+        when(databaseMetaData.getTables(isNull(), isNull(), eq("%"), any(String[].class))).thenAnswer(invocation -> {
+            String[] tableTypes = invocation.getArgument(3);
+            return mockMultiRowResultSet("TABLE".equals(tableTypes[0])
+                    ? List.of(Map.of("TABLE_CAT", "orders", "TABLE_SCHEM", "", "TABLE_NAME", "orders"))
+                    : List.of());
+        });
+        ResultSet orderColumns = mockResultSet("COLUMN_NAME", "order_id");
+        when(databaseMetaData.getColumns(eq("orders"), isNull(), eq("orders"), eq("%"))).thenReturn(orderColumns);
+        ResultSet orderIndexes = mockResultSet("INDEX_NAME", "idx_orders_status");
+        when(databaseMetaData.getIndexInfo(eq("orders"), isNull(), eq("orders"), eq(false), eq(false))).thenReturn(orderIndexes);
+        RuntimeDatabaseConfiguration runtimeDatabaseConfiguration = createMockRuntimeDatabaseConfiguration(connection);
+        MCPDatabaseMetadata actual = load(Map.of("logic_db", runtimeDatabaseConfiguration)).findMetadata("logic_db").orElseThrow();
+        assertThat(actual.getSchemas().get(0).getSchema(), is("logic_db"));
+        assertTrue(containsMetadata(actual, SupportedMCPMetadataObjectType.TABLE, "orders"));
+        assertTrue(containsMetadata(actual, SupportedMCPMetadataObjectType.INDEX, "idx_orders_status"));
+    }
+    
+    @Test
+    void assertLoadWithoutEmptyTableName() throws SQLException {
+        Connection connection = createConnectionWithMetadata(
+                List.of(Map.of("TABLE_SCHEM", "PUBLIC", "TABLE_NAME", ""), Map.of("TABLE_SCHEM", "PUBLIC", "TABLE_NAME", "orders")),
+                List.of(), Map.of("orders", List.of("order_id")), Map.of(), List.of());
+        RuntimeDatabaseConfiguration runtimeDatabaseConfiguration = createMockRuntimeDatabaseConfiguration(connection);
+        MCPDatabaseMetadata actual = load(Map.of("logic_db", runtimeDatabaseConfiguration)).findMetadata("logic_db").orElseThrow();
+        assertTrue(containsMetadata(actual, SupportedMCPMetadataObjectType.TABLE, "orders"));
+        assertFalse(containsMetadata(actual, SupportedMCPMetadataObjectType.TABLE, ""));
+    }
+    
+    @Test
+    void assertLoadWithoutEmptyViewName() throws SQLException {
+        Connection connection = createConnectionWithMetadata(
+                List.of(), List.of(Map.of("TABLE_SCHEM", "PUBLIC", "TABLE_NAME", ""), Map.of("TABLE_SCHEM", "PUBLIC", "TABLE_NAME", "active_orders")),
+                Map.of("active_orders", List.of("order_id")), Map.of(), List.of());
+        RuntimeDatabaseConfiguration runtimeDatabaseConfiguration = createMockRuntimeDatabaseConfiguration(connection);
+        MCPDatabaseMetadata actual = load(Map.of("logic_db", runtimeDatabaseConfiguration)).findMetadata("logic_db").orElseThrow();
+        assertTrue(containsMetadata(actual, SupportedMCPMetadataObjectType.VIEW, "active_orders"));
+        assertFalse(containsMetadata(actual, SupportedMCPMetadataObjectType.VIEW, ""));
+    }
+    
+    @Test
+    void assertLoadWithoutEmptyColumnName() throws SQLException {
+        Connection connection = createConnectionWithMetadata(
+                List.of(Map.of("TABLE_SCHEM", "PUBLIC", "TABLE_NAME", "orders")), List.of(), Map.of("orders", List.of("", "order_id")), Map.of(), List.of());
+        RuntimeDatabaseConfiguration runtimeDatabaseConfiguration = createMockRuntimeDatabaseConfiguration(connection);
+        MCPDatabaseMetadata actual = load(Map.of("logic_db", runtimeDatabaseConfiguration)).findMetadata("logic_db").orElseThrow();
+        assertTrue(containsMetadata(actual, SupportedMCPMetadataObjectType.COLUMN, "order_id"));
+        assertFalse(containsMetadata(actual, SupportedMCPMetadataObjectType.COLUMN, ""));
+    }
+    
+    @Test
+    void assertLoadWithoutEmptyOrDuplicateIndexName() throws SQLException {
+        Connection connection = createConnectionWithMetadata(
+                List.of(Map.of("TABLE_SCHEM", "PUBLIC", "TABLE_NAME", "orders")), List.of(), Map.of("orders", List.of("order_id")),
+                Map.of("orders", List.of("", "idx_orders_status", "idx_orders_status")), List.of());
+        RuntimeDatabaseConfiguration runtimeDatabaseConfiguration = createMockRuntimeDatabaseConfiguration(connection);
+        MCPDatabaseMetadata actual = load(Map.of("logic_db", runtimeDatabaseConfiguration)).findMetadata("logic_db").orElseThrow();
+        assertTrue(containsMetadata(actual, SupportedMCPMetadataObjectType.INDEX, "idx_orders_status"));
+        assertThat(countMetadata(actual, SupportedMCPMetadataObjectType.INDEX, "idx_orders_status"), is(1));
+    }
+    
+    @Test
+    void assertLoadWithDuplicateTableRows() throws SQLException {
+        Connection connection = createConnectionWithMetadata(
+                List.of(Map.of("TABLE_SCHEM", "PUBLIC", "TABLE_NAME", "orders"), Map.of("TABLE_SCHEM", "PUBLIC", "TABLE_NAME", "orders")),
+                List.of(), Map.of("orders", List.of("order_id")), Map.of(), List.of());
+        RuntimeDatabaseConfiguration runtimeDatabaseConfiguration = createMockRuntimeDatabaseConfiguration(connection);
+        MCPDatabaseMetadata actual = load(Map.of("logic_db", runtimeDatabaseConfiguration)).findMetadata("logic_db").orElseThrow();
+        assertThat(countMetadata(actual, SupportedMCPMetadataObjectType.TABLE, "orders"), is(1));
+    }
+    
+    @Test
+    void assertLoadWithDuplicateViewRows() throws SQLException {
+        Connection connection = createConnectionWithMetadata(
+                List.of(), List.of(Map.of("TABLE_SCHEM", "PUBLIC", "TABLE_NAME", "active_orders"), Map.of("TABLE_SCHEM", "PUBLIC", "TABLE_NAME", "active_orders")),
+                Map.of("active_orders", List.of("order_id")), Map.of(), List.of());
+        RuntimeDatabaseConfiguration runtimeDatabaseConfiguration = createMockRuntimeDatabaseConfiguration(connection);
+        MCPDatabaseMetadata actual = load(Map.of("logic_db", runtimeDatabaseConfiguration)).findMetadata("logic_db").orElseThrow();
+        assertThat(countMetadata(actual, SupportedMCPMetadataObjectType.VIEW, "active_orders"), is(1));
+    }
+}
diff --git a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcMetadataLoaderSequenceTest.java b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcMetadataLoaderSequenceTest.java
new file mode 100644
index 00000000000..fc9782c95c4
--- /dev/null
+++ b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcMetadataLoaderSequenceTest.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.mcp.support.database.metadata.jdbc;
+
+import org.apache.shardingsphere.mcp.support.database.capability.SupportedMCPMetadataObjectType;
+import org.apache.shardingsphere.mcp.support.database.metadata.model.MCPDatabaseMetadata;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.sql.Connection;
+import java.sql.Driver;
+import java.sql.SQLException;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class MCPJdbcMetadataLoaderSequenceTest extends AbstractMCPJdbcMetadataLoaderTest {
+    
+    @ParameterizedTest(name = "{0}")
+    @MethodSource("loadWithoutSequenceQueryArguments")
+    void assertLoadWithoutSequenceQuery(final String name, final String databaseType) throws SQLException {
+        RuntimeDatabaseConfiguration runtimeDatabaseConfiguration = createMockRuntimeDatabaseConfiguration(createConnectionWithoutSchema(databaseType));
+        MCPDatabaseMetadata actual = load(Map.of("logic_db", runtimeDatabaseConfiguration)).findMetadata("logic_db").orElseThrow();
+        assertTrue(containsMetadata(actual, SupportedMCPMetadataObjectType.TABLE, "orders"));
+        assertFalse(containsMetadata(actual, SupportedMCPMetadataObjectType.SEQUENCE, "order_seq"));
+    }
+    
+    @Test
+    void assertLoadWithoutSystemSchemaSequence() throws SQLException {
+        Connection connection = createConnectionWithMetadata("PostgreSQL", List.of(), List.of(), Map.of(), Map.of(),
+                List.of(Map.of("SEQUENCE_SCHEMA", "PG_CATALOG", "SEQUENCE_NAME", "order_seq")));
+        RuntimeDatabaseConfiguration runtimeDatabaseConfiguration = createMockRuntimeDatabaseConfiguration(connection);
+        MCPDatabaseMetadata actual = load(Map.of("logic_db", runtimeDatabaseConfiguration)).findMetadata("logic_db").orElseThrow();
+        assertFalse(containsMetadata(actual, SupportedMCPMetadataObjectType.SEQUENCE, "order_seq"));
+    }
+    
+    @Test
+    void assertLoadWithoutEmptySequenceName() throws SQLException {
+        Connection connection = createConnectionWithMetadata("PostgreSQL", List.of(), List.of(), Map.of(), Map.of(),
+                List.of(Map.of("SEQUENCE_SCHEMA", "public", "SEQUENCE_NAME", "")));
+        RuntimeDatabaseConfiguration runtimeDatabaseConfiguration = createMockRuntimeDatabaseConfiguration(connection);
+        MCPDatabaseMetadata actual = load(Map.of("logic_db", runtimeDatabaseConfiguration)).findMetadata("logic_db").orElseThrow();
+        assertFalse(containsMetadata(actual, SupportedMCPMetadataObjectType.SEQUENCE, ""));
+    }
+    
+    @Test
+    void assertLoadWithFailedSequenceMetadataQuery() throws SQLException {
+        Driver mockDriver = new MockDriver("jdbc:mock:failed-sequence-query", createConnectionWithFailedSequenceMetadataQuery());
+        try (MockDriverRegistration ignored = MockDriverRegistration.register(mockDriver)) {
+            RuntimeDatabaseConnectionException actual = assertThrows(RuntimeDatabaseConnectionException.class,
+                    () -> load(Map.of("logic_db", new RuntimeDatabaseConfiguration("jdbc:mock:failed-sequence-query", "", "", MockDriver.class.getName()))));
+            assertThat(actual.getMessage(), is("Runtime database `logic_db` connection failed: connection_failed."));
+            assertThat(actual.getCause().getMessage(), is("sequence metadata query failed"));
+        }
+    }
+    
+    @ParameterizedTest(name = "{0}")
+    @MethodSource("loadSequenceDialectArguments")
+    void assertLoadWithDialectSequenceMetadata(final String name, final String databaseType, final String sequenceSchema,
+                                               final String sequenceName, final String sequenceQuery) throws SQLException {
+        String jdbcUrl = "jdbc:mock:sequence:" + databaseType.toLowerCase(Locale.ENGLISH);
+        Driver mockDriver = new MockDriver(jdbcUrl, createConnectionWithSequenceMetadata(databaseType, sequenceSchema, sequenceName, sequenceQuery));
+        try (MockDriverRegistration ignored = MockDriverRegistration.register(mockDriver)) {
+            LoadedMetadataCatalog actual = load(Map.of("logic_db", new RuntimeDatabaseConfiguration(jdbcUrl, "", "", MockDriver.class.getName())));
+            assertTrue(containsMetadata(actual.findMetadata("logic_db").orElseThrow(), SupportedMCPMetadataObjectType.SEQUENCE, sequenceName));
+        }
+    }
+}
diff --git a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcMetadataLoaderTest.java b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcMetadataLoaderTest.java
index 86302879859..054b3152a3e 100644
--- a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcMetadataLoaderTest.java
+++ b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcMetadataLoaderTest.java
@@ -17,67 +17,20 @@
 
 package org.apache.shardingsphere.mcp.support.database.metadata.jdbc;
 
-import lombok.AccessLevel;
-import lombok.RequiredArgsConstructor;
-import org.apache.shardingsphere.database.connector.core.type.DatabaseTypeFactory;
-import org.apache.shardingsphere.mcp.support.database.metadata.model.MCPColumnMetadata;
-import org.apache.shardingsphere.mcp.support.database.metadata.model.MCPDatabaseMetadata;
-import org.apache.shardingsphere.mcp.support.database.metadata.model.MCPIndexMetadata;
-import org.apache.shardingsphere.mcp.support.database.metadata.model.MCPSequenceMetadata;
-import org.apache.shardingsphere.mcp.support.database.metadata.model.MCPSchemaMetadata;
-import org.apache.shardingsphere.mcp.support.database.metadata.model.MCPTableMetadata;
-import org.apache.shardingsphere.mcp.support.database.metadata.model.MCPViewMetadata;
 import org.apache.shardingsphere.mcp.support.database.capability.SupportedMCPMetadataObjectType;
-import org.apache.shardingsphere.mcp.support.fixture.SupportDatabaseTypeFactoryMocker;
+import org.apache.shardingsphere.mcp.support.database.metadata.model.MCPDatabaseMetadata;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.Arguments;
 import org.junit.jupiter.params.provider.MethodSource;
-import org.mockito.MockedStatic;
 
-import java.sql.Connection;
-import java.sql.DatabaseMetaData;
-import java.sql.Driver;
-import java.sql.DriverManager;
-import java.sql.DriverPropertyInfo;
-import java.sql.ResultSet;
 import java.sql.SQLException;
-import java.sql.SQLFeatureNotSupportedException;
-import java.sql.Statement;
-import java.util.LinkedHashMap;
-import java.util.Locale;
-import java.util.List;
 import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Optional;
-import java.util.Properties;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.logging.Logger;
-import java.util.stream.Stream;
 
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.is;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.isNull;
-import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
 
-class MCPJdbcMetadataLoaderTest {
-    
-    private static final Map<String, String> METADATA_JDBC_URLS = Map.of(
-            "MySQL", "jdbc:mysql://metadata-loader/test",
-            "PostgreSQL", "jdbc:postgresql://metadata-loader/test",
-            "openGauss", "jdbc:opengauss://metadata-loader/test",
-            "SQLServer", "jdbc:sqlserver://metadata-loader",
-            "Oracle", "jdbc:oracle:thin:@metadata-loader",
-            "MariaDB", "jdbc:mariadb://metadata-loader/test",
-            "Firebird", "jdbc:firebirdsql://metadata-loader/test");
+class MCPJdbcMetadataLoaderTest extends AbstractMCPJdbcMetadataLoaderTest {
     
     @Test
     void assertLoad() throws SQLException {
@@ -103,74 +56,6 @@ class MCPJdbcMetadataLoaderTest {
         assertTrue(actual.findMetadata("analytics_db").isPresent());
     }
     
-    @Test
-    void assertLoadWithoutSchemaObjects() throws SQLException {
-        Driver mockDriver = new MockDriver("jdbc:mock:no-schema", createConnectionWithoutSchema("MySQL"));
-        try (MockDriverRegistration registration = MockDriverRegistration.register(mockDriver)) {
-            LoadedMetadataCatalog actual = load(Map.of("logic_db", new RuntimeDatabaseConfiguration("jdbc:mock:no-schema", "", "", MockDriver.class.getName())));
-            MCPDatabaseMetadata databaseMetadata = actual.findMetadata("logic_db").orElseThrow();
-            assertThat(databaseMetadata.getSchemas().size(), is(1));
-            assertThat(databaseMetadata.getSchemas().get(0).getSchema(), is("logic_db"));
-            assertTrue(containsMetadata(databaseMetadata, SupportedMCPMetadataObjectType.TABLE, "orders"));
-            assertTrue(containsMetadata(databaseMetadata, SupportedMCPMetadataObjectType.COLUMN, "order_id"));
-            assertThat(databaseMetadata.getDatabaseVersion(), is(""));
-        }
-    }
-    
-    @Test
-    void assertLoadWithoutSystemCatalogForDatabaseAsSchema() throws SQLException {
-        Connection connection = mock(Connection.class);
-        DatabaseMetaData databaseMetaData = mock(DatabaseMetaData.class);
-        when(connection.getMetaData()).thenReturn(databaseMetaData);
-        when(databaseMetaData.getDatabaseProductVersion()).thenReturn("");
-        when(databaseMetaData.getURL()).thenReturn(getMetadataJdbcUrl("MySQL"));
-        mockEmptyScalarQueries(connection);
-        when(databaseMetaData.getTables(isNull(), isNull(), eq("%"), any(String[].class))).thenAnswer(invocation -> {
-            String[] tableTypes = invocation.getArgument(3);
-            return mockMultiRowResultSet("TABLE".equals(tableTypes[0])
-                    ? List.of(
-                            Map.of("TABLE_CAT", "information_schema", "TABLE_SCHEM", "", "TABLE_NAME", "GLOBAL_STATUS"),
-                            Map.of("TABLE_CAT", "logic_db", "TABLE_SCHEM", "", "TABLE_NAME", "orders"))
-                    : List.of());
-        });
-        ResultSet orderColumns = mockResultSet("COLUMN_NAME", "order_id");
-        when(databaseMetaData.getColumns(eq("logic_db"), isNull(), eq("orders"), eq("%"))).thenReturn(orderColumns);
-        ResultSet orderIndexes = mockResultSet("INDEX_NAME", "idx_orders_status");
-        when(databaseMetaData.getIndexInfo(eq("logic_db"), isNull(), eq("orders"), eq(false), eq(false))).thenReturn(orderIndexes);
-        when(databaseMetaData.getColumns(eq("information_schema"), isNull(), eq("GLOBAL_STATUS"), eq("%")))
-                .thenThrow(new SQLException("system catalog should be skipped"));
-        RuntimeDatabaseConfiguration runtimeDatabaseConfiguration = createMockRuntimeDatabaseConfiguration(connection);
-        MCPDatabaseMetadata actual = load(Map.of("logic_db", runtimeDatabaseConfiguration)).findMetadata("logic_db").orElseThrow();
-        assertTrue(containsMetadata(actual, SupportedMCPMetadataObjectType.TABLE, "orders"));
-        assertFalse(containsMetadata(actual, SupportedMCPMetadataObjectType.TABLE, "GLOBAL_STATUS"));
-        assertThat(actual.getSchemas().get(0).getSchema(), is("logic_db"));
-    }
-    
-    @Test
-    void assertLoadWithCatalogBackedMetadataUsingLogicalSchemaName() throws SQLException {
-        Connection connection = mock(Connection.class);
-        DatabaseMetaData databaseMetaData = mock(DatabaseMetaData.class);
-        when(connection.getMetaData()).thenReturn(databaseMetaData);
-        when(databaseMetaData.getDatabaseProductVersion()).thenReturn("");
-        when(databaseMetaData.getURL()).thenReturn(getMetadataJdbcUrl("MySQL"));
-        mockEmptyScalarQueries(connection);
-        when(databaseMetaData.getTables(isNull(), isNull(), eq("%"), any(String[].class))).thenAnswer(invocation -> {
-            String[] tableTypes = invocation.getArgument(3);
-            return mockMultiRowResultSet("TABLE".equals(tableTypes[0])
-                    ? List.of(Map.of("TABLE_CAT", "orders", "TABLE_SCHEM", "", "TABLE_NAME", "orders"))
-                    : List.of());
-        });
-        ResultSet orderColumns = mockResultSet("COLUMN_NAME", "order_id");
-        when(databaseMetaData.getColumns(eq("orders"), isNull(), eq("orders"), eq("%"))).thenReturn(orderColumns);
-        ResultSet orderIndexes = mockResultSet("INDEX_NAME", "idx_orders_status");
-        when(databaseMetaData.getIndexInfo(eq("orders"), isNull(), eq("orders"), eq(false), eq(false))).thenReturn(orderIndexes);
-        RuntimeDatabaseConfiguration runtimeDatabaseConfiguration = createMockRuntimeDatabaseConfiguration(connection);
-        MCPDatabaseMetadata actual = load(Map.of("logic_db", runtimeDatabaseConfiguration)).findMetadata("logic_db").orElseThrow();
-        assertThat(actual.getSchemas().get(0).getSchema(), is("logic_db"));
-        assertTrue(containsMetadata(actual, SupportedMCPMetadataObjectType.TABLE, "orders"));
-        assertTrue(containsMetadata(actual, SupportedMCPMetadataObjectType.INDEX, "idx_orders_status"));
-    }
-    
     @Test
     void assertLoadWithSchemaRegisteredOnce() throws SQLException {
         LoadedMetadataCatalog actual = load(Map.of("logic_db", createMockRuntimeDatabaseConfiguration(createStandardPostgreSQLMetadataConnection())));
@@ -179,505 +64,4 @@ class MCPJdbcMetadataLoaderTest {
         assertTrue(containsMetadata(databaseMetadata, SupportedMCPMetadataObjectType.VIEW, "active_orders"));
         assertThat(databaseMetadata.getSchemas().size(), is(1));
     }
-    
-    @ParameterizedTest(name = "{0}")
-    @MethodSource("loadWithoutSequenceQueryArguments")
-    void assertLoadWithoutSequenceQuery(final String name, final String databaseType) throws SQLException {
-        RuntimeDatabaseConfiguration runtimeDatabaseConfiguration = createMockRuntimeDatabaseConfiguration(createConnectionWithoutSchema(databaseType));
-        MCPDatabaseMetadata actual = load(Map.of("logic_db", runtimeDatabaseConfiguration)).findMetadata("logic_db").orElseThrow();
-        assertTrue(containsMetadata(actual, SupportedMCPMetadataObjectType.TABLE, "orders"));
-        assertFalse(containsMetadata(actual, SupportedMCPMetadataObjectType.SEQUENCE, "order_seq"));
-    }
-    
-    @Test
-    void assertLoadWithFailedConnection() throws SQLException {
-        RuntimeDatabaseConfiguration runtimeDatabaseConfiguration = mock(RuntimeDatabaseConfiguration.class);
-        SQLException expected = new SQLException("connection failed");
-        when(runtimeDatabaseConfiguration.openConnection("logic_db")).thenThrow(expected);
-        MCPJdbcMetadataLoader metadataLoader = new MCPJdbcMetadataLoader();
-        RuntimeDatabaseConnectionException actual = assertThrows(RuntimeDatabaseConnectionException.class,
-                () -> metadataLoader.load("logic_db", runtimeDatabaseConfiguration, new RuntimeDatabaseProfile("logic_db", "PostgreSQL", "")));
-        assertThat(actual.getMessage(), is("Runtime database `logic_db` connection failed: connection_failed."));
-        assertThat(actual.getCause(), is(expected));
-    }
-    
-    @Test
-    void assertLoadWithoutEmptyTableName() throws SQLException {
-        Connection connection = createConnectionWithMetadata(
-                List.of(Map.of("TABLE_SCHEM", "PUBLIC", "TABLE_NAME", ""), Map.of("TABLE_SCHEM", "PUBLIC", "TABLE_NAME", "orders")),
-                List.of(),
-                Map.of("orders", List.of("order_id")),
-                Map.of(),
-                List.of());
-        RuntimeDatabaseConfiguration runtimeDatabaseConfiguration = createMockRuntimeDatabaseConfiguration(connection);
-        MCPDatabaseMetadata actual = load(Map.of("logic_db", runtimeDatabaseConfiguration)).findMetadata("logic_db").orElseThrow();
-        assertTrue(containsMetadata(actual, SupportedMCPMetadataObjectType.TABLE, "orders"));
-        assertFalse(containsMetadata(actual, SupportedMCPMetadataObjectType.TABLE, ""));
-    }
-    
-    @Test
-    void assertLoadWithoutEmptyViewName() throws SQLException {
-        Connection connection = createConnectionWithMetadata(
-                List.of(),
-                List.of(Map.of("TABLE_SCHEM", "PUBLIC", "TABLE_NAME", ""), Map.of("TABLE_SCHEM", "PUBLIC", "TABLE_NAME", "active_orders")),
-                Map.of("active_orders", List.of("order_id")),
-                Map.of(),
-                List.of());
-        RuntimeDatabaseConfiguration runtimeDatabaseConfiguration = createMockRuntimeDatabaseConfiguration(connection);
-        MCPDatabaseMetadata actual = load(Map.of("logic_db", runtimeDatabaseConfiguration)).findMetadata("logic_db").orElseThrow();
-        assertTrue(containsMetadata(actual, SupportedMCPMetadataObjectType.VIEW, "active_orders"));
-        assertFalse(containsMetadata(actual, SupportedMCPMetadataObjectType.VIEW, ""));
-    }
-    
-    @Test
-    void assertLoadWithoutEmptyColumnName() throws SQLException {
-        Connection connection = createConnectionWithMetadata(
-                List.of(Map.of("TABLE_SCHEM", "PUBLIC", "TABLE_NAME", "orders")),
-                List.of(),
-                Map.of("orders", List.of("", "order_id")),
-                Map.of(),
-                List.of());
-        RuntimeDatabaseConfiguration runtimeDatabaseConfiguration = createMockRuntimeDatabaseConfiguration(connection);
-        MCPDatabaseMetadata actual = load(Map.of("logic_db", runtimeDatabaseConfiguration)).findMetadata("logic_db").orElseThrow();
-        assertTrue(containsMetadata(actual, SupportedMCPMetadataObjectType.COLUMN, "order_id"));
-        assertFalse(containsMetadata(actual, SupportedMCPMetadataObjectType.COLUMN, ""));
-    }
-    
-    @Test
-    void assertLoadWithoutEmptyOrDuplicateIndexName() throws SQLException {
-        Connection connection = createConnectionWithMetadata(
-                List.of(Map.of("TABLE_SCHEM", "PUBLIC", "TABLE_NAME", "orders")),
-                List.of(),
-                Map.of("orders", List.of("order_id")),
-                Map.of("orders", List.of("", "idx_orders_status", "idx_orders_status")),
-                List.of());
-        RuntimeDatabaseConfiguration runtimeDatabaseConfiguration = createMockRuntimeDatabaseConfiguration(connection);
-        MCPDatabaseMetadata actual = load(Map.of("logic_db", runtimeDatabaseConfiguration)).findMetadata("logic_db").orElseThrow();
-        assertTrue(containsMetadata(actual, SupportedMCPMetadataObjectType.INDEX, "idx_orders_status"));
-        assertThat(countMetadata(actual, SupportedMCPMetadataObjectType.INDEX, "idx_orders_status"), is(1));
-    }
-    
-    @Test
-    void assertLoadWithoutSystemSchemaSequence() throws SQLException {
-        Connection connection = createConnectionWithMetadata("PostgreSQL",
-                List.of(),
-                List.of(),
-                Map.of(),
-                Map.of(),
-                List.of(Map.of("SEQUENCE_SCHEMA", "PG_CATALOG", "SEQUENCE_NAME", "order_seq")));
-        RuntimeDatabaseConfiguration runtimeDatabaseConfiguration = createMockRuntimeDatabaseConfiguration(connection);
-        MCPDatabaseMetadata actual = load(Map.of("logic_db", runtimeDatabaseConfiguration)).findMetadata("logic_db").orElseThrow();
-        assertFalse(containsMetadata(actual, SupportedMCPMetadataObjectType.SEQUENCE, "order_seq"));
-    }
-    
-    @Test
-    void assertLoadWithoutEmptySequenceName() throws SQLException {
-        Connection connection = createConnectionWithMetadata("PostgreSQL",
-                List.of(),
-                List.of(),
-                Map.of(),
-                Map.of(),
-                List.of(Map.of("SEQUENCE_SCHEMA", "public", "SEQUENCE_NAME", "")));
-        RuntimeDatabaseConfiguration runtimeDatabaseConfiguration = createMockRuntimeDatabaseConfiguration(connection);
-        MCPDatabaseMetadata actual = load(Map.of("logic_db", runtimeDatabaseConfiguration)).findMetadata("logic_db").orElseThrow();
-        assertFalse(containsMetadata(actual, SupportedMCPMetadataObjectType.SEQUENCE, ""));
-    }
-    
-    @Test
-    void assertLoadWithDuplicateTableRows() throws SQLException {
-        Connection connection = createConnectionWithMetadata(
-                List.of(Map.of("TABLE_SCHEM", "PUBLIC", "TABLE_NAME", "orders"), Map.of("TABLE_SCHEM", "PUBLIC", "TABLE_NAME", "orders")),
-                List.of(),
-                Map.of("orders", List.of("order_id")),
-                Map.of(),
-                List.of());
-        RuntimeDatabaseConfiguration runtimeDatabaseConfiguration = createMockRuntimeDatabaseConfiguration(connection);
-        MCPDatabaseMetadata actual = load(Map.of("logic_db", runtimeDatabaseConfiguration)).findMetadata("logic_db").orElseThrow();
-        assertThat(countMetadata(actual, SupportedMCPMetadataObjectType.TABLE, "orders"), is(1));
-    }
-    
-    @Test
-    void assertLoadWithDuplicateViewRows() throws SQLException {
-        Connection connection = createConnectionWithMetadata(
-                List.of(),
-                List.of(Map.of("TABLE_SCHEM", "PUBLIC", "TABLE_NAME", "active_orders"), Map.of("TABLE_SCHEM", "PUBLIC", "TABLE_NAME", "active_orders")),
-                Map.of("active_orders", List.of("order_id")),
-                Map.of(),
-                List.of());
-        RuntimeDatabaseConfiguration runtimeDatabaseConfiguration = createMockRuntimeDatabaseConfiguration(connection);
-        MCPDatabaseMetadata actual = load(Map.of("logic_db", runtimeDatabaseConfiguration)).findMetadata("logic_db").orElseThrow();
-        assertThat(countMetadata(actual, SupportedMCPMetadataObjectType.VIEW, "active_orders"), is(1));
-    }
-    
-    @Test
-    void assertLoadWithFailedSequenceMetadataQuery() throws SQLException {
-        Driver mockDriver = new MockDriver("jdbc:mock:failed-sequence-query", createConnectionWithFailedSequenceMetadataQuery());
-        try (MockDriverRegistration registration = MockDriverRegistration.register(mockDriver)) {
-            RuntimeDatabaseConnectionException actual = assertThrows(RuntimeDatabaseConnectionException.class,
-                    () -> load(Map.of("logic_db", new RuntimeDatabaseConfiguration("jdbc:mock:failed-sequence-query", "", "", MockDriver.class.getName()))));
-            assertThat(actual.getMessage(), is("Runtime database `logic_db` connection failed: connection_failed."));
-            assertThat(actual.getCause().getMessage(), is("sequence metadata query failed"));
-        }
-    }
-    
-    @ParameterizedTest(name = "{0}")
-    @MethodSource("loadSequenceDialectArguments")
-    void assertLoadWithDialectSequenceMetadata(final String name, final String databaseType, final String sequenceSchema,
-                                               final String sequenceName, final String sequenceQuery) throws SQLException {
-        String jdbcUrl = "jdbc:mock:sequence:" + databaseType.toLowerCase(Locale.ENGLISH);
-        Driver mockDriver = new MockDriver(jdbcUrl, createConnectionWithSequenceMetadata(databaseType, sequenceSchema, sequenceName, sequenceQuery));
-        try (MockDriverRegistration registration = MockDriverRegistration.register(mockDriver)) {
-            LoadedMetadataCatalog actual = load(Map.of("logic_db", new RuntimeDatabaseConfiguration(jdbcUrl, "", "", MockDriver.class.getName())));
-            assertTrue(containsMetadata(actual.findMetadata("logic_db").orElseThrow(), SupportedMCPMetadataObjectType.SEQUENCE, sequenceName));
-        }
-    }
-    
-    private LoadedMetadataCatalog load(final Map<String, RuntimeDatabaseConfiguration> runtimeDatabases) {
-        try (MockedStatic<DatabaseTypeFactory> ignored = SupportDatabaseTypeFactoryMocker.mockByConnectionMetadata()) {
-            MCPJdbcDatabaseProfileLoader databaseProfileLoader = new MCPJdbcDatabaseProfileLoader();
-            MCPJdbcMetadataLoader metadataLoader = new MCPJdbcMetadataLoader();
-            Map<String, RuntimeDatabaseProfile> databaseProfiles = databaseProfileLoader.load(runtimeDatabases);
-            Map<String, MCPDatabaseMetadata> result = new LinkedHashMap<>(runtimeDatabases.size(), 1F);
-            for (Entry<String, RuntimeDatabaseConfiguration> entry : runtimeDatabases.entrySet()) {
-                result.put(entry.getKey(), metadataLoader.load(entry.getKey(), entry.getValue(), databaseProfiles.get(entry.getKey())));
-            }
-            return new LoadedMetadataCatalog(result);
-        }
-    }
-    
-    private static Stream<Arguments> loadTypedMetadataArguments() {
-        return Stream.of(
-                Arguments.of("table orders", SupportedMCPMetadataObjectType.TABLE, "orders"),
-                Arguments.of("table order_items", SupportedMCPMetadataObjectType.TABLE, "order_items"),
-                Arguments.of("view active_orders", SupportedMCPMetadataObjectType.VIEW, "active_orders"),
-                Arguments.of("column status", SupportedMCPMetadataObjectType.COLUMN, "status"),
-                Arguments.of("index idx_orders_status", SupportedMCPMetadataObjectType.INDEX, "idx_orders_status"),
-                Arguments.of("sequence order_seq", SupportedMCPMetadataObjectType.SEQUENCE, "order_seq"));
-    }
-    
-    private static Stream<Arguments> loadSequenceDialectArguments() {
-        return Stream.of(
-                Arguments.of("postgresql", "PostgreSQL", "public", "order_seq",
-                        "SELECT sequence_schema AS SEQUENCE_SCHEMA, sequence_name AS SEQUENCE_NAME FROM information_schema.sequences"),
-                Arguments.of("open gauss", "openGauss", "public", "order_seq",
-                        "SELECT sequence_schema AS SEQUENCE_SCHEMA, sequence_name AS SEQUENCE_NAME FROM information_schema.sequences"),
-                Arguments.of("sql server", "SQLServer", "dbo", "order_seq",
-                        "SELECT schemas.name AS SEQUENCE_SCHEMA, seq.name AS SEQUENCE_NAME FROM sys.sequences seq INNER JOIN sys.schemas schemas ON seq.schema_id = schemas.schema_id"),
-                Arguments.of("oracle", "Oracle", "APP", "ORDER_SEQ",
-                        "SELECT USER AS SEQUENCE_SCHEMA, sequence_name AS SEQUENCE_NAME FROM USER_SEQUENCES"),
-                Arguments.of("mariadb", "MariaDB", "logic_db", "order_seq",
-                        "SELECT TABLE_SCHEMA AS SEQUENCE_SCHEMA, TABLE_NAME AS SEQUENCE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'SEQUENCE'"),
-                Arguments.of("firebird", "Firebird", "", "ORDER_SEQ",
-                        "SELECT '' AS SEQUENCE_SCHEMA, TRIM(RDB$GENERATOR_NAME) AS SEQUENCE_NAME FROM RDB$GENERATORS WHERE COALESCE(RDB$SYSTEM_FLAG, 0) = 0"));
-    }
-    
-    private static Stream<Arguments> loadWithoutSequenceQueryArguments() {
-        return Stream.of(
-                Arguments.of("unsupported database type", "MySQL"));
-    }
-    
-    private Connection createConnectionWithoutSchema(final String databaseType) throws SQLException {
-        return createConnectionWithoutSchemaByURL(getMetadataJdbcUrl(databaseType));
-    }
-    
-    private Connection createConnectionWithoutSchemaByURL(final String jdbcUrl) throws SQLException {
-        Connection result = mock(Connection.class);
-        DatabaseMetaData databaseMetaData = mock(DatabaseMetaData.class);
-        Statement statement = mock(Statement.class);
-        ResultSet tableResultSet = mockResultSet("TABLE_NAME", "orders");
-        ResultSet viewResultSet = mockResultSet("TABLE_NAME");
-        ResultSet columnResultSet = mockResultSet("COLUMN_NAME", "order_id");
-        ResultSet indexResultSet = mockResultSet("INDEX_NAME");
-        ResultSet emptyQueryResultSet = mockMultiRowResultSet(List.of());
-        when(result.getMetaData()).thenReturn(databaseMetaData);
-        when(result.createStatement()).thenReturn(statement);
-        when(databaseMetaData.getURL()).thenReturn(jdbcUrl);
-        when(statement.executeQuery(anyString())).thenReturn(emptyQueryResultSet);
-        when(databaseMetaData.getTables(isNull(), isNull(), eq("%"), any(String[].class))).thenAnswer(invocation -> {
-            String[] tableTypes = invocation.getArgument(3);
-            return "TABLE".equals(tableTypes[0]) ? tableResultSet : viewResultSet;
-        });
-        when(databaseMetaData.getColumns(isNull(), isNull(), eq("orders"), eq("%"))).thenReturn(columnResultSet);
-        when(databaseMetaData.getIndexInfo(isNull(), isNull(), eq("orders"), eq(false), eq(false))).thenReturn(indexResultSet);
-        return result;
-    }
-    
-    private Connection createConnectionWithSequenceMetadata(final String databaseType, final String sequenceSchema,
-                                                            final String sequenceName, final String sequenceQuery) throws SQLException {
-        Connection result = mock(Connection.class);
-        DatabaseMetaData databaseMetaData = mock(DatabaseMetaData.class);
-        Statement statement = mock(Statement.class);
-        ResultSet tableResultSet = mockResultSet("TABLE_NAME");
-        ResultSet viewResultSet = mockResultSet("TABLE_NAME");
-        ResultSet sequenceResultSet = mockSingleRowResultSet(Map.of("SEQUENCE_SCHEMA", sequenceSchema, "SEQUENCE_NAME", sequenceName));
-        ResultSet emptyQueryResultSet = mockMultiRowResultSet(List.of());
-        when(result.getMetaData()).thenReturn(databaseMetaData);
-        when(result.createStatement()).thenReturn(statement);
-        when(databaseMetaData.getDatabaseProductVersion()).thenReturn("");
-        when(databaseMetaData.getURL()).thenReturn(getMetadataJdbcUrl(databaseType));
-        when(statement.executeQuery(anyString())).thenReturn(emptyQueryResultSet);
-        when(databaseMetaData.getTables(isNull(), isNull(), eq("%"), any(String[].class))).thenAnswer(invocation -> {
-            String[] tableTypes = invocation.getArgument(3);
-            return "TABLE".equals(tableTypes[0]) ? tableResultSet : viewResultSet;
-        });
-        when(statement.executeQuery(sequenceQuery)).thenReturn(sequenceResultSet);
-        return result;
-    }
-    
-    private Connection createConnectionWithFailedSequenceMetadataQuery() throws SQLException {
-        Connection result = mock(Connection.class);
-        DatabaseMetaData databaseMetaData = mock(DatabaseMetaData.class);
-        Statement statement = mock(Statement.class);
-        ResultSet tableResultSet = mockResultSet("TABLE_NAME");
-        ResultSet viewResultSet = mockResultSet("TABLE_NAME");
-        when(result.getMetaData()).thenReturn(databaseMetaData);
-        when(result.createStatement()).thenReturn(statement);
-        when(databaseMetaData.getDatabaseProductVersion()).thenReturn("");
-        when(databaseMetaData.getURL()).thenReturn(getMetadataJdbcUrl("PostgreSQL"));
-        when(databaseMetaData.getTables(isNull(), isNull(), eq("%"), any(String[].class))).thenAnswer(invocation -> {
-            String[] tableTypes = invocation.getArgument(3);
-            return "TABLE".equals(tableTypes[0]) ? tableResultSet : viewResultSet;
-        });
-        when(statement.executeQuery("SELECT sequence_schema AS SEQUENCE_SCHEMA, sequence_name AS SEQUENCE_NAME FROM information_schema.sequences"))
-                .thenThrow(new SQLException("sequence metadata query failed"));
-        return result;
-    }
-    
-    private Connection createStandardPostgreSQLMetadataConnection() throws SQLException {
-        Connection result = createConnectionWithMetadata(
-                List.of(
-                        Map.of("TABLE_SCHEM", "PUBLIC", "TABLE_NAME", "orders"),
-                        Map.of("TABLE_SCHEM", "PUBLIC", "TABLE_NAME", "order_items")),
-                List.of(Map.of("TABLE_SCHEM", "PUBLIC", "TABLE_NAME", "active_orders")),
-                Map.of(
-                        "orders", List.of("order_id", "status", "amount"),
-                        "order_items", List.of("item_id", "order_id", "sku"),
-                        "active_orders", List.of("order_id", "status")),
-                Map.of(
-                        "orders", List.of("PRIMARY_KEY_C", "idx_orders_status"),
-                        "order_items", List.of("PRIMARY_KEY_C")),
-                List.of(Map.of("SEQUENCE_SCHEMA", "PUBLIC", "SEQUENCE_NAME", "order_seq")));
-        DatabaseMetaData databaseMetaData = result.getMetaData();
-        when(databaseMetaData.getDatabaseProductVersion()).thenReturn("16.2");
-        when(databaseMetaData.getURL()).thenReturn("jdbc:postgresql://metadata-loader/test");
-        return result;
-    }
-    
-    private RuntimeDatabaseConfiguration createMockRuntimeDatabaseConfiguration(final Connection connection) throws SQLException {
-        RuntimeDatabaseConfiguration result = mock(RuntimeDatabaseConfiguration.class);
-        when(result.openConnection(anyString())).thenReturn(connection);
-        return result;
-    }
-    
-    private Connection createConnectionWithMetadata(final List<Map<String, String>> tableRows, final List<Map<String, String>> viewRows, final Map<String, List<String>> columns,
-                                                    final Map<String, List<String>> indexes, final List<Map<String, String>> sequenceRows) throws SQLException {
-        return createConnectionWithMetadata("PostgreSQL", tableRows, viewRows, columns, indexes, sequenceRows);
-    }
-    
-    private Connection createConnectionWithMetadata(final String databaseType, final List<Map<String, String>> tableRows, final List<Map<String, String>> viewRows,
-                                                    final Map<String, List<String>> columns, final Map<String, List<String>> indexes,
-                                                    final List<Map<String, String>> sequenceRows) throws SQLException {
-        Connection result = mock(Connection.class);
-        DatabaseMetaData databaseMetaData = mock(DatabaseMetaData.class);
-        Statement statement = mock(Statement.class);
-        ResultSet sequenceResultSet = mockMultiRowResultSet(sequenceRows);
-        ResultSet emptyQueryResultSet = mockMultiRowResultSet(List.of());
-        when(result.getMetaData()).thenReturn(databaseMetaData);
-        when(result.createStatement()).thenReturn(statement);
-        when(databaseMetaData.getDatabaseProductVersion()).thenReturn("");
-        when(databaseMetaData.getURL()).thenReturn(getMetadataJdbcUrl(databaseType));
-        when(statement.executeQuery(anyString())).thenReturn(emptyQueryResultSet);
-        when(databaseMetaData.getTables(isNull(), isNull(), eq("%"), any(String[].class))).thenAnswer(invocation -> {
-            String[] tableTypes = invocation.getArgument(3);
-            return mockMultiRowResultSet("TABLE".equals(tableTypes[0]) ? tableRows : viewRows);
-        });
-        when(databaseMetaData.getColumns(isNull(), nullable(String.class), anyString(), eq("%"))).thenAnswer(invocation -> {
-            String objectName = invocation.getArgument(2);
-            return mockResultSet("COLUMN_NAME", columns.getOrDefault(objectName, List.of()).toArray(new String[0]));
-        });
-        when(databaseMetaData.getIndexInfo(isNull(), nullable(String.class), anyString(), eq(false), eq(false))).thenAnswer(invocation -> {
-            String tableName = invocation.getArgument(2);
-            return mockResultSet("INDEX_NAME", indexes.getOrDefault(tableName, List.of()).toArray(new String[0]));
-        });
-        when(statement.executeQuery(getSequenceQuery(databaseType))).thenReturn(sequenceResultSet);
-        return result;
-    }
-    
-    private void mockEmptyScalarQueries(final Connection connection) throws SQLException {
-        Statement statement = mock(Statement.class);
-        ResultSet resultSet = mockMultiRowResultSet(List.of());
-        when(connection.createStatement()).thenReturn(statement);
-        when(statement.executeQuery(anyString())).thenReturn(resultSet);
-    }
-    
-    private String getSequenceQuery(final String databaseType) {
-        switch (databaseType) {
-            case "PostgreSQL":
-            case "openGauss":
-                return "SELECT sequence_schema AS SEQUENCE_SCHEMA, sequence_name AS SEQUENCE_NAME FROM information_schema.sequences";
-            case "SQLServer":
-                return "SELECT schemas.name AS SEQUENCE_SCHEMA, seq.name AS SEQUENCE_NAME FROM sys.sequences seq INNER JOIN sys.schemas schemas ON seq.schema_id = schemas.schema_id";
-            case "Oracle":
-                return "SELECT USER AS SEQUENCE_SCHEMA, sequence_name AS SEQUENCE_NAME FROM USER_SEQUENCES";
-            case "MariaDB":
-                return "SELECT TABLE_SCHEMA AS SEQUENCE_SCHEMA, TABLE_NAME AS SEQUENCE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'SEQUENCE'";
-            case "Firebird":
-                return "SELECT '' AS SEQUENCE_SCHEMA, TRIM(RDB$GENERATOR_NAME) AS SEQUENCE_NAME FROM RDB$GENERATORS WHERE COALESCE(RDB$SYSTEM_FLAG, 0) = 0";
-            default:
-                return "";
-        }
-    }
-    
-    private ResultSet mockResultSet(final String columnName, final String... values) throws SQLException {
-        ResultSet result = mock(ResultSet.class);
-        AtomicInteger nextIndex = new AtomicInteger();
-        AtomicInteger valueIndex = new AtomicInteger();
-        when(result.next()).thenAnswer(invocation -> nextIndex.getAndIncrement() < values.length);
-        when(result.getString(columnName)).thenAnswer(invocation -> values[valueIndex.getAndIncrement()]);
-        return result;
-    }
-    
-    private ResultSet mockSingleRowResultSet(final Map<String, String> values) throws SQLException {
-        ResultSet result = mock(ResultSet.class);
-        when(result.next()).thenReturn(true, false);
-        for (Entry<String, String> entry : values.entrySet()) {
-            when(result.getString(entry.getKey())).thenReturn(entry.getValue());
-        }
-        return result;
-    }
-    
-    private ResultSet mockMultiRowResultSet(final List<Map<String, String>> values) throws SQLException {
-        ResultSet result = mock(ResultSet.class);
-        AtomicInteger rowIndex = new AtomicInteger(-1);
-        when(result.next()).thenAnswer(invocation -> rowIndex.incrementAndGet() < values.size());
-        when(result.getString(anyString())).thenAnswer(invocation -> {
-            int currentRowIndex = rowIndex.get();
-            return 0 <= currentRowIndex && currentRowIndex < values.size() ? values.get(currentRowIndex).get(invocation.getArgument(0)) : null;
-        });
-        return result;
-    }
-    
-    private String getMetadataJdbcUrl(final String databaseType) {
-        return METADATA_JDBC_URLS.getOrDefault(databaseType, METADATA_JDBC_URLS.get("PostgreSQL"));
-    }
-    
-    private int countMetadata(final MCPDatabaseMetadata databaseMetadata, final SupportedMCPMetadataObjectType objectType, final String objectName) {
-        int result = 0;
-        for (MCPSchemaMetadata each : databaseMetadata.getSchemas()) {
-            if (SupportedMCPMetadataObjectType.SCHEMA == objectType && objectName.equals(each.getSchema())) {
-                result++;
-            }
-            for (MCPTableMetadata table : each.getTables()) {
-                if (SupportedMCPMetadataObjectType.TABLE == objectType && objectName.equals(table.getTable())) {
-                    result++;
-                }
-                for (MCPColumnMetadata column : table.getColumns()) {
-                    if (SupportedMCPMetadataObjectType.COLUMN == objectType && objectName.equals(column.getColumn())) {
-                        result++;
-                    }
-                }
-                for (MCPIndexMetadata index : table.getIndexes()) {
-                    if (SupportedMCPMetadataObjectType.INDEX == objectType && objectName.equals(index.getIndex())) {
-                        result++;
-                    }
-                }
-            }
-            for (MCPViewMetadata view : each.getViews()) {
-                if (SupportedMCPMetadataObjectType.VIEW == objectType && objectName.equals(view.getView())) {
-                    result++;
-                }
-                for (MCPColumnMetadata column : view.getColumns()) {
-                    if (SupportedMCPMetadataObjectType.COLUMN == objectType && objectName.equals(column.getColumn())) {
-                        result++;
-                    }
-                }
-            }
-            for (MCPSequenceMetadata sequence : each.getSequences()) {
-                if (SupportedMCPMetadataObjectType.SEQUENCE == objectType && objectName.equals(sequence.getSequence())) {
-                    result++;
-                }
-            }
-        }
-        return result;
-    }
-    
-    private boolean containsMetadata(final MCPDatabaseMetadata databaseMetadata, final SupportedMCPMetadataObjectType objectType, final String objectName) {
-        return 0 < countMetadata(databaseMetadata, objectType, objectName);
-    }
-    
-    @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
-    private static final class LoadedMetadataCatalog {
-        
-        private final Map<String, MCPDatabaseMetadata> databaseMetadataMap;
-        
-        private Optional<MCPDatabaseMetadata> findMetadata(final String databaseName) {
-            return Optional.ofNullable(databaseMetadataMap.get(databaseName));
-        }
-        
-        private Map<String, MCPDatabaseMetadata> getDatabaseMetadataMap() {
-            return databaseMetadataMap;
-        }
-    }
-    
-    private static final class MockDriverRegistration implements AutoCloseable {
-        
-        private final Driver driver;
-        
-        private MockDriverRegistration(final Driver driver) throws SQLException {
-            this.driver = driver;
-            DriverManager.registerDriver(driver);
-        }
-        
-        private static MockDriverRegistration register(final Driver driver) throws SQLException {
-            return new MockDriverRegistration(driver);
-        }
-        
-        @Override
-        public void close() throws SQLException {
-            DriverManager.deregisterDriver(driver);
-        }
-    }
-    
-    @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
-    private static final class MockDriver implements Driver {
-        
-        private final String jdbcUrl;
-        
-        private final Connection connection;
-        
-        @Override
-        public Connection connect(final String url, final Properties info) {
-            return acceptsURL(url) ? connection : null;
-        }
-        
-        @Override
-        public boolean acceptsURL(final String url) {
-            return jdbcUrl.equals(url);
-        }
-        
-        @Override
-        public DriverPropertyInfo[] getPropertyInfo(final String url, final Properties info) {
-            return new DriverPropertyInfo[0];
-        }
-        
-        @Override
-        public int getMajorVersion() {
-            return 1;
-        }
-        
-        @Override
-        public int getMinorVersion() {
-            return 0;
-        }
-        
-        @Override
-        public boolean jdbcCompliant() {
-            return false;
-        }
-        
-        @Override
-        public Logger getParentLogger() throws SQLFeatureNotSupportedException {
-            throw new SQLFeatureNotSupportedException("Mock driver does not expose a parent logger.");
-        }
-    }
 }
diff --git a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/descriptor/MCPDescriptorComponentValidatorsTest.java b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/descriptor/MCPDescriptorComponentValidatorsTest.java
new file mode 100644
index 00000000000..7eb5818d8fa
--- /dev/null
+++ b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/descriptor/MCPDescriptorComponentValidatorsTest.java
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.mcp.support.descriptor;
+
+import org.apache.shardingsphere.mcp.api.prompt.descriptor.MCPPromptArgumentDescriptor;
+import org.apache.shardingsphere.mcp.api.prompt.descriptor.MCPPromptDescriptor;
+import org.apache.shardingsphere.mcp.api.resource.descriptor.MCPResourceAnnotations;
+import org.apache.shardingsphere.mcp.api.resource.descriptor.MCPResourceDescriptor;
+import org.apache.shardingsphere.mcp.api.tool.descriptor.MCPToolAnnotations;
+import org.apache.shardingsphere.mcp.api.tool.descriptor.MCPToolDescriptor;
+import org.junit.jupiter.api.Test;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class MCPDescriptorComponentValidatorsTest {
+    
+    @Test
+    void assertResourceDescriptorValidator() {
+        MCPDescriptorCatalog catalog =
+                new MCPDescriptorCatalog(List.of(createResourceDescriptor()), List.of(createResourceDescriptor()), List.of(), List.of(), List.of(), List.of(), List.of(), List.of(), List.of());
+        IllegalStateException actual = assertThrows(IllegalStateException.class, () -> MCPResourceDescriptorValidator.validate(catalog));
+        assertThat(actual.getMessage(), is("Resource template `shardingsphere://capabilities` must contain template variables."));
+    }
+    
+    @Test
+    void assertToolDescriptorCatalogValidator() {
+        Map<String, Object> inputSchema = new LinkedHashMap<>(createInputSchema());
+        inputSchema.put("oneOf", List.of(Map.of("required", List.of("query"))));
+        MCPDescriptorCatalog catalog = new MCPDescriptorCatalog(List.of(), List.of(), List.of(), List.of(new MCPToolDescriptor(
+                "database_gateway_test_tool", "Test Tool", "Run a test tool.", inputSchema, createOutputSchema(), createAnnotations(), Map.of())), List.of(), List.of(), List.of(), List.of(),
+                List.of());
+        IllegalStateException actual = assertThrows(IllegalStateException.class, () -> MCPToolDescriptorCatalogValidator.validate(catalog));
+        assertThat(actual.getMessage(), is("Tool `database_gateway_test_tool` inputSchema contains unsupported top-level field `oneOf`."));
+    }
+    
+    @Test
+    void assertToolOutputSchemaValidator() {
+        MCPToolDescriptor descriptor = new MCPToolDescriptor("database_gateway_test_tool", "Test Tool", "Run a test tool.",
+                createInputSchema(), createOutputSchema(Map.of("recommended_next_tool", Map.of("type", "string", "description", "Removed alias."))), createAnnotations(), Map.of());
+        IllegalStateException actual = assertThrows(IllegalStateException.class, () -> MCPToolOutputSchemaValidator.validate(descriptor));
+        assertThat(actual.getMessage(), is("Tool `database_gateway_test_tool` model-facing contract must use canonical fields instead of removed `recommended_next_tool`."));
+    }
+    
+    @Test
+    void assertPromptDescriptorValidator() {
+        MCPPromptDescriptor prompt = new MCPPromptDescriptor("test_prompt", "Test Prompt", "Guide the model through a test prompt.", List.of(), Map.of());
+        MCPPromptTemplateBinding binding = new MCPPromptTemplateBinding("test_prompt", "META-INF/shardingsphere-mcp/prompts/fixture-single-brace-placeholder.md");
+        IllegalStateException actual = assertThrows(IllegalStateException.class, () -> MCPPromptDescriptorValidator.validate(List.of(prompt), List.of(binding)));
+        assertThat(actual.getMessage(), is("Prompt template `META-INF/shardingsphere-mcp/prompts/fixture-single-brace-placeholder.md` contains unsupported model-facing placeholder `{database}`."));
+    }
+    
+    @Test
+    void assertCompletionTargetDescriptorValidator() {
+        MCPPromptDescriptor prompt = new MCPPromptDescriptor("test_prompt", "Test Prompt", "Guide the model through a test prompt.",
+                List.of(new MCPPromptArgumentDescriptor("database", "Database", "Logical database.", false)), Map.of());
+        MCPCompletionTargetDescriptor completion = new MCPCompletionTargetDescriptor("prompt", "test_prompt", List.of("database"), 50,
+                Map.of(MCPShardingSphereMetadataKeys.REQUIRED_CONTEXT_ARGUMENTS, Map.of("database", List.of("tenant"))));
+        IllegalStateException actual = assertThrows(IllegalStateException.class, () -> MCPCompletionTargetDescriptorValidator.validate(List.of(completion), List.of(prompt), List.of()));
+        assertThat(actual.getMessage(), is("Completion target `prompt:test_prompt` context argument `tenant` for `database` is not declared by the target."));
+    }
+    
+    @Test
+    void assertResourceNavigationDescriptorValidator() {
+        MCPResourceNavigationDescriptor navigation = new MCPResourceNavigationDescriptor("missing", "database_gateway_test_tool", List.of(), List.of(), "Read test tool.");
+        MCPDescriptorCatalog catalog = new MCPDescriptorCatalog(List.of(), List.of(), List.of(), List.of(createToolDescriptor()), List.of(), List.of(), List.of(), List.of(), List.of());
+        IllegalStateException actual = assertThrows(IllegalStateException.class, () -> MCPResourceNavigationDescriptorValidator.validate(List.of(navigation), catalog));
+        assertThat(actual.getMessage(), is("Resource navigation references unknown source `missing`."));
+    }
+    
+    private MCPResourceDescriptor createResourceDescriptor() {
+        return new MCPResourceDescriptor("shardingsphere://capabilities", "server-capability-catalog", "Server Capability Catalog",
+                "Read the model-facing capability catalog.", "application/json", MCPResourceAnnotations.EMPTY, Map.of());
+    }
+    
+    private MCPToolDescriptor createToolDescriptor() {
+        return new MCPToolDescriptor("database_gateway_test_tool", "Test Tool", "Run a test tool.", createInputSchema(), createOutputSchema(), createAnnotations(), Map.of());
+    }
+    
+    private MCPToolAnnotations createAnnotations() {
+        return new MCPToolAnnotations("Test Tool", true, false, true, true);
+    }
+    
+    private Map<String, Object> createInputSchema() {
+        return Map.of("type", "object", "properties", Map.of("query", Map.of("type", "string", "description", "Query.")), "required", List.of("query"), "additionalProperties", false);
+    }
+    
+    private Map<String, Object> createOutputSchema() {
+        return createOutputSchema(Map.of("status", Map.of("type", "string", "description", "Status.")));
+    }
+    
+    private Map<String, Object> createOutputSchema(final Map<String, Object> properties) {
+        return Map.of("type", "object", "properties", properties, "examples", List.of(Map.of("status", "ok")));
+    }
+}
diff --git a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPConversationRunner.java b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPConversationRunner.java
index 3a04401696c..7ffd5dce9b7 100644
--- a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPConversationRunner.java
+++ b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPConversationRunner.java
@@ -26,7 +26,6 @@ import org.apache.shardingsphere.test.e2e.mcp.llm.conversation.client.LLMChatMod
 import org.apache.shardingsphere.test.e2e.mcp.llm.conversation.client.LLMToolCall;
 import org.apache.shardingsphere.test.e2e.mcp.llm.scenario.LLME2EScenario;
 import org.apache.shardingsphere.test.e2e.mcp.llm.scenario.LLMStructuredAnswer;
-import org.apache.shardingsphere.test.e2e.mcp.support.transport.MCPInteractionActionNames;
 import org.apache.shardingsphere.test.e2e.mcp.support.transport.MCPInteractionTraceRecord;
 import org.apache.shardingsphere.test.e2e.mcp.support.transport.client.MCPInteractionClient;
 
@@ -58,6 +57,8 @@ public final class LLMMCPConversationRunner {
     
     private final LLMMCPSafetyValidator safetyValidator = new LLMMCPSafetyValidator();
     
+    private final LLMMCPTraceRecordFactory traceRecordFactory = new LLMMCPTraceRecordFactory();
+    
     private final String modelProvider;
     
     private final String modelName;
@@ -189,9 +190,10 @@ public final class LLMMCPConversationRunner {
         }
         messages.add(LLMChatMessage.assistant(completion.getContent(), completion.getToolCalls()));
         for (LLMToolCall each : completion.getToolCalls()) {
-            List<String> availableToolNames = instructionFactory.hasSideEffectExecutionNextAction(artifacts.getInteractionTrace())
+            List<String> availableToolNames = turnPlanner.createImmediateNextActionToolNames(artifacts.getInteractionTrace());
+            availableToolNames = availableToolNames.isEmpty() && instructionFactory.hasSideEffectExecutionNextAction(artifacts.getInteractionTrace())
                     ? turnPlanner.createTurnToolNames(scenario, artifacts.getInteractionTrace())
-                    : List.of();
+                    : availableToolNames;
             if (!availableToolNames.isEmpty() && !availableToolNames.contains(each.getName())) {
                 Map<String, Object> toolResponse = new LinkedHashMap<>(4, 1F);
                 toolResponse.put("response_mode", "tool_call_rejected");
@@ -212,7 +214,7 @@ public final class LLMMCPConversationRunner {
                         each.getName(), String.join(", ", availableToolNames), expectedQueryInstruction)));
                 return Optional.empty();
             }
-            Optional<LLME2EArtifactBundle> result = processToolCall(scenario, each, messages, artifacts);
+            Optional<LLME2EArtifactBundle> result = processToolCall(scenario, each, messages, artifacts, availableToolNames);
             if (result.isPresent()) {
                 return result;
             }
@@ -225,8 +227,8 @@ public final class LLMMCPConversationRunner {
     }
     
     private Optional<LLME2EArtifactBundle> processToolCall(final LLME2EScenario scenario, final LLMToolCall toolCall, final List<LLMChatMessage> messages,
-                                                           final LLMMCPConversationArtifacts artifacts) throws InterruptedException {
-        if (!scenario.getAllowedToolNames().contains(toolCall.getName())) {
+                                                           final LLMMCPConversationArtifacts artifacts, final List<String> extraAllowedToolNames) throws InterruptedException {
+        if (!scenario.getAllowedToolNames().contains(toolCall.getName()) && !extraAllowedToolNames.contains(toolCall.getName())) {
             artifacts.addInteractionTrace(MCPInteractionTraceRecord.createInvalidAction(artifacts.nextSequence(), "tool_call", toolCall.getName(),
                     Map.of("rawArgumentsJson", toolCall.getArgumentsJson()), "unexpected_tool_requested"));
             return Optional.of(createFailureBundle(scenario, artifacts, "unexpected_tool_requested", "Model requested an unsupported tool."));
@@ -260,7 +262,7 @@ public final class LLMMCPConversationRunner {
         artifacts.addRuntimeLogLine("action=" + toolName + " args=" + JsonUtils.toJsonString(arguments));
         artifacts.addRuntimeLogLine("response=" + JsonUtils.toJsonString(response));
         String actionOrigin = MCPInteractionTraceRecord.MODEL_TOOL_CALL_ORIGIN;
-        artifacts.addInteractionTrace(createTraceRecord(artifacts.nextSequence(), toolName, actionOrigin, arguments, response, latencyMillis));
+        artifacts.addInteractionTrace(traceRecordFactory.createTraceRecord(artifacts.nextSequence(), toolName, actionOrigin, arguments, response, latencyMillis));
         messages.add(LLMChatMessage.tool(toolCall.getId(), LLMMCPModelFacingToolResponseFormatter.format(response)));
         return Optional.empty();
     }
@@ -306,31 +308,4 @@ public final class LLMMCPConversationRunner {
         }
     }
     
-    private MCPInteractionTraceRecord createTraceRecord(final int sequence, final String actionName, final String actionOrigin, final Map<String, Object> arguments,
-                                                        final Map<String, Object> structuredContent, final long latencyMillis) {
-        String bridgeActionOrigin = MCPInteractionTraceRecord.PROTOCOL_BRIDGE_ORIGIN;
-        if (MCPInteractionActionNames.LIST_RESOURCES.equals(actionName)) {
-            return new MCPInteractionTraceRecord(sequence, MCPInteractionActionNames.RESOURCE_LIST_KIND, bridgeActionOrigin, MCPInteractionActionNames.LIST_RESOURCES,
-                    Map.of(), structuredContent, true, latencyMillis);
-        }
-        if (MCPInteractionActionNames.READ_RESOURCE.equals(actionName)) {
-            return new MCPInteractionTraceRecord(sequence, MCPInteractionActionNames.RESOURCE_READ_KIND, bridgeActionOrigin, MCPInteractionActionNames.READ_RESOURCE,
-                    Map.of("uri", Objects.toString(arguments.get("uri"), "").trim()), structuredContent, true, latencyMillis);
-        }
-        if (MCPInteractionActionNames.LIST_PROMPTS.equals(actionName)) {
-            return new MCPInteractionTraceRecord(sequence, MCPInteractionActionNames.PROMPT_LIST_KIND, bridgeActionOrigin, MCPInteractionActionNames.LIST_PROMPTS,
-                    Map.of(), structuredContent, true, latencyMillis);
-        }
-        if (MCPInteractionActionNames.GET_PROMPT.equals(actionName)) {
-            return new MCPInteractionTraceRecord(sequence, MCPInteractionActionNames.PROMPT_GET_KIND, bridgeActionOrigin, MCPInteractionActionNames.GET_PROMPT,
-                    Map.of("name", Objects.toString(arguments.get("name"), "").trim(), "arguments", LLMMCPJsonValues.castToMap(arguments.getOrDefault("arguments", Map.of()))),
-                    structuredContent, true, latencyMillis);
-        }
-        if (MCPInteractionActionNames.COMPLETE.equals(actionName)) {
-            return new MCPInteractionTraceRecord(sequence, MCPInteractionActionNames.COMPLETION_KIND, bridgeActionOrigin, MCPInteractionActionNames.COMPLETE,
-                    arguments, structuredContent, true, latencyMillis);
-        }
-        return new MCPInteractionTraceRecord(sequence, "tool_call", actionOrigin, actionName, arguments, structuredContent, true, latencyMillis);
-    }
-    
 }
diff --git a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPConversationRunnerNextActionTest.java b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPConversationRunnerNextActionTest.java
index aacc7594f03..4e9a6a4c8a2 100644
--- a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPConversationRunnerNextActionTest.java
+++ b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPConversationRunnerNextActionTest.java
@@ -373,6 +373,36 @@ class LLMMCPConversationRunnerNextActionTest extends AbstractLLMMCPConversationR
         verify(getMCPInteractionClient(), never()).call("database_gateway_execute_update", misroutedQueryArguments);
     }
     
+    @Test
+    void assertRunFollowsImmediateCompletionRecoveryAction() throws IOException, InterruptedException {
+        List<String> toolNames = List.of("database_gateway_plan_mask_rule", "database_gateway_execute_query");
+        LLME2EScenario actualScenario = createScenario(toolNames);
+        LLMMCPConversationRunner actualRunner = createRunner(4);
+        Map<String, Object> planArguments = Map.of("plan_id", "plan_id", "database", DATABASE_NAME, "schema", SCHEMA_NAME, "table", TABLE_NAME);
+        Map<String, Object> reference = Map.of("type", "ref/resource", "uri", "shardingsphere://workflows/{plan_id}");
+        Map<String, Object> argument = Map.of("name", "plan_id", "value", "");
+        Map<String, Object> completionArguments = Map.of("ref", reference, "argument", argument);
+        Map<String, Object> executeQueryArguments = createExecuteQueryArguments(QUERY);
+        when(getLLMChatClient().complete(anyList(), anyList(), eq("required"), eq(false))).thenReturn(
+                createToolCallCompletion("tool-1", "database_gateway_plan_mask_rule", planArguments, "plan-response"),
+                createToolCallCompletion("tool-2", MCPInteractionActionNames.COMPLETE, completionArguments, "completion-response"),
+                createToolCallCompletion("tool-3", "database_gateway_execute_query", executeQueryArguments, "query-response"));
+        when(getMCPInteractionClient().call("database_gateway_plan_mask_rule", planArguments)).thenReturn(Map.of("response_mode", "recovery", "recovery", Map.of(
+                "next_actions", List.of(Map.of("type", "completion", "ref", reference, "argument", argument)))));
+        when(getMCPInteractionClient().complete(reference, "plan_id", "", Map.of())).thenReturn(Map.of("completion", Map.of("values", List.of())));
+        when(getMCPInteractionClient().call("database_gateway_execute_query", executeQueryArguments)).thenReturn(createResultSetPayload(2));
+        when(getLLMChatClient().complete(anyList(), eq(List.of()), eq("none"), eq(true))).thenReturn(
+                createFinalAnswerCompletion(List.of("database_gateway_plan_mask_rule", MCPInteractionActionNames.COMPLETE, "database_gateway_execute_query"), 2, "final-answer-response"));
+        
+        LLME2EArtifactBundle actual = actualRunner.run(actualScenario);
+        
+        assertTrue(actual.getAssertionReport().isSuccess());
+        List<List<Map<String, Object>>> actualTools = captureRequiredToolDefinitions(3);
+        assertThat(getToolNames(actualTools.get(1)), is(List.of(MCPInteractionActionNames.COMPLETE)));
+        assertThat(actual.getInteractionTrace().get(1).getActionKind(), is(MCPInteractionActionNames.COMPLETION_KIND));
+        verify(getMCPInteractionClient()).complete(reference, "plan_id", "", Map.of());
+    }
+    
     @Test
     void assertRunCompactsManualArtifactsBeforeReadOnlyVerification() throws IOException, InterruptedException {
         List<String> toolNames = List.of("database_gateway_apply_workflow", "database_gateway_execute_query");
diff --git a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPConversationTurnPlanner.java b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPConversationTurnPlanner.java
index 0e397ec021b..ef07ee89fe8 100644
--- a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPConversationTurnPlanner.java
+++ b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPConversationTurnPlanner.java
@@ -34,11 +34,9 @@ final class LLMMCPConversationTurnPlanner {
     }
     
     List<String> createTurnToolNames(final LLME2EScenario scenario, final List<MCPInteractionTraceRecord> interactionTrace) {
-        if (!interactionTrace.isEmpty()) {
-            String immediateActionName = instructionFactory.findImmediateNextActionName(interactionTrace.getLast());
-            if (!immediateActionName.isEmpty() && scenario.getAllowedToolNames().contains(immediateActionName)) {
-                return List.of(immediateActionName);
-            }
+        List<String> immediateActionToolNames = createImmediateNextActionToolNames(interactionTrace);
+        if (!immediateActionToolNames.isEmpty()) {
+            return immediateActionToolNames;
         }
         if (instructionFactory.hasSideEffectExecutionNextAction(interactionTrace)) {
             List<String> readOnlyToolNames = findMissingReadOnlyToolNames(scenario, interactionTrace);
@@ -50,6 +48,14 @@ final class LLMMCPConversationTurnPlanner {
         return missingToolNames.isEmpty() ? scenario.getAllowedToolNames() : List.of(missingToolNames.getFirst());
     }
     
+    List<String> createImmediateNextActionToolNames(final List<MCPInteractionTraceRecord> interactionTrace) {
+        if (interactionTrace.isEmpty()) {
+            return List.of();
+        }
+        String immediateActionName = instructionFactory.findImmediateNextActionName(interactionTrace.getLast());
+        return immediateActionName.isEmpty() ? List.of() : List.of(immediateActionName);
+    }
+    
     String createToolChoice(final LLME2EScenario scenario, final List<MCPInteractionTraceRecord> interactionTrace, final boolean finalAnswerRequested) {
         if (finalAnswerRequested) {
             return "none";
diff --git a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPConversationTurnPlannerTest.java b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPConversationTurnPlannerTest.java
index b69c87d947f..c779c0656ad 100644
--- a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPConversationTurnPlannerTest.java
+++ b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPConversationTurnPlannerTest.java
@@ -42,6 +42,17 @@ class LLMMCPConversationTurnPlannerTest {
         assertThat(actual, is(List.of(MCPInteractionActionNames.READ_RESOURCE)));
     }
     
+    @Test
+    void assertCreateTurnToolNamesWithImmediateCompletionAction() {
+        LLMMCPConversationInstructionFactory instructionFactory = new LLMMCPConversationInstructionFactory();
+        LLMMCPConversationTurnPlanner planner = new LLMMCPConversationTurnPlanner(instructionFactory);
+        List<String> actual = planner.createTurnToolNames(createScenario(List.of("database_gateway_plan_mask_rule", "database_gateway_execute_query"),
+                List.of("database_gateway_plan_mask_rule", "database_gateway_execute_query")),
+                List.of(createTraceRecord("database_gateway_plan_mask_rule",
+                        Map.of("recovery", Map.of("next_actions", List.of(Map.of("type", "completion")))))));
+        assertThat(actual, is(List.of(MCPInteractionActionNames.COMPLETE)));
+    }
+    
     @Test
     void assertCreateTurnToolNamesPrefersReadOnlyAfterSideEffectNextAction() {
         LLMMCPConversationInstructionFactory instructionFactory = new LLMMCPConversationInstructionFactory();
diff --git a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPTraceRecordFactory.java b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPTraceRecordFactory.java
new file mode 100644
index 00000000000..1c168ff70bd
--- /dev/null
+++ b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPTraceRecordFactory.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.test.e2e.mcp.llm.conversation;
+
+import org.apache.shardingsphere.test.e2e.mcp.support.transport.MCPInteractionActionNames;
+import org.apache.shardingsphere.test.e2e.mcp.support.transport.MCPInteractionTraceRecord;
+
+import java.util.Map;
+import java.util.Objects;
+
+final class LLMMCPTraceRecordFactory {
+    
+    MCPInteractionTraceRecord createTraceRecord(final int sequence, final String actionName, final String actionOrigin, final Map<String, Object> args,
+                                                final Map<String, Object> structuredContent, final long latencyMillis) {
+        String bridgeActionOrigin = MCPInteractionTraceRecord.PROTOCOL_BRIDGE_ORIGIN;
+        if (MCPInteractionActionNames.LIST_RESOURCES.equals(actionName)) {
+            return new MCPInteractionTraceRecord(sequence, MCPInteractionActionNames.RESOURCE_LIST_KIND, bridgeActionOrigin, MCPInteractionActionNames.LIST_RESOURCES,
+                    Map.of(), structuredContent, true, latencyMillis);
+        }
+        if (MCPInteractionActionNames.READ_RESOURCE.equals(actionName)) {
+            return new MCPInteractionTraceRecord(sequence, MCPInteractionActionNames.RESOURCE_READ_KIND, bridgeActionOrigin, MCPInteractionActionNames.READ_RESOURCE,
+                    Map.of("uri", Objects.toString(args.get("uri"), "").trim()), structuredContent, true, latencyMillis);
+        }
+        if (MCPInteractionActionNames.LIST_PROMPTS.equals(actionName)) {
+            return new MCPInteractionTraceRecord(sequence, MCPInteractionActionNames.PROMPT_LIST_KIND, bridgeActionOrigin, MCPInteractionActionNames.LIST_PROMPTS,
+                    Map.of(), structuredContent, true, latencyMillis);
+        }
+        if (MCPInteractionActionNames.GET_PROMPT.equals(actionName)) {
+            return new MCPInteractionTraceRecord(sequence, MCPInteractionActionNames.PROMPT_GET_KIND, bridgeActionOrigin, MCPInteractionActionNames.GET_PROMPT,
+                    Map.of("name", Objects.toString(args.get("name"), "").trim(), "arguments", LLMMCPJsonValues.castToMap(args.getOrDefault("arguments", Map.of()))),
+                    structuredContent, true, latencyMillis);
+        }
+        if (MCPInteractionActionNames.COMPLETE.equals(actionName)) {
+            return new MCPInteractionTraceRecord(sequence, MCPInteractionActionNames.COMPLETION_KIND, bridgeActionOrigin, MCPInteractionActionNames.COMPLETE,
+                    args, structuredContent, true, latencyMillis);
+        }
+        return new MCPInteractionTraceRecord(sequence, "tool_call", actionOrigin, actionName, args, structuredContent, true, latencyMillis);
+    }
+}
diff --git a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/client/LLMChatModelClient.java b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/client/LLMChatModelClient.java
index 6a3fd6bd7e7..901edd68d57 100644
--- a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/client/LLMChatModelClient.java
+++ b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/client/LLMChatModelClient.java
@@ -22,8 +22,6 @@ import com.google.common.base.Preconditions;
 import lombok.RequiredArgsConstructor;
 import org.apache.shardingsphere.infra.util.json.JsonUtils;
 import org.apache.shardingsphere.test.e2e.mcp.llm.config.LLME2EConfiguration;
-import org.apache.shardingsphere.test.e2e.mcp.support.runtime.ReadinessProbe;
-import org.apache.shardingsphere.test.e2e.mcp.support.runtime.ReadinessProbe.ReadinessResult;
 
 import java.io.IOException;
 import java.net.URI;
@@ -44,10 +42,6 @@ import java.util.stream.Collectors;
 @RequiredArgsConstructor
 public final class LLMChatModelClient {
     
-    private static final long INITIAL_READINESS_INTERVAL_MILLIS = 250L;
-    
-    private static final long MAX_READINESS_INTERVAL_MILLIS = 2000L;
-    
     private static final int COMPLETION_MAX_TOKENS = 512;
     
     private static final int READINESS_MAX_TOKENS = 64;
@@ -63,88 +57,10 @@ public final class LLMChatModelClient {
      * @throws IllegalStateException model service is not ready
      */
     public void waitUntilReady() throws InterruptedException {
-        new ReadinessProbe(config.getReadyTimeoutSeconds() * 1000L, INITIAL_READINESS_INTERVAL_MILLIS, MAX_READINESS_INTERVAL_MILLIS)
-                .waitUntilReady(this::checkReadiness, this::createReadinessException);
-    }
-    
-    private ReadinessResult<Boolean> checkReadiness() throws InterruptedException {
-        try {
-            return isReadinessContractReady() ? ReadinessResult.ready(Boolean.TRUE) : ReadinessResult.retry(null);
-        } catch (final IOException ex) {
-            return ReadinessResult.retry(ex);
-        } catch (final IllegalStateException ex) {
-            return isNonRetryableReadinessFailure(ex) ? ReadinessResult.failed(ex) : ReadinessResult.retry(ex);
-        }
-    }
-    
-    private boolean isNonRetryableReadinessFailure(final Exception cause) {
-        String message = Objects.toString(cause.getMessage(), "");
-        return message.contains("HTTP 400") || message.contains("HTTP 401");
-    }
-    
-    private IllegalStateException createReadinessException(final Exception cause, final int attemptCount, final long elapsedMillis) {
-        IllegalStateException result = new IllegalStateException(createReadinessFailureMessage(cause, attemptCount, elapsedMillis));
-        if (null != cause) {
-            result.initCause(cause);
-        }
-        return result;
-    }
-    
-    private String createReadinessFailureMessage(final Exception cause, final int attemptCount, final long elapsedMillis) {
-        String result = String.format("Model service is not ready for `%s` after %d readiness attempt(s), elapsedMillis=%d, timeoutSeconds=%d.",
-                config.getModelName(), attemptCount, elapsedMillis, config.getReadyTimeoutSeconds());
-        return null == cause || null == cause.getMessage() || cause.getMessage().isBlank()
-                ? result
-                : result + " Last readiness failure: " + cause.getMessage();
-    }
-    
-    private boolean isModelListReady() throws IOException, InterruptedException {
-        HttpResponse<String> response = sendModelListRequest();
-        if (200 != response.statusCode()) {
-            throw new IllegalStateException(createHttpReadinessFailure("model-list", response));
-        }
-        return containsModel(response.body());
-    }
-    
-    private boolean isReadinessContractReady() throws IOException, InterruptedException {
-        return isModelListReady() && isCompletionProbeReady() && isRequiredToolProbeReady() && isAutoToolProbeReady() && isFinalAnswerProbeReady();
-    }
-    
-    private boolean isCompletionProbeReady() throws IOException, InterruptedException {
-        HttpResponse<String> response = sendReadinessCompletionRequest(List.of(LLMChatMessage.user("Return ok.")), List.of(), "", false);
-        if (200 != response.statusCode()) {
-            throw new IllegalStateException(createHttpReadinessFailure("completion", response));
-        }
-        return hasCompletionChoice(response.body());
-    }
-    
-    private boolean isRequiredToolProbeReady() throws IOException, InterruptedException {
-        HttpResponse<String> response = sendReadinessCompletionRequest(List.of(LLMChatMessage.user("Call mcp_read_resource with uri mcp://readiness.")),
-                createReadinessTools(), "required", false);
-        if (200 != response.statusCode()) {
-            throw new IllegalStateException(createHttpReadinessFailure("tool-choice-required", response));
-        }
-        return hasToolCallChoice(response.body());
-    }
-    
-    private boolean isAutoToolProbeReady() throws IOException, InterruptedException {
-        HttpResponse<String> response = sendReadinessCompletionRequest(List.of(LLMChatMessage.user("Return ok without calling tools.")), createReadinessTools(), "auto", false);
-        if (200 != response.statusCode()) {
-            throw new IllegalStateException(createHttpReadinessFailure("tool-choice-auto", response));
-        }
-        return hasCompletionChoice(response.body());
-    }
-    
-    private boolean isFinalAnswerProbeReady() throws IOException, InterruptedException {
-        HttpResponse<String> response = sendReadinessCompletionRequest(List.of(LLMChatMessage.user(
-                "Respond with exactly {\"status\":\"ok\"} and no Markdown fences.")), List.of(), "none", true);
-        if (200 != response.statusCode()) {
-            throw new IllegalStateException(createHttpReadinessFailure("tool-choice-none-json", response));
-        }
-        return hasJsonCompletionChoice(response.body());
+        new LLMChatReadinessProbe(config, this).waitUntilReady();
     }
     
-    private String createHttpReadinessFailure(final String requestKind, final HttpResponse<String> response) {
+    String createHttpReadinessFailure(final String requestKind, final HttpResponse<String> response) {
         return String.format("%s readiness request returned HTTP %d%s.", requestKind, response.statusCode(), createErrorCodeSuffix(response.body()));
     }
     
@@ -216,7 +132,7 @@ public final class LLMChatModelClient {
         return httpClient.send(request, HttpResponse.BodyHandlers.ofString());
     }
     
-    private HttpResponse<String> sendModelListRequest() throws IOException, InterruptedException {
+    HttpResponse<String> sendModelListRequest() throws IOException, InterruptedException {
         HttpRequest request = HttpRequest.newBuilder(URI.create(config.getModelsUrl()))
                 .timeout(Duration.ofSeconds(Math.min(config.getRequestTimeoutSeconds(), 30)))
                 .header("Authorization", "Bearer " + config.getApiKey())
@@ -225,25 +141,25 @@ public final class LLMChatModelClient {
         return httpClient.send(request, HttpResponse.BodyHandlers.ofString());
     }
     
-    private HttpResponse<String> sendReadinessCompletionRequest(final List<LLMChatMessage> messages, final List<Map<String, Object>> tools,
-                                                                final String toolChoice, final boolean jsonResponse) throws IOException, InterruptedException {
+    HttpResponse<String> sendReadinessCompletionRequest(final List<LLMChatMessage> messages, final List<Map<String, Object>> tools,
+                                                        final String toolChoice, final boolean jsonResponse) throws IOException, InterruptedException {
         return sendCompletionRequest(createCompletionRequestPayload(messages, tools, toolChoice, jsonResponse, READINESS_MAX_TOKENS), Math.min(config.getRequestTimeoutSeconds(), 30));
     }
     
-    private boolean containsModel(final String responseBody) {
+    boolean containsModel(final String responseBody) {
         Map<String, Object> payload = parseJsonObject(responseBody, "Failed to parse model-list response.");
         return castToList(payload.get("data")).stream().anyMatch(each -> config.getModelName().equals(Objects.toString(each.get("id"), "").trim()));
     }
     
-    private boolean hasCompletionChoice(final String responseBody) {
+    boolean hasCompletionChoice(final String responseBody) {
         return !castToList(parseJsonObject(responseBody, "Failed to parse readiness completion response.").get("choices")).isEmpty();
     }
     
-    private boolean hasToolCallChoice(final String responseBody) {
+    boolean hasToolCallChoice(final String responseBody) {
         return !createToolCalls(getFirstAssistantMessage(responseBody, "Failed to parse readiness tool-call response.").get("tool_calls")).isEmpty();
     }
     
-    private boolean hasJsonCompletionChoice(final String responseBody) {
+    boolean hasJsonCompletionChoice(final String responseBody) {
         String content = Objects.toString(getFirstAssistantMessage(responseBody, "Failed to parse readiness JSON response.").get("content"), "").trim();
         if (content.isEmpty()) {
             return false;
@@ -257,7 +173,7 @@ public final class LLMChatModelClient {
         return choices.isEmpty() ? Map.of() : castToMap(choices.get(0).get("message"));
     }
     
-    private List<Map<String, Object>> createReadinessTools() {
+    List<Map<String, Object>> createReadinessTools() {
         return List.of(Map.of(
                 "type", "function",
                 "function", Map.of(
diff --git a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/client/LLMChatReadinessProbe.java b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/client/LLMChatReadinessProbe.java
new file mode 100644
index 00000000000..66a2e9a7c22
--- /dev/null
+++ b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/client/LLMChatReadinessProbe.java
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.test.e2e.mcp.llm.conversation.client;
+
+import org.apache.shardingsphere.test.e2e.mcp.llm.config.LLME2EConfiguration;
+import org.apache.shardingsphere.test.e2e.mcp.support.runtime.ReadinessProbe;
+import org.apache.shardingsphere.test.e2e.mcp.support.runtime.ReadinessProbe.ReadinessResult;
+
+import java.io.IOException;
+import java.net.http.HttpResponse;
+import java.util.List;
+import java.util.Objects;
+
+final class LLMChatReadinessProbe {
+    
+    private static final long INITIAL_READINESS_INTERVAL_MILLIS = 250L;
+    
+    private static final long MAX_READINESS_INTERVAL_MILLIS = 2000L;
+    
+    private final LLME2EConfiguration config;
+    
+    private final LLMChatModelClient client;
+    
+    LLMChatReadinessProbe(final LLME2EConfiguration config, final LLMChatModelClient client) {
+        this.config = config;
+        this.client = client;
+    }
+    
+    void waitUntilReady() throws InterruptedException {
+        new ReadinessProbe(config.getReadyTimeoutSeconds() * 1000L, INITIAL_READINESS_INTERVAL_MILLIS, MAX_READINESS_INTERVAL_MILLIS)
+                .waitUntilReady(this::checkReadiness, this::createReadinessException);
+    }
+    
+    private ReadinessResult<Boolean> checkReadiness() throws InterruptedException {
+        try {
+            return isReadinessContractReady() ? ReadinessResult.ready(Boolean.TRUE) : ReadinessResult.retry(null);
+        } catch (final IOException ex) {
+            return ReadinessResult.retry(ex);
+        } catch (final IllegalStateException ex) {
+            return isNonRetryableReadinessFailure(ex) ? ReadinessResult.failed(ex) : ReadinessResult.retry(ex);
+        }
+    }
+    
+    private boolean isReadinessContractReady() throws IOException, InterruptedException {
+        return isModelListReady() && isCompletionProbeReady() && isRequiredToolProbeReady() && isAutoToolProbeReady() && isFinalAnswerProbeReady();
+    }
+    
+    private boolean isModelListReady() throws IOException, InterruptedException {
+        HttpResponse<String> response = client.sendModelListRequest();
+        if (200 != response.statusCode()) {
+            throw new IllegalStateException(client.createHttpReadinessFailure("model-list", response));
+        }
+        return client.containsModel(response.body());
+    }
+    
+    private boolean isCompletionProbeReady() throws IOException, InterruptedException {
+        HttpResponse<String> response = client.sendReadinessCompletionRequest(List.of(LLMChatMessage.user("Return ok.")), List.of(), "", false);
+        if (200 != response.statusCode()) {
+            throw new IllegalStateException(client.createHttpReadinessFailure("completion", response));
+        }
+        return client.hasCompletionChoice(response.body());
+    }
+    
+    private boolean isRequiredToolProbeReady() throws IOException, InterruptedException {
+        HttpResponse<String> response = client.sendReadinessCompletionRequest(List.of(LLMChatMessage.user("Call mcp_read_resource with uri mcp://readiness.")),
+                client.createReadinessTools(), "required", false);
+        if (200 != response.statusCode()) {
+            throw new IllegalStateException(client.createHttpReadinessFailure("tool-choice-required", response));
+        }
+        return client.hasToolCallChoice(response.body());
+    }
+    
+    private boolean isAutoToolProbeReady() throws IOException, InterruptedException {
+        HttpResponse<String> response = client.sendReadinessCompletionRequest(List.of(LLMChatMessage.user("Return ok without calling tools.")), client.createReadinessTools(), "auto", false);
+        if (200 != response.statusCode()) {
+            throw new IllegalStateException(client.createHttpReadinessFailure("tool-choice-auto", response));
+        }
+        return client.hasCompletionChoice(response.body());
+    }
+    
+    private boolean isFinalAnswerProbeReady() throws IOException, InterruptedException {
+        HttpResponse<String> response = client.sendReadinessCompletionRequest(List.of(LLMChatMessage.user(
+                "Respond with exactly {\"status\":\"ok\"} and no Markdown fences.")), List.of(), "none", true);
+        if (200 != response.statusCode()) {
+            throw new IllegalStateException(client.createHttpReadinessFailure("tool-choice-none-json", response));
+        }
+        return client.hasJsonCompletionChoice(response.body());
+    }
+    
+    private boolean isNonRetryableReadinessFailure(final Exception cause) {
+        String message = Objects.toString(cause.getMessage(), "");
+        return message.contains("HTTP 400") || message.contains("HTTP 401");
+    }
+    
+    private IllegalStateException createReadinessException(final Exception cause, final int attemptCount, final long elapsedMillis) {
+        IllegalStateException result = new IllegalStateException(createReadinessFailureMessage(cause, attemptCount, elapsedMillis));
+        if (null != cause) {
+            result.initCause(cause);
+        }
+        return result;
+    }
+    
+    private String createReadinessFailureMessage(final Exception cause, final int attemptCount, final long elapsedMillis) {
+        String result = String.format("Model service is not ready for `%s` after %d readiness attempt(s), elapsedMillis=%d, timeoutSeconds=%d.",
+                config.getModelName(), attemptCount, elapsedMillis, config.getReadyTimeoutSeconds());
+        return null == cause || null == cause.getMessage() || cause.getMessage().isBlank()
+                ? result
+                : result + " Last readiness failure: " + cause.getMessage();
+    }
+}
diff --git a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/AbstractProductionMySQLRuntimeE2ETest.java b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/AbstractProductionMySQLRuntimeE2ETest.java
index ace56833999..4956c78d63d 100644
--- a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/AbstractProductionMySQLRuntimeE2ETest.java
+++ b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/AbstractProductionMySQLRuntimeE2ETest.java
@@ -17,18 +17,10 @@
 
 package org.apache.shardingsphere.test.e2e.mcp.runtime.production;
 
-import io.modelcontextprotocol.client.McpClient;
 import io.modelcontextprotocol.client.McpSyncClient;
-import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
-import io.modelcontextprotocol.client.transport.ServerParameters;
-import io.modelcontextprotocol.client.transport.StdioClientTransport;
 import io.modelcontextprotocol.spec.McpClientTransport;
 import io.modelcontextprotocol.spec.McpSchema;
-import io.modelcontextprotocol.spec.ProtocolVersions;
-import org.apache.shardingsphere.mcp.bootstrap.MCPBootstrap;
-import org.apache.shardingsphere.mcp.bootstrap.transport.MCPTransportJsonMapperFactory;
 import org.apache.shardingsphere.mcp.support.database.metadata.jdbc.RuntimeDatabaseConfiguration;
-import org.apache.shardingsphere.mcp.support.descriptor.MCPShardingSphereMetadataKeys;
 import org.apache.shardingsphere.test.e2e.mcp.env.MCPE2ECondition;
 import org.apache.shardingsphere.test.e2e.mcp.support.OfficialMCPToolNames;
 import org.apache.shardingsphere.test.e2e.mcp.support.runtime.MySQLRuntimeTestSupport;
@@ -36,7 +28,6 @@ import org.apache.shardingsphere.test.e2e.mcp.support.runtime.RuntimeTransport;
 import org.apache.shardingsphere.test.e2e.mcp.support.transport.MCPInteractionPayloads;
 import org.apache.shardingsphere.test.e2e.mcp.support.transport.MCPPayloadAssertions;
 import org.apache.shardingsphere.test.e2e.mcp.support.transport.client.MCPInteractionClient;
-import org.apache.shardingsphere.test.e2e.mcp.support.transport.client.MCPStdioLogbackConfiguration;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Assumptions;
@@ -46,18 +37,13 @@ import org.junit.jupiter.params.provider.Arguments;
 import org.testcontainers.containers.GenericContainer;
 
 import java.io.IOException;
-import java.net.URI;
-import java.nio.file.Path;
-import java.nio.file.Paths;
 import java.sql.SQLException;
-import java.time.Duration;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Stream;
 
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.containsInAnyOrder;
-import static org.hamcrest.Matchers.hasItems;
 import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.isA;
 import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -73,8 +59,6 @@ abstract class AbstractProductionMySQLRuntimeE2ETest extends AbstractTransportPa
     
     protected static final String MASK_PLAN_TOOL_NAME = "database_gateway_plan_mask_rule";
     
-    protected static final String STDIO_LOGBACK_CONFIG_FILE_NAME = "mcp-e2e-sdk-stdio-logback.xml";
-    
     private GenericContainer<?> container;
     
     private String physicalSchemaName;
@@ -174,7 +158,7 @@ abstract class AbstractProductionMySQLRuntimeE2ETest extends AbstractTransportPa
     }
     
     protected static Stream<Arguments> transports() {
-        return runtimeTransports().map(each -> Arguments.of(getTransportName(each), each));
+        return ProductionRuntimeTransportCases.transports();
     }
     
     protected static Stream<Arguments> dualTransports() {
@@ -182,43 +166,15 @@ abstract class AbstractProductionMySQLRuntimeE2ETest extends AbstractTransportPa
     }
     
     protected static Stream<Arguments> semanticPrimaryTransport() {
-        return semanticPrimaryRuntimeTransports().map(each -> Arguments.of(getTransportName(each), each));
+        return ProductionRuntimeTransportCases.semanticPrimaryTransport();
     }
     
     protected static Stream<Arguments> assertReadSingleMetadataResourceCases() {
-        return semanticPrimaryRuntimeTransports().flatMap(each -> Stream.of(
-                Arguments.of(getTransportName(each) + " database detail", each, "shardingsphere://databases/logic_db", "database", LOGICAL_DATABASE_NAME),
-                Arguments.of(getTransportName(each) + " schema detail", each, "shardingsphere://databases/logic_db/schemas/logic_db", "schema", LOGICAL_DATABASE_NAME),
-                Arguments.of(getTransportName(each) + " table column detail", each,
-                        "shardingsphere://databases/logic_db/schemas/logic_db/tables/orders/columns/status", "column", "status"),
-                Arguments.of(getTransportName(each) + " view detail", each, "shardingsphere://databases/logic_db/schemas/logic_db/views/active_orders", "view", "active_orders"),
-                Arguments.of(getTransportName(each) + " view column detail", each,
-                        "shardingsphere://databases/logic_db/schemas/logic_db/views/active_orders/columns/status", "column", "status"),
-                Arguments.of(getTransportName(each) + " index detail", each,
-                        "shardingsphere://databases/logic_db/schemas/logic_db/tables/orders/indexes/idx_orders_status", "index", "idx_orders_status")));
+        return ProductionRuntimeTransportCases.assertReadSingleMetadataResourceCases(LOGICAL_DATABASE_NAME);
     }
     
     protected static Stream<Arguments> assertReadCollectionMetadataResourceCases() {
-        return semanticPrimaryRuntimeTransports().flatMap(each -> Stream.of(
-                Arguments.of(getTransportName(each) + " schemas list", each, "shardingsphere://databases/logic_db/schemas", "schema", List.of(LOGICAL_DATABASE_NAME)),
-                Arguments.of(getTransportName(each) + " tables list", each,
-                        "shardingsphere://databases/logic_db/schemas/logic_db/tables", "table", List.of("order_items", "orders")),
-                Arguments.of(getTransportName(each) + " table columns list", each,
-                        "shardingsphere://databases/logic_db/schemas/logic_db/tables/orders/columns", "column", List.of("amount", "order_id", "status")),
-                Arguments.of(getTransportName(each) + " view columns list", each,
-                        "shardingsphere://databases/logic_db/schemas/logic_db/views/active_orders/columns", "column", List.of("order_id", "status"))));
-    }
-    
-    protected static Stream<RuntimeTransport> runtimeTransports() {
-        return Stream.of(RuntimeTransport.HTTP, RuntimeTransport.STDIO);
-    }
-    
-    private static Stream<RuntimeTransport> semanticPrimaryRuntimeTransports() {
-        return Stream.of(RuntimeTransport.HTTP);
-    }
-    
-    protected static String getTransportName(final RuntimeTransport transport) {
-        return RuntimeTransport.HTTP == transport ? "http" : "stdio";
+        return ProductionRuntimeTransportCases.assertReadCollectionMetadataResourceCases(LOGICAL_DATABASE_NAME);
     }
     
     protected Map<String, RuntimeDatabaseConfiguration> createPreparedProgrammaticRuntimeDatabases() throws IOException {
@@ -338,33 +294,13 @@ abstract class AbstractProductionMySQLRuntimeE2ETest extends AbstractTransportPa
     }
     
     protected McpSyncClient createElicitationClient(final RuntimeTransport transport, final List<McpSchema.ElicitRequest> elicitationRequests) throws IOException {
-        return McpClient.sync(createClientTransport(transport))
-                .clientInfo(new McpSchema.Implementation("mcp-e2e-elicitation", "MCP E2E Elicitation", "1.0.0"))
-                .capabilities(McpSchema.ClientCapabilities.builder().elicitation().build())
-                .requestTimeout(Duration.ofSeconds(30L))
-                .initializationTimeout(Duration.ofSeconds(30L))
-                .elicitation(request -> createElicitationResult(elicitationRequests, request))
-                .build();
+        return ProductionMCPClientTransportFactory.createElicitationClient(createClientTransport(transport), elicitationRequests, this::createElicitationResult);
     }
     
-    protected McpClientTransport createClientTransport(final RuntimeTransport transport) throws IOException {
+    private McpClientTransport createClientTransport(final RuntimeTransport transport) throws IOException {
         return RuntimeTransport.HTTP == transport
-                ? createHttpClientTransport()
-                : createStdioClientTransport();
-    }
-    
-    protected McpClientTransport createHttpClientTransport() throws IOException {
-        URI endpointUri = getHttpEndpointUri();
-        return HttpClientStreamableHttpTransport.builder(String.format("%s://%s:%d", endpointUri.getScheme(), endpointUri.getHost(), endpointUri.getPort()))
-                .endpoint(endpointUri.getPath()).build();
-    }
-    
-    protected StdioClientTransport createStdioClientTransport() throws IOException {
-        Path configFile = getConfigFile();
-        return new ProtocolAwareStdioClientTransport(ServerParameters.builder(Paths.get(System.getProperty("java.home"), "bin", "java").toString())
-                .args("-Dlogback.configurationFile=" + MCPStdioLogbackConfiguration.createForConfig(configFile, STDIO_LOGBACK_CONFIG_FILE_NAME),
-                        "-cp", System.getProperty("java.class.path"), MCPBootstrap.class.getName(), configFile.toString())
-                .build());
+                ? ProductionMCPClientTransportFactory.createHttpClientTransport(getHttpEndpointUri())
+                : ProductionMCPClientTransportFactory.createStdioClientTransport(getConfigFile());
     }
     
     protected McpSchema.ElicitResult createElicitationResult(final List<McpSchema.ElicitRequest> elicitationRequests,
@@ -377,20 +313,7 @@ abstract class AbstractProductionMySQLRuntimeE2ETest extends AbstractTransportPa
     }
     
     protected void assertElicitationRequest(final List<McpSchema.ElicitRequest> actualRequests) {
-        assertThat(actualRequests.size(), is(1));
-        McpSchema.ElicitRequest actual = actualRequests.getFirst();
-        assertThat(actual.meta().get(MCPShardingSphereMetadataKeys.TOOL), is(MASK_PLAN_TOOL_NAME));
-        assertFalse(String.valueOf(actual.meta().get(MCPShardingSphereMetadataKeys.PLAN_ID)).isBlank());
-        Map<String, Object> actualRequestedSchema = actual.requestedSchema();
-        assertThat(actualRequestedSchema.get("type"), is("object"));
-        assertFalse((Boolean) actualRequestedSchema.get("additionalProperties"));
-        Map<String, Object> actualProperties = castToMap(actualRequestedSchema.get("properties"));
-        assertTrue(actualProperties.containsKey("field_1"));
-        assertTrue(actualProperties.containsKey("field_2"));
-        assertThat(String.valueOf(castToMap(actualProperties.get("field_1")).get("description")), is("Please provide property `from-x`."));
-        assertThat(String.valueOf(castToMap(actualProperties.get("field_2")).get("description")), is("Please provide property `to-y`."));
-        assertFalse(actualProperties.keySet().stream().map(String::valueOf).anyMatch(each -> each.contains("secret") || each.contains("password") || each.contains("token")));
-        assertThat(getStringList(actualRequestedSchema.get("required")), hasItems("field_1", "field_2"));
+        ProductionMCPClientTransportFactory.assertElicitationRequest(actualRequests);
     }
     
     @SuppressWarnings("unchecked")
@@ -415,18 +338,6 @@ abstract class AbstractProductionMySQLRuntimeE2ETest extends AbstractTransportPa
         return ((List<?>) value).stream().map(String::valueOf).toList();
     }
     
-    protected static final class ProtocolAwareStdioClientTransport extends StdioClientTransport {
-        
-        protected ProtocolAwareStdioClientTransport(final ServerParameters params) {
-            super(params, MCPTransportJsonMapperFactory.create());
-        }
-        
-        @Override
-        public List<String> protocolVersions() {
-            return List.of(ProtocolVersions.MCP_2025_06_18, ProtocolVersions.MCP_2025_11_25);
-        }
-    }
-    
     private static final class MySQLRuntimeFixture implements AutoCloseable {
         
         private final GenericContainer<?> container;
diff --git a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/ProductionMCPClientTransportFactory.java b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/ProductionMCPClientTransportFactory.java
new file mode 100644
index 00000000000..3a55210f428
--- /dev/null
+++ b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/ProductionMCPClientTransportFactory.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.test.e2e.mcp.runtime.production;
+
+import io.modelcontextprotocol.client.McpClient;
+import io.modelcontextprotocol.client.McpSyncClient;
+import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
+import io.modelcontextprotocol.client.transport.ServerParameters;
+import io.modelcontextprotocol.client.transport.StdioClientTransport;
+import io.modelcontextprotocol.spec.McpClientTransport;
+import io.modelcontextprotocol.spec.McpSchema;
+import io.modelcontextprotocol.spec.ProtocolVersions;
+import org.apache.shardingsphere.mcp.bootstrap.MCPBootstrap;
+import org.apache.shardingsphere.mcp.bootstrap.transport.MCPTransportJsonMapperFactory;
+import org.apache.shardingsphere.mcp.support.descriptor.MCPShardingSphereMetadataKeys;
+import org.apache.shardingsphere.test.e2e.mcp.support.transport.client.MCPStdioLogbackConfiguration;
+
+import java.io.IOException;
+import java.net.URI;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiFunction;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.hasItems;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+final class ProductionMCPClientTransportFactory {
+    
+    private static final String STDIO_LOGBACK_CONFIG_FILE_NAME = "mcp-e2e-sdk-stdio-logback.xml";
+    
+    private static final String MASK_PLAN_TOOL_NAME = "database_gateway_plan_mask_rule";
+    
+    private ProductionMCPClientTransportFactory() {
+    }
+    
+    static McpSyncClient createElicitationClient(final McpClientTransport clientTransport, final List<McpSchema.ElicitRequest> elicitationRequests,
+                                                 final BiFunction<List<McpSchema.ElicitRequest>, McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler) {
+        return McpClient.sync(clientTransport)
+                .clientInfo(new McpSchema.Implementation("mcp-e2e-elicitation", "MCP E2E Elicitation", "1.0.0"))
+                .capabilities(McpSchema.ClientCapabilities.builder().elicitation().build())
+                .requestTimeout(Duration.ofSeconds(30L))
+                .initializationTimeout(Duration.ofSeconds(30L))
+                .elicitation(request -> elicitationHandler.apply(elicitationRequests, request))
+                .build();
+    }
+    
+    static McpClientTransport createHttpClientTransport(final URI endpointUri) {
+        return HttpClientStreamableHttpTransport.builder(String.format("%s://%s:%d", endpointUri.getScheme(), endpointUri.getHost(), endpointUri.getPort()))
+                .endpoint(endpointUri.getPath()).build();
+    }
+    
+    static StdioClientTransport createStdioClientTransport(final Path configFile) throws IOException {
+        return new ProtocolAwareStdioClientTransport(ServerParameters.builder(Paths.get(System.getProperty("java.home"), "bin", "java").toString())
+                .args("-Dlogback.configurationFile=" + MCPStdioLogbackConfiguration.createForConfig(configFile, STDIO_LOGBACK_CONFIG_FILE_NAME),
+                        "-cp", System.getProperty("java.class.path"), MCPBootstrap.class.getName(), configFile.toString())
+                .build());
+    }
+    
+    static void assertElicitationRequest(final List<McpSchema.ElicitRequest> actualRequests) {
+        assertThat(actualRequests.size(), is(1));
+        McpSchema.ElicitRequest actual = actualRequests.getFirst();
+        assertThat(actual.meta().get(MCPShardingSphereMetadataKeys.TOOL), is(MASK_PLAN_TOOL_NAME));
+        assertFalse(String.valueOf(actual.meta().get(MCPShardingSphereMetadataKeys.PLAN_ID)).isBlank());
+        Map<String, Object> actualRequestedSchema = actual.requestedSchema();
+        assertThat(actualRequestedSchema.get("type"), is("object"));
+        assertFalse((Boolean) actualRequestedSchema.get("additionalProperties"));
+        Map<String, Object> actualProperties = castToMap(actualRequestedSchema.get("properties"));
+        assertTrue(actualProperties.containsKey("field_1"));
+        assertTrue(actualProperties.containsKey("field_2"));
+        assertThat(String.valueOf(castToMap(actualProperties.get("field_1")).get("description")), is("Please provide property `from-x`."));
+        assertThat(String.valueOf(castToMap(actualProperties.get("field_2")).get("description")), is("Please provide property `to-y`."));
+        assertFalse(actualProperties.keySet().stream().map(String::valueOf).anyMatch(each -> each.contains("secret") || each.contains("password") || each.contains("token")));
+        assertThat(getStringList(actualRequestedSchema.get("required")), hasItems("field_1", "field_2"));
+    }
+    
+    @SuppressWarnings("unchecked")
+    private static Map<String, Object> castToMap(final Object value) {
+        return (Map<String, Object>) value;
+    }
+    
+    private static List<String> getStringList(final Object value) {
+        return ((List<?>) value).stream().map(String::valueOf).toList();
+    }
+    
+    private static final class ProtocolAwareStdioClientTransport extends StdioClientTransport {
+        
+        private ProtocolAwareStdioClientTransport(final ServerParameters params) {
+            super(params, MCPTransportJsonMapperFactory.create());
+        }
+        
+        @Override
+        public List<String> protocolVersions() {
+            return List.of(ProtocolVersions.MCP_2025_06_18, ProtocolVersions.MCP_2025_11_25);
+        }
+    }
+}
diff --git a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/ProductionRuntimeTransportCases.java b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/ProductionRuntimeTransportCases.java
new file mode 100644
index 00000000000..aa145d11d30
--- /dev/null
+++ b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/ProductionRuntimeTransportCases.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.test.e2e.mcp.runtime.production;
+
+import org.apache.shardingsphere.test.e2e.mcp.support.runtime.RuntimeTransport;
+import org.junit.jupiter.params.provider.Arguments;
+
+import java.util.List;
+import java.util.stream.Stream;
+
+final class ProductionRuntimeTransportCases {
+    
+    private ProductionRuntimeTransportCases() {
+    }
+    
+    static Stream<Arguments> transports() {
+        return runtimeTransports().map(each -> Arguments.of(getTransportName(each), each));
+    }
+    
+    static Stream<Arguments> semanticPrimaryTransport() {
+        return semanticPrimaryRuntimeTransports().map(each -> Arguments.of(getTransportName(each), each));
+    }
+    
+    static Stream<Arguments> assertReadSingleMetadataResourceCases(final String logicalDatabaseName) {
+        return semanticPrimaryRuntimeTransports().flatMap(each -> createSingleMetadataResourceCases(logicalDatabaseName, each));
+    }
+    
+    static Stream<Arguments> assertReadCollectionMetadataResourceCases(final String logicalDatabaseName) {
+        return semanticPrimaryRuntimeTransports().flatMap(each -> Stream.of(
+                Arguments.of(getTransportName(each) + " schemas list", each, "shardingsphere://databases/" + logicalDatabaseName + "/schemas", "schema", List.of(logicalDatabaseName)),
+                Arguments.of(getTransportName(each) + " tables list", each,
+                        "shardingsphere://databases/" + logicalDatabaseName + "/schemas/" + logicalDatabaseName + "/tables", "table", List.of("order_items", "orders")),
+                Arguments.of(getTransportName(each) + " table columns list", each,
+                        "shardingsphere://databases/" + logicalDatabaseName + "/schemas/" + logicalDatabaseName + "/tables/orders/columns", "column", List.of("amount", "order_id", "status")),
+                Arguments.of(getTransportName(each) + " view columns list", each,
+                        "shardingsphere://databases/" + logicalDatabaseName + "/schemas/" + logicalDatabaseName + "/views/active_orders/columns", "column", List.of("order_id", "status"))));
+    }
+    
+    static Stream<RuntimeTransport> runtimeTransports() {
+        return Stream.of(RuntimeTransport.HTTP, RuntimeTransport.STDIO);
+    }
+    
+    static String getTransportName(final RuntimeTransport transport) {
+        return RuntimeTransport.HTTP == transport ? "http" : "stdio";
+    }
+    
+    private static Stream<RuntimeTransport> semanticPrimaryRuntimeTransports() {
+        return Stream.of(RuntimeTransport.HTTP);
+    }
+    
+    private static Stream<Arguments> createSingleMetadataResourceCases(final String logicalDatabaseName, final RuntimeTransport transport) {
+        return Stream.of(
+                Arguments.of(getTransportName(transport) + " database detail", transport, "shardingsphere://databases/" + logicalDatabaseName, "database", logicalDatabaseName),
+                Arguments.of(getTransportName(transport) + " schema detail", transport, "shardingsphere://databases/" + logicalDatabaseName + "/schemas/" + logicalDatabaseName, "schema",
+                        logicalDatabaseName),
+                Arguments.of(getTransportName(transport) + " table column detail", transport,
+                        "shardingsphere://databases/" + logicalDatabaseName + "/schemas/" + logicalDatabaseName + "/tables/orders/columns/status", "column", "status"),
+                Arguments.of(getTransportName(transport) + " view detail", transport,
+                        "shardingsphere://databases/" + logicalDatabaseName + "/schemas/" + logicalDatabaseName + "/views/active_orders", "view", "active_orders"),
+                Arguments.of(getTransportName(transport) + " view column detail", transport,
+                        "shardingsphere://databases/" + logicalDatabaseName + "/schemas/" + logicalDatabaseName + "/views/active_orders/columns/status", "column", "status"),
+                Arguments.of(getTransportName(transport) + " index detail", transport,
+                        "shardingsphere://databases/" + logicalDatabaseName + "/schemas/" + logicalDatabaseName + "/tables/orders/indexes/idx_orders_status", "index", "idx_orders_status"));
+    }
+}
diff --git a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/runtime/MySQLRuntimeDockerSupport.java b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/runtime/MySQLRuntimeDockerSupport.java
new file mode 100644
index 00000000000..7260416075e
--- /dev/null
+++ b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/runtime/MySQLRuntimeDockerSupport.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.test.e2e.mcp.support.runtime;
+
+import org.apache.shardingsphere.test.e2e.env.runtime.EnvironmentPropertiesLoader;
+import org.testcontainers.DockerClientFactory;
+
+import java.time.Duration;
+import java.util.Optional;
+import java.util.Properties;
+
+final class MySQLRuntimeDockerSupport {
+    
+    private MySQLRuntimeDockerSupport() {
+    }
+    
+    static String getMySQLImage() {
+        return getMySQLImage(EnvironmentPropertiesLoader.loadProperties());
+    }
+    
+    static String getMySQLImage(final Properties props) {
+        String result = props.getProperty("mcp.e2e.mysql.image", "").trim();
+        if (result.isEmpty()) {
+            throw new IllegalStateException("MCP E2E MySQL image property `mcp.e2e.mysql.image` is required.");
+        }
+        return result;
+    }
+    
+    static boolean isDockerAvailable() {
+        return getDockerUnavailableReason().isEmpty();
+    }
+    
+    static Optional<String> getDockerUnavailableReason() {
+        try {
+            return DockerClientFactory.instance().isDockerAvailable()
+                    ? Optional.empty()
+                    : Optional.of("Testcontainers Docker client reported Docker unavailable.");
+        } catch (final IllegalStateException ex) {
+            return Optional.of(createDockerUnavailableReason(ex));
+        }
+    }
+    
+    static String createDockerRequiredMessage(final String scenarioMessage) {
+        return createDockerRequiredMessage(scenarioMessage, getDockerUnavailableReason().orElse(""));
+    }
+    
+    static String createDockerRequiredMessage(final String scenarioMessage, final String unavailableReason) {
+        return unavailableReason.isEmpty() ? scenarioMessage : scenarioMessage + " Docker readiness diagnostic: " + unavailableReason;
+    }
+    
+    static long getJdbcReadyTimeoutMillis(final Duration defaultReadyTimeout) {
+        String configuredJdbcReadyTimeoutSeconds = EnvironmentPropertiesLoader.loadProperties().getProperty("mcp.e2e.mysql.ready-timeout-seconds", "").trim();
+        try {
+            if (configuredJdbcReadyTimeoutSeconds.isEmpty()) {
+                return defaultReadyTimeout.toMillis();
+            }
+            int parsedJdbcReadyTimeoutSeconds = Integer.parseInt(configuredJdbcReadyTimeoutSeconds);
+            if (0 >= parsedJdbcReadyTimeoutSeconds) {
+                throw new IllegalArgumentException("MCP E2E MySQL JDBC readiness timeout must be positive.");
+            }
+            return Duration.ofSeconds(parsedJdbcReadyTimeoutSeconds).toMillis();
+        } catch (final NumberFormatException ex) {
+            throw new IllegalArgumentException("MCP E2E MySQL JDBC readiness timeout must be an integer.", ex);
+        }
+    }
+    
+    private static String createDockerUnavailableReason(final IllegalStateException ex) {
+        return null == ex.getMessage() || ex.getMessage().isBlank()
+                ? "Testcontainers Docker availability check failed without a message."
+                : ex.getMessage();
+    }
+}
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 1ad17219004..2edf51b44fa 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
@@ -22,8 +22,6 @@ import lombok.Getter;
 import lombok.NoArgsConstructor;
 import lombok.RequiredArgsConstructor;
 import org.apache.shardingsphere.mcp.support.database.metadata.jdbc.RuntimeDatabaseConfiguration;
-import org.apache.shardingsphere.test.e2e.env.runtime.EnvironmentPropertiesLoader;
-import org.testcontainers.DockerClientFactory;
 import org.testcontainers.containers.GenericContainer;
 import org.testcontainers.containers.wait.strategy.Wait;
 import org.testcontainers.utility.DockerImageName;
@@ -75,7 +73,7 @@ public final class MySQLRuntimeTestSupport {
      * @return MySQL runtime container
      */
     public static GenericContainer<?> createContainer() {
-        return new GenericContainer<>(DockerImageName.parse(getMySQLImage()))
+        return new GenericContainer<>(DockerImageName.parse(MySQLRuntimeDockerSupport.getMySQLImage()))
                 .withEnv("MYSQL_ROOT_PASSWORD", ROOT_PASSWORD)
                 .withEnv("MYSQL_DATABASE", DATABASE_NAME)
                 .withEnv("MYSQL_USER", USERNAME)
@@ -85,16 +83,8 @@ public final class MySQLRuntimeTestSupport {
                 .withStartupTimeout(Duration.ofMinutes(2));
     }
     
-    private static String getMySQLImage() {
-        return getMySQLImage(EnvironmentPropertiesLoader.loadProperties());
-    }
-    
     static String getMySQLImage(final Properties props) {
-        String result = props.getProperty("mcp.e2e.mysql.image", "").trim();
-        if (result.isEmpty()) {
-            throw new IllegalStateException("MCP E2E MySQL image property `mcp.e2e.mysql.image` is required.");
-        }
-        return result;
+        return MySQLRuntimeDockerSupport.getMySQLImage(props);
     }
     
     /**
@@ -103,7 +93,7 @@ public final class MySQLRuntimeTestSupport {
      * @return whether Docker is available
      */
     public static boolean isDockerAvailable() {
-        return getDockerUnavailableReason().isEmpty();
+        return MySQLRuntimeDockerSupport.isDockerAvailable();
     }
     
     /**
@@ -112,13 +102,7 @@ public final class MySQLRuntimeTestSupport {
      * @return Docker unavailable reason
      */
     public static Optional<String> getDockerUnavailableReason() {
-        try {
-            return DockerClientFactory.instance().isDockerAvailable()
-                    ? Optional.empty()
-                    : Optional.of("Testcontainers Docker client reported Docker unavailable.");
-        } catch (final IllegalStateException ex) {
-            return Optional.of(createDockerUnavailableReason(ex));
-        }
+        return MySQLRuntimeDockerSupport.getDockerUnavailableReason();
     }
     
     /**
@@ -128,17 +112,11 @@ public final class MySQLRuntimeTestSupport {
      * @return Docker-required message
      */
     public static String createDockerRequiredMessage(final String scenarioMessage) {
-        return createDockerRequiredMessage(scenarioMessage, getDockerUnavailableReason().orElse(""));
+        return MySQLRuntimeDockerSupport.createDockerRequiredMessage(scenarioMessage);
     }
     
     static String createDockerRequiredMessage(final String scenarioMessage, final String unavailableReason) {
-        return unavailableReason.isEmpty() ? scenarioMessage : scenarioMessage + " Docker readiness diagnostic: " + unavailableReason;
-    }
-    
-    private static String createDockerUnavailableReason(final IllegalStateException ex) {
-        return null == ex.getMessage() || ex.getMessage().isBlank()
-                ? "Testcontainers Docker availability check failed without a message."
-                : ex.getMessage();
+        return MySQLRuntimeDockerSupport.createDockerRequiredMessage(scenarioMessage, unavailableReason);
     }
     
     /**
@@ -324,21 +302,7 @@ public final class MySQLRuntimeTestSupport {
     
     private static Connection getConnection(final GenericContainer<?> container, final String databaseName) throws SQLException {
         String jdbcUrl = createJdbcUrl(container.getHost(), container.getMappedPort(3306), databaseName);
-        String configuredJdbcReadyTimeoutSeconds = EnvironmentPropertiesLoader.loadProperties().getProperty("mcp.e2e.mysql.ready-timeout-seconds", "").trim();
-        long jdbcReadyTimeoutMillis;
-        try {
-            if (configuredJdbcReadyTimeoutSeconds.isEmpty()) {
-                jdbcReadyTimeoutMillis = DEFAULT_JDBC_READY_TIMEOUT.toMillis();
-            } else {
-                int parsedJdbcReadyTimeoutSeconds = Integer.parseInt(configuredJdbcReadyTimeoutSeconds);
-                if (0 >= parsedJdbcReadyTimeoutSeconds) {
-                    throw new IllegalArgumentException("MCP E2E MySQL JDBC readiness timeout must be positive.");
-                }
-                jdbcReadyTimeoutMillis = Duration.ofSeconds(parsedJdbcReadyTimeoutSeconds).toMillis();
-            }
-        } catch (final NumberFormatException ex) {
-            throw new IllegalArgumentException("MCP E2E MySQL JDBC readiness timeout must be an integer.", ex);
-        }
+        long jdbcReadyTimeoutMillis = MySQLRuntimeDockerSupport.getJdbcReadyTimeoutMillis(DEFAULT_JDBC_READY_TIMEOUT);
         try {
             return new ReadinessProbe(jdbcReadyTimeoutMillis, JDBC_READY_INITIAL_INTERVAL_MILLIS, JDBC_READY_MAX_INTERVAL_MILLIS)
                     .waitUntilReady(() -> getConnectionIfReady(jdbcUrl),