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/05/28 04:51:54 UTC

(shardingsphere) branch master updated: Simplify tool elicitation handler (#38740)

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 5c5270057ca Simplify tool elicitation handler (#38740)
5c5270057ca is described below

commit 5c5270057cad39b8c252a1199b3f5959c3063bd2
Author: Liang Zhang <zh...@apache.org>
AuthorDate: Thu May 28 12:51:48 2026 +0800

    Simplify tool elicitation handler (#38740)
    
    Refactor MCP tool elicitation handling by extracting client capability
    detection and fallback response construction into dedicated public types.
    Restore ArgumentBinding semantic helper usage, merge elicitation workflow
    coverage into the real specification factory test, and add focused tests for
    new public contracts.
    
    Also document helper extraction boundaries in AGENTS.md.
---
 AGENTS.md                                          |  20 +-
 .../tool/MCPClientElicitationCapabilities.java     |  51 +++
 .../tool/MCPToolClarificationPolicy.java           |   7 +-
 .../tool/MCPToolElicitationFallbackReason.java     |  63 ++++
 .../MCPToolElicitationFallbackResponseFactory.java | 106 ++++++
 .../capability/tool/MCPToolElicitationHandler.java | 164 ++-------
 .../tool/MCPClientElicitationCapabilitiesTest.java |  85 +++++
 .../tool/MCPToolClarificationPolicyTest.java       |  35 ++
 .../tool/MCPToolElicitationFallbackReasonTest.java |  88 +++++
 ...ToolElicitationFallbackResponseFactoryTest.java |  78 ++++
 ...MCPToolSpecificationElicitationFactoryTest.java | 397 ---------------------
 .../tool/MCPToolSpecificationFactoryTest.java      | 364 +++++++++++++++++++
 12 files changed, 921 insertions(+), 537 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md
index 77a31843edc..2c4197a4f84 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -18,6 +18,15 @@ This guide is written **for AI coding agents only**. Follow it literally; improv
 - **Architecture**: follow SOLID, DRY, separation of concerns, and YAGNI (build only what you need).
 - **Code Quality**:
     - Use clear naming and reasonable abstractions.
+    - Do not introduce package-private top-level helper types by default.
+      Keep very small, single-owner state or continuation helpers as private nested types, but avoid accumulating multiple nested collaborators inside one class.
+      When a helper has cohesive behavior, multiple callers, direct test value, or enough logic to distract from the owner class, split it into a public top-level type with a clear contract and direct tests.
+      If neither private nor public fits, pause before coding and explain why.
+    - Every new public production type must have direct, focused tests.
+      Broad workflow tests do not replace public contract tests unless they explicitly exercise that public type's behavior.
+    - New internal abstractions must reduce cognitive complexity instead of merely wrapping branches in more types.
+      For simple internal two-path flows, avoid marker interfaces, multi-type result hierarchies, or extra DTO-style helpers.
+      Add them only when they define a stable boundary, keep owner classes readable, or remove meaningful duplicated logic.
     - Delete unused code; when changing functionality, remove legacy compatibility shims.
     - Keep variable declarations adjacent to first use; if a value must be retained, declare it `final` to satisfy Checkstyle VariableDeclarationUsageDistance.
     - Single-use local variables must be inlined by default; keep a local variable only when it is reused (for stubbing/verification/assertions) or materially improves readability.
@@ -43,7 +52,9 @@ This guide is written **for AI coding agents only**. Follow it literally; improv
 - **Test-Driven**: design for testability, ensure unit-test coverage, and keep background unit tests under 60s to avoid job stalls.
 - **Quality Assurance**: run static checks, formatting, and code reviews.
 - **Checkstyle Gate**: do not hand off code with Checkstyle/Spotless failures—run the relevant module check locally and fix before completion.
-- **Formatting Gate**: after code changes, format only with `./mvnw spotless:apply -Pcheck -T1C`, then check style with `./mvnw checkstyle:check -Pcheck -T1C`; do not use any other formatting method.
+- **Formatting Gate**: after code or documentation changes, format only with `./mvnw spotless:apply -Pcheck -T1C`, then check style with `./mvnw checkstyle:check -Pcheck -T1C`; do not use any other formatting method.
+  Spotless must run after the last file-changing action and before Checkstyle/tests in the final handoff sequence.
+  If any file is edited, generated, moved, or manually whitespace-cleaned after Spotless, rerun Spotless before replying.
 - **Continuous Verification**: rely on automated tests and integration validation.
 - **Test Naming Simplicity**: keep test names concise and scenario-focused (avoid “ReturnsXXX”/overly wordy or AI-like phrasing); describe the scenario directly.
 - **Coverage Discipline**: follow the dedicated coverage & branch checklist before coding when coverage targets are stated.
@@ -120,6 +131,8 @@ Dangerous operation detected! Operation type: [specific action] Scope of impact:
 - **Execution discipline:** inspect existing code before edits; keep changes minimal; default to mocks and SPI loaders; keep variable declarations near first use and mark retained values `final`; inline single-use locals by default unless reuse/readability justifies retention; delete dead code and avoid placeholders/TODOs.
 - **AGENTS.md maintenance:** do not add or update a `Session Notes` section in `AGENTS.md`. Keep task-specific notes in the active conversation, issue, or PR; only stable project-level rules may be generalized into this file.
 - **Post-task self-check (before replying):** confirm all instructions were honored; verify no placeholders/unused code; ensure Checkstyle/Spotless gates for touched modules are satisfied or explain why not run and what to run; list commands with exit codes; call out risks and follow-ups; complete all applicable checks before replying and do not rely on users to find missed rule violations.
+- **End-of-task format/style gate:** for any task that edits files, run `./mvnw spotless:apply -Pcheck -T1C` after the final edit, then run `./mvnw checkstyle:check -Pcheck -T1C` when production, test, or project-rule files are touched.
+  Do not perform manual formatting or whitespace cleanup after the final Spotless run; if a later cleanup is required, repeat Spotless and then Checkstyle before the final response.
 - **Final response template:** include intent/why, changed files with paths, rationale per file/section, commands run (with exit codes), verification status, and remaining risks/next actions (if tests skipped, state reason and the exact command to run); include a concise self-check result statement confirming final clean status after fixes.
 
 ## Final Self-Iteration Gate
@@ -228,8 +241,9 @@ Always state which topology, registry, and engine versions (e.g., MySQL 5.7 vs 8
 ## Verification & Commands
 - Core commands: `./mvnw clean install -B -T1C -Pcheck` (full build), `./mvnw test -pl <module>[-am]` (scoped unit tests), `./mvnw -pl <module> -DskipITs -Dspotless.skip=true -Dtest=ClassName test` (fast verification), `./mvnw -pl proxy -am -DskipTests package` (proxy packaging/perf smoke).
 - Coverage: when tests change or targets demand it, run `./mvnw test jacoco:check@jacoco-check -Pcoverage-check` or scoped `-pl <module> -am -Djacoco.skip=false test jacoco:report`; pair with the Coverage & Branch Checklist.
-- Format: after code changes, run `./mvnw spotless:apply -Pcheck -T1C`; do not use any other formatting method.
-- Style: after code changes and formatting, run `./mvnw checkstyle:check -Pcheck -T1C`.
+- Format: after code or documentation changes, run `./mvnw spotless:apply -Pcheck -T1C`; do not use any other formatting method.
+  This must be repeated after the last file-changing action before handoff.
+- Style: after formatting, run `./mvnw checkstyle:check -Pcheck -T1C` when production, test, or project-rule files are touched.
 - Scoped defaults: prefer module-scoped runs over whole-repo builds; include `-Dsurefire.failIfNoSpecifiedTests=false` when targeting specific tests.
 - Testing ground rules: JUnit 5 + Mockito, `ClassNameTest` naming, Arrange–Act–Assert, mock external systems/time/network, reset static caches, and reuse swappers/helpers for complex configs.
 - API bans: if a user forbids a tool/assertion, add it to the plan, avoid it during implementation, and cite verification searches (e.g., `rg assertEquals`) in the final report.
diff --git a/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPClientElicitationCapabilities.java b/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPClientElicitationCapabilities.java
new file mode 100644
index 00000000000..eebdc80d6b3
--- /dev/null
+++ b/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPClientElicitationCapabilities.java
@@ -0,0 +1,51 @@
+/*
+ * 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.server.McpSyncServerExchange;
+import io.modelcontextprotocol.spec.McpSchema;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * MCP client elicitation capabilities.
+ */
+@Getter
+@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
+public final class MCPClientElicitationCapabilities {
+    
+    private final boolean formModeSupported;
+    
+    private final boolean urlModeSupported;
+    
+    /**
+     * Create client elicitation capabilities from server exchange.
+     *
+     * @param exchange MCP sync server exchange
+     * @return client elicitation capabilities
+     */
+    public static MCPClientElicitationCapabilities from(final McpSyncServerExchange exchange) {
+        McpSchema.ClientCapabilities clientCapabilities = exchange.getClientCapabilities();
+        if (null == clientCapabilities || null == clientCapabilities.elicitation()) {
+            return new MCPClientElicitationCapabilities(false, false);
+        }
+        McpSchema.ClientCapabilities.Elicitation elicitation = clientCapabilities.elicitation();
+        return new MCPClientElicitationCapabilities(null != elicitation.form() || null == elicitation.url(), null != elicitation.url());
+    }
+}
diff --git a/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPToolClarificationPolicy.java b/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPToolClarificationPolicy.java
index d730e6cab4a..87d2b68c93a 100644
--- a/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPToolClarificationPolicy.java
+++ b/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPToolClarificationPolicy.java
@@ -81,15 +81,12 @@ final class MCPToolClarificationPolicy {
                 return Optional.empty();
             }
             ArgumentBinding argumentBinding = binding.get();
-            if (fieldBindings.containsKey(argumentBinding.formPropertyName())) {
-                return Optional.empty();
-            }
             properties.put(argumentBinding.formPropertyName(), createRequestedPropertySchema(question, argumentBinding));
             required.add(argumentBinding.formPropertyName());
             fieldBindings.put(argumentBinding.formPropertyName(), argumentBinding);
             questionIndex++;
         }
-        return properties.isEmpty() ? Optional.empty() : Optional.of(new ClarificationForm(createObjectSchema(properties, required), fieldBindings, planId));
+        return Optional.of(new ClarificationForm(createObjectSchema(properties, required), fieldBindings, planId));
     }
     
     boolean hasSensitiveClarificationQuestions(final Map<String, Object> payload) {
@@ -140,7 +137,7 @@ final class MCPToolClarificationPolicy {
         return value.replaceAll(CAMEL_CASE_SEPARATOR_PATTERN, "$1 $2").toLowerCase(Locale.ENGLISH).replaceAll(NON_ALPHANUMERIC_PATTERN, "");
     }
     
-    String getPlanId(final Map<String, Object> payload) {
+    private String getPlanId(final Map<String, Object> payload) {
         return Objects.toString(payload.get(WorkflowFieldNames.PLAN_ID), "");
     }
     
diff --git a/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPToolElicitationFallbackReason.java b/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPToolElicitationFallbackReason.java
new file mode 100644
index 00000000000..7a80a06a190
--- /dev/null
+++ b/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPToolElicitationFallbackReason.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.bootstrap.transport.capability.tool;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * MCP tool elicitation fallback reason.
+ */
+@Getter
+@RequiredArgsConstructor
+public enum MCPToolElicitationFallbackReason {
+    
+    CLIENT_UNSUPPORTED("client_unsupported", "structured_fallback"),
+    
+    REMOTE_IDENTITY_REQUIRED("remote_identity_required", "structured_fallback"),
+    
+    MISSING_PLAN_ID("missing_plan_id", "structured_fallback"),
+    
+    SENSITIVE_FORM_BLOCKED("sensitive_form_blocked", "url_fallback"),
+    
+    URL_MODE_NOT_IMPLEMENTED("url_mode_not_implemented", "url_fallback"),
+    
+    AMBIGUOUS_FIELD_BINDING("ambiguous_field_binding", "structured_fallback"),
+    
+    ELICITATION_FAILED("elicitation_failed", "structured_fallback"),
+    
+    MALFORMED_ELICITATION_RESULT("malformed_elicitation_result", "structured_fallback"),
+    
+    INVALID_ELICITED_CONTENT("invalid_elicited_content", "structured_fallback"),
+    
+    STALE_ELICITATION("stale_elicitation", "structured_fallback");
+    
+    private final String value;
+    
+    private final String selectedInteraction;
+    
+    /**
+     * Adjust fallback reason according to client capabilities.
+     *
+     * @param clientCapabilities client elicitation capabilities
+     * @return fallback reason
+     */
+    public MCPToolElicitationFallbackReason withClientCapabilities(final MCPClientElicitationCapabilities clientCapabilities) {
+        return SENSITIVE_FORM_BLOCKED == this && clientCapabilities.isUrlModeSupported() ? URL_MODE_NOT_IMPLEMENTED : this;
+    }
+}
diff --git a/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPToolElicitationFallbackResponseFactory.java b/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPToolElicitationFallbackResponseFactory.java
new file mode 100644
index 00000000000..46b8d3f0412
--- /dev/null
+++ b/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPToolElicitationFallbackResponseFactory.java
@@ -0,0 +1,106 @@
+/*
+ * 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 org.apache.shardingsphere.mcp.api.protocol.response.MCPResponse;
+import org.apache.shardingsphere.mcp.support.protocol.MCPPayloadFieldNames;
+import org.apache.shardingsphere.mcp.support.protocol.response.MCPMapResponse;
+
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * MCP tool elicitation fallback response factory.
+ */
+public final class MCPToolElicitationFallbackResponseFactory {
+    
+    private static final String ELICITATION_SUPPORT_FIELD = "elicitation_support";
+    
+    private static final String FALLBACK_REASON_FIELD = "fallback_reason";
+    
+    private static final String FORM_MODE_FIELD = "form_mode";
+    
+    private static final String URL_MODE_FIELD = "url_mode";
+    
+    private static final String SELECTED_INTERACTION_FIELD = "selected_interaction";
+    
+    private final MCPToolClarificationPolicy clarificationPolicy = new MCPToolClarificationPolicy();
+    
+    /**
+     * Create fallback response.
+     *
+     * @param payload original payload
+     * @param fallbackReason fallback reason
+     * @param clientCapabilities client elicitation capabilities
+     * @return MCP response
+     */
+    public MCPResponse create(final Map<String, Object> payload, final MCPToolElicitationFallbackReason fallbackReason,
+                              final MCPClientElicitationCapabilities clientCapabilities) {
+        Map<String, Object> result = new LinkedHashMap<>(payload);
+        if (clarificationPolicy.hasSensitiveClarificationQuestions(payload)) {
+            result.put(MCPPayloadFieldNames.CLARIFICATION_QUESTIONS, createSanitizedClarificationQuestions(payload));
+            result.put(MCPPayloadFieldNames.NEXT_ACTIONS, createSensitiveNextActions());
+        }
+        result.put(ELICITATION_SUPPORT_FIELD, createElicitationSupportPayload(clientCapabilities, fallbackReason.getSelectedInteraction()));
+        result.put(FALLBACK_REASON_FIELD, fallbackReason.getValue());
+        return new MCPMapResponse(result);
+    }
+    
+    private List<Map<String, Object>> createSanitizedClarificationQuestions(final Map<String, Object> payload) {
+        Object clarificationQuestions = payload.get(MCPPayloadFieldNames.CLARIFICATION_QUESTIONS);
+        if (!(clarificationQuestions instanceof List<?> questions)) {
+            return List.of();
+        }
+        List<Map<String, Object>> result = new LinkedList<>();
+        for (Object each : questions) {
+            if (each instanceof Map<?, ?> question) {
+                result.add(createSanitizedClarificationQuestion(question));
+            }
+        }
+        return result;
+    }
+    
+    private Map<String, Object> createSanitizedClarificationQuestion(final Map<?, ?> question) {
+        Map<String, Object> result = new LinkedHashMap<>(4, 1F);
+        result.put(MCPPayloadFieldNames.FIELD, Objects.toString(question.get(MCPPayloadFieldNames.FIELD), ""));
+        result.put(MCPPayloadFieldNames.INPUT_TYPE, "secret");
+        result.put(MCPPayloadFieldNames.SECRET, true);
+        result.put(MCPPayloadFieldNames.MESSAGE, "Sensitive input must be provided through configured secure channels before continuing the same planner.");
+        return result;
+    }
+    
+    private List<Map<String, Object>> createSensitiveNextActions() {
+        Map<String, Object> result = new LinkedHashMap<>(4, 1F);
+        result.put("order", 1);
+        result.put("type", "terminal");
+        result.put("title", "Collect sensitive inputs through configured secure channels.");
+        result.put(MCPPayloadFieldNames.REASON, "MCP form elicitation is limited to non-sensitive STDIO continuations; URL mode is not implemented in this release.");
+        return List.of(result);
+    }
+    
+    private Map<String, Object> createElicitationSupportPayload(final MCPClientElicitationCapabilities clientCapabilities, final String selectedInteraction) {
+        Map<String, Object> result = new LinkedHashMap<>(3, 1F);
+        result.put(FORM_MODE_FIELD, clientCapabilities.isFormModeSupported());
+        result.put(URL_MODE_FIELD, clientCapabilities.isUrlModeSupported());
+        result.put(SELECTED_INTERACTION_FIELD, selectedInteraction);
+        return result;
+    }
+}
diff --git a/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPToolElicitationHandler.java b/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPToolElicitationHandler.java
index 55ce709b117..14d6c031041 100644
--- a/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPToolElicitationHandler.java
+++ b/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPToolElicitationHandler.java
@@ -26,15 +26,11 @@ import org.apache.shardingsphere.mcp.api.tool.descriptor.MCPToolDescriptor;
 import org.apache.shardingsphere.mcp.core.tool.MCPToolController;
 import org.apache.shardingsphere.mcp.core.tool.handler.MCPToolDefinition;
 import org.apache.shardingsphere.mcp.support.descriptor.MCPShardingSphereMetadataKeys;
-import org.apache.shardingsphere.mcp.support.protocol.MCPPayloadFieldNames;
-import org.apache.shardingsphere.mcp.support.protocol.response.MCPMapResponse;
+import org.apache.shardingsphere.mcp.support.workflow.model.WorkflowFieldNames;
 
 import java.time.Clock;
 import java.time.Duration;
 import java.time.Instant;
-import java.util.LinkedHashMap;
-import java.util.LinkedList;
-import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
@@ -50,40 +46,6 @@ final class MCPToolElicitationHandler {
     
     private static final Duration FORM_CONTINUATION_TTL = Duration.ofMinutes(10L);
     
-    private static final String ELICITATION_SUPPORT_FIELD = "elicitation_support";
-    
-    private static final String FALLBACK_REASON_FIELD = "fallback_reason";
-    
-    private static final String FORM_MODE_FIELD = "form_mode";
-    
-    private static final String URL_MODE_FIELD = "url_mode";
-    
-    private static final String SELECTED_INTERACTION_FIELD = "selected_interaction";
-    
-    private static final String STRUCTURED_FALLBACK_INTERACTION = "structured_fallback";
-    
-    private static final String URL_FALLBACK_INTERACTION = "url_fallback";
-    
-    private static final String CLIENT_UNSUPPORTED_REASON = "client_unsupported";
-    
-    private static final String REMOTE_IDENTITY_REQUIRED_REASON = "remote_identity_required";
-    
-    private static final String MISSING_PLAN_ID_REASON = "missing_plan_id";
-    
-    private static final String SENSITIVE_FORM_BLOCKED_REASON = "sensitive_form_blocked";
-    
-    private static final String URL_MODE_NOT_IMPLEMENTED_REASON = "url_mode_not_implemented";
-    
-    private static final String AMBIGUOUS_FIELD_BINDING_REASON = "ambiguous_field_binding";
-    
-    private static final String ELICITATION_FAILED_REASON = "elicitation_failed";
-    
-    private static final String MALFORMED_ELICITATION_RESULT_REASON = "malformed_elicitation_result";
-    
-    private static final String INVALID_ELICITED_CONTENT_REASON = "invalid_elicited_content";
-    
-    private static final String STALE_ELICITATION_REASON = "stale_elicitation";
-    
     private final MCPToolController toolController;
     
     private final String activeTransport;
@@ -92,6 +54,8 @@ final class MCPToolElicitationHandler {
     
     private final MCPToolClarificationPolicy clarificationPolicy = new MCPToolClarificationPolicy();
     
+    private final MCPToolElicitationFallbackResponseFactory fallbackResponseFactory = new MCPToolElicitationFallbackResponseFactory();
+    
     boolean shouldHandle(final MCPToolDescriptor toolDescriptor, final Map<String, Object> payload) {
         return clarificationPolicy.requiresPlanningClarification(toolDescriptor, payload);
     }
@@ -99,122 +63,62 @@ final class MCPToolElicitationHandler {
     MCPResponse handle(final McpSyncServerExchange exchange, final MCPToolDefinition toolDefinition, final Map<String, Object> arguments,
                        final MCPResponse fallbackResponse, final Map<String, Object> payload) {
         MCPToolDescriptor toolDescriptor = toolDefinition.getDescriptor();
-        ClientElicitationSupport clientSupport = getClientElicitationSupport(exchange);
+        MCPClientElicitationCapabilities clientCapabilities = MCPClientElicitationCapabilities.from(exchange);
         Optional<MCPToolClarificationPolicy.ClarificationForm> clarificationForm = clarificationPolicy.createClarificationForm(payload, toolDescriptor);
         if (clarificationForm.isEmpty()) {
-            return createFallbackResponse(payload, determineUnavailableFormReason(payload, clientSupport), clientSupport);
+            return createFallbackResponse(payload, getUnavailableFormFallbackReason(payload, clientCapabilities), clientCapabilities);
         }
-        if (!clientSupport.supportsFormMode()) {
-            return createFallbackResponse(payload, CLIENT_UNSUPPORTED_REASON, clientSupport);
+        if (!clientCapabilities.isFormModeSupported()) {
+            return createFallbackResponse(payload, MCPToolElicitationFallbackReason.CLIENT_UNSUPPORTED, clientCapabilities);
         }
         if (!STDIO_TRANSPORT.equals(activeTransport)) {
-            return createFallbackResponse(payload, REMOTE_IDENTITY_REQUIRED_REASON, clientSupport);
+            return createFallbackResponse(payload, MCPToolElicitationFallbackReason.REMOTE_IDENTITY_REQUIRED, clientCapabilities);
         }
         FormContinuationContext continuationContext = createContinuationContext(exchange, toolDescriptor, arguments, clarificationForm.get());
         McpSchema.ElicitResult elicitedResult;
         try {
             elicitedResult = exchange.createElicitation(createElicitRequest(toolDescriptor.getName(), clarificationForm.get(), continuationContext.formRequestId()));
         } catch (final McpError | IllegalStateException | UnsupportedOperationException ignored) {
-            return createFallbackResponse(payload, ELICITATION_FAILED_REASON, clientSupport);
+            return createFallbackResponse(payload, MCPToolElicitationFallbackReason.ELICITATION_FAILED, clientCapabilities);
         }
-        return continueOrFallback(exchange, toolDefinition, arguments, fallbackResponse, payload, clarificationForm.get(), continuationContext, elicitedResult, clientSupport);
+        return continueOrFallback(exchange, toolDefinition, arguments, fallbackResponse, payload, clarificationForm.get(), continuationContext, elicitedResult, clientCapabilities);
     }
     
     private MCPResponse continueOrFallback(final McpSyncServerExchange exchange, final MCPToolDefinition toolDefinition, final Map<String, Object> arguments,
                                            final MCPResponse fallbackResponse, final Map<String, Object> payload,
                                            final MCPToolClarificationPolicy.ClarificationForm clarificationForm,
                                            final FormContinuationContext continuationContext, final McpSchema.ElicitResult elicitedResult,
-                                           final ClientElicitationSupport clientSupport) {
+                                           final MCPClientElicitationCapabilities clientCapabilities) {
         if (null == elicitedResult || null == elicitedResult.action()) {
-            return createFallbackResponse(payload, MALFORMED_ELICITATION_RESULT_REASON, clientSupport);
+            return createFallbackResponse(payload, MCPToolElicitationFallbackReason.MALFORMED_ELICITATION_RESULT, clientCapabilities);
         }
         if (McpSchema.ElicitResult.Action.ACCEPT != elicitedResult.action()) {
             return fallbackResponse;
         }
         if (null == elicitedResult.content()) {
-            return createFallbackResponse(payload, MALFORMED_ELICITATION_RESULT_REASON, clientSupport);
+            return createFallbackResponse(payload, MCPToolElicitationFallbackReason.MALFORMED_ELICITATION_RESULT, clientCapabilities);
         }
-        if (!isActiveContinuation(exchange, toolDefinition.getDescriptor(), arguments, clarificationForm, continuationContext)) {
-            return createFallbackResponse(payload, STALE_ELICITATION_REASON, clientSupport);
+        if (!continuationContext.isActive(activeTransport, clock, exchange, toolDefinition.getDescriptor(), arguments, clarificationForm)) {
+            return createFallbackResponse(payload, MCPToolElicitationFallbackReason.STALE_ELICITATION, clientCapabilities);
         }
         if (!clarificationPolicy.isValidElicitedContent(clarificationForm, elicitedResult.content())) {
-            return createFallbackResponse(payload, INVALID_ELICITED_CONTENT_REASON, clientSupport);
+            return createFallbackResponse(payload, MCPToolElicitationFallbackReason.INVALID_ELICITED_CONTENT, clientCapabilities);
         }
         return toolController.handle(exchange.sessionId(), toolDefinition, clarificationPolicy.mergeArguments(arguments, clarificationForm, elicitedResult.content()));
     }
     
-    private ClientElicitationSupport getClientElicitationSupport(final McpSyncServerExchange exchange) {
-        McpSchema.ClientCapabilities clientCapabilities = exchange.getClientCapabilities();
-        if (null == clientCapabilities || null == clientCapabilities.elicitation()) {
-            return new ClientElicitationSupport(false, false);
-        }
-        McpSchema.ClientCapabilities.Elicitation elicitation = clientCapabilities.elicitation();
-        return new ClientElicitationSupport(null != elicitation.form() || null == elicitation.url(), null != elicitation.url());
+    private MCPResponse createFallbackResponse(final Map<String, Object> payload, final MCPToolElicitationFallbackReason fallbackReason,
+                                               final MCPClientElicitationCapabilities clientCapabilities) {
+        return fallbackResponseFactory.create(payload, fallbackReason, clientCapabilities);
     }
     
-    private String determineUnavailableFormReason(final Map<String, Object> payload, final ClientElicitationSupport clientSupport) {
-        if (clarificationPolicy.getPlanId(payload).trim().isEmpty()) {
-            return MISSING_PLAN_ID_REASON;
+    private MCPToolElicitationFallbackReason getUnavailableFormFallbackReason(final Map<String, Object> payload, final MCPClientElicitationCapabilities clientCapabilities) {
+        if (Objects.toString(payload.get(WorkflowFieldNames.PLAN_ID), "").trim().isEmpty()) {
+            return MCPToolElicitationFallbackReason.MISSING_PLAN_ID;
         }
-        if (!clarificationPolicy.hasSensitiveClarificationQuestions(payload)) {
-            return AMBIGUOUS_FIELD_BINDING_REASON;
-        }
-        return clientSupport.supportsUrlMode() ? URL_MODE_NOT_IMPLEMENTED_REASON : SENSITIVE_FORM_BLOCKED_REASON;
-    }
-    
-    private MCPResponse createFallbackResponse(final Map<String, Object> payload, final String fallbackReason, final ClientElicitationSupport clientSupport) {
-        Map<String, Object> result = new LinkedHashMap<>(payload);
-        if (clarificationPolicy.hasSensitiveClarificationQuestions(payload)) {
-            result.put(MCPPayloadFieldNames.CLARIFICATION_QUESTIONS, createSanitizedClarificationQuestions(payload));
-            result.put(MCPPayloadFieldNames.NEXT_ACTIONS, createSensitiveNextActions());
-        }
-        result.put(ELICITATION_SUPPORT_FIELD, createElicitationSupportPayload(clientSupport, selectFallbackInteraction(fallbackReason)));
-        result.put(FALLBACK_REASON_FIELD, fallbackReason);
-        return new MCPMapResponse(result);
-    }
-    
-    private List<Map<String, Object>> createSanitizedClarificationQuestions(final Map<String, Object> payload) {
-        Object clarificationQuestions = payload.get(MCPPayloadFieldNames.CLARIFICATION_QUESTIONS);
-        if (!(clarificationQuestions instanceof List<?> questions)) {
-            return List.of();
-        }
-        List<Map<String, Object>> result = new LinkedList<>();
-        for (Object each : questions) {
-            if (each instanceof Map<?, ?> question) {
-                result.add(createSanitizedClarificationQuestion(question));
-            }
-        }
-        return result;
-    }
-    
-    private Map<String, Object> createSanitizedClarificationQuestion(final Map<?, ?> question) {
-        Map<String, Object> result = new LinkedHashMap<>(4, 1F);
-        result.put(MCPPayloadFieldNames.FIELD, Objects.toString(question.get(MCPPayloadFieldNames.FIELD), ""));
-        result.put(MCPPayloadFieldNames.INPUT_TYPE, "secret");
-        result.put(MCPPayloadFieldNames.SECRET, true);
-        result.put(MCPPayloadFieldNames.MESSAGE, "Sensitive input must be provided through configured secure channels before continuing the same planner.");
-        return result;
-    }
-    
-    private List<Map<String, Object>> createSensitiveNextActions() {
-        Map<String, Object> result = new LinkedHashMap<>(4, 1F);
-        result.put("order", 1);
-        result.put("type", "terminal");
-        result.put("title", "Collect sensitive inputs through configured secure channels.");
-        result.put(MCPPayloadFieldNames.REASON, "MCP form elicitation is limited to non-sensitive STDIO continuations; URL mode is not implemented in this release.");
-        return List.of(result);
-    }
-    
-    private Map<String, Object> createElicitationSupportPayload(final ClientElicitationSupport clientSupport, final String selectedInteraction) {
-        Map<String, Object> result = new LinkedHashMap<>(3, 1F);
-        result.put(FORM_MODE_FIELD, clientSupport.supportsFormMode());
-        result.put(URL_MODE_FIELD, clientSupport.supportsUrlMode());
-        result.put(SELECTED_INTERACTION_FIELD, selectedInteraction);
-        return result;
-    }
-    
-    private String selectFallbackInteraction(final String fallbackReason) {
-        return URL_MODE_NOT_IMPLEMENTED_REASON.equals(fallbackReason) || SENSITIVE_FORM_BLOCKED_REASON.equals(fallbackReason) ? URL_FALLBACK_INTERACTION : STRUCTURED_FALLBACK_INTERACTION;
+        return clarificationPolicy.hasSensitiveClarificationQuestions(payload)
+                ? MCPToolElicitationFallbackReason.SENSITIVE_FORM_BLOCKED.withClientCapabilities(clientCapabilities)
+                : MCPToolElicitationFallbackReason.AMBIGUOUS_FIELD_BINDING;
     }
     
     private FormContinuationContext createContinuationContext(final McpSyncServerExchange exchange, final MCPToolDescriptor toolDescriptor, final Map<String, Object> arguments,
@@ -223,13 +127,6 @@ final class MCPToolElicitationHandler {
                 UUID.randomUUID().toString());
     }
     
-    private boolean isActiveContinuation(final McpSyncServerExchange exchange, final MCPToolDescriptor toolDescriptor, final Map<String, Object> arguments,
-                                         final MCPToolClarificationPolicy.ClarificationForm clarificationForm, final FormContinuationContext continuationContext) {
-        return STDIO_TRANSPORT.equals(activeTransport) && continuationContext.sessionId().equals(exchange.sessionId())
-                && continuationContext.toolName().equals(toolDescriptor.getName()) && continuationContext.planId().equals(clarificationForm.planId())
-                && continuationContext.argumentsHashCode() == arguments.hashCode() && clock.instant().isBefore(continuationContext.expiresAt());
-    }
-    
     private McpSchema.ElicitRequest createElicitRequest(final String toolName, final MCPToolClarificationPolicy.ClarificationForm clarificationForm, final String formRequestId) {
         return McpSchema.ElicitRequest.builder()
                 .message(String.format("Provide missing ShardingSphere workflow inputs for `%s`.", toolName))
@@ -245,10 +142,13 @@ final class MCPToolElicitationHandler {
                 MCPShardingSphereMetadataKeys.FORM_REQUEST_ID, formRequestId);
     }
     
-    private record ClientElicitationSupport(boolean supportsFormMode, boolean supportsUrlMode) {
-        
-    }
-    
     private record FormContinuationContext(String toolName, String sessionId, String planId, int argumentsHashCode, Instant expiresAt, String formRequestId) {
+
+        private boolean isActive(final String activeTransport, final Clock clock, final McpSyncServerExchange exchange, final MCPToolDescriptor toolDescriptor,
+                                 final Map<String, Object> arguments, final MCPToolClarificationPolicy.ClarificationForm clarificationForm) {
+            return STDIO_TRANSPORT.equals(activeTransport) && sessionId.equals(exchange.sessionId())
+                    && toolName.equals(toolDescriptor.getName()) && planId.equals(clarificationForm.planId())
+                    && argumentsHashCode == arguments.hashCode() && clock.instant().isBefore(expiresAt);
+        }
     }
 }
diff --git a/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPClientElicitationCapabilitiesTest.java b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPClientElicitationCapabilitiesTest.java
new file mode 100644
index 00000000000..81316dbe256
--- /dev/null
+++ b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPClientElicitationCapabilitiesTest.java
@@ -0,0 +1,85 @@
+/*
+ * 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.server.McpSyncServerExchange;
+import io.modelcontextprotocol.spec.McpSchema;
+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 java.util.Collections;
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class MCPClientElicitationCapabilitiesTest {
+    
+    @Test
+    void assertFromWithoutClientCapabilities() {
+        McpSyncServerExchange exchange = mock(McpSyncServerExchange.class);
+        MCPClientElicitationCapabilities actual = MCPClientElicitationCapabilities.from(exchange);
+        assertFalse(actual.isFormModeSupported());
+        assertFalse(actual.isUrlModeSupported());
+    }
+    
+    @ParameterizedTest(name = "{0}")
+    @MethodSource("getClientCapabilities")
+    void assertFrom(final String name, final McpSchema.ClientCapabilities clientCapabilities, final boolean expectedFormModeSupported, final boolean expectedUrlModeSupported) {
+        McpSyncServerExchange exchange = mock(McpSyncServerExchange.class);
+        when(exchange.getClientCapabilities()).thenReturn(clientCapabilities);
+        MCPClientElicitationCapabilities actual = MCPClientElicitationCapabilities.from(exchange);
+        assertFormModeSupported(actual, expectedFormModeSupported);
+        assertUrlModeSupported(actual, expectedUrlModeSupported);
+    }
+    
+    private void assertFormModeSupported(final MCPClientElicitationCapabilities actual, final boolean expectedFormModeSupported) {
+        if (expectedFormModeSupported) {
+            assertTrue(actual.isFormModeSupported());
+        } else {
+            assertFalse(actual.isFormModeSupported());
+        }
+    }
+    
+    private void assertUrlModeSupported(final MCPClientElicitationCapabilities actual, final boolean expectedUrlModeSupported) {
+        if (expectedUrlModeSupported) {
+            assertTrue(actual.isUrlModeSupported());
+        } else {
+            assertFalse(actual.isUrlModeSupported());
+        }
+    }
+    
+    private static Stream<Arguments> getClientCapabilities() {
+        return Stream.of(
+                Arguments.of("without elicitation capabilities", new McpSchema.ClientCapabilities(Collections.emptyMap(), null, null, null), false, false),
+                Arguments.of("with default elicitation capabilities", McpSchema.ClientCapabilities.builder().elicitation().build(), true, false),
+                Arguments.of("with form elicitation capabilities", new McpSchema.ClientCapabilities(
+                        Collections.emptyMap(), null, null,
+                        new McpSchema.ClientCapabilities.Elicitation(new McpSchema.ClientCapabilities.Elicitation.Form(), null)), true, false),
+                Arguments.of("with form and url elicitation capabilities", new McpSchema.ClientCapabilities(
+                        Collections.emptyMap(), null, null,
+                        new McpSchema.ClientCapabilities.Elicitation(new McpSchema.ClientCapabilities.Elicitation.Form(), new McpSchema.ClientCapabilities.Elicitation.Url())), true, true),
+                Arguments.of("with url elicitation capabilities", new McpSchema.ClientCapabilities(
+                        Collections.emptyMap(), null, null,
+                        new McpSchema.ClientCapabilities.Elicitation(null, new McpSchema.ClientCapabilities.Elicitation.Url())), false, true));
+    }
+}
diff --git a/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPToolClarificationPolicyTest.java b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPToolClarificationPolicyTest.java
index 1add55192bf..9821d95aef3 100644
--- a/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPToolClarificationPolicyTest.java
+++ b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPToolClarificationPolicyTest.java
@@ -23,7 +23,10 @@ import org.junit.jupiter.api.Test;
 
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 
+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;
 
@@ -59,4 +62,36 @@ class MCPToolClarificationPolicyTest extends AbstractMCPToolSpecificationFactory
                 "status", "clarifying",
                 MCPPayloadFieldNames.CLARIFICATION_QUESTIONS, List.of())));
     }
+    
+    @Test
+    void assertCreateClarificationForm() {
+        Optional<MCPToolClarificationPolicy.ClarificationForm> actual = policy.createClarificationForm(createClarifyingPayload(),
+                createPlanningToolDescriptor("database_gateway_plan_encrypt_rule"));
+        assertTrue(actual.isPresent());
+        assertThat(actual.get().planId(), is("plan-1"));
+        assertThat(actual.get().requestedSchema(), is(createExpectedElicitRequestedSchema()));
+    }
+    
+    @Test
+    void assertCreateClarificationFormWithoutPlanId() {
+        Optional<MCPToolClarificationPolicy.ClarificationForm> actual = policy.createClarificationForm(createClarifyingPayloadWithoutPlanId(),
+                createPlanningToolDescriptor("database_gateway_plan_encrypt_rule"));
+        assertTrue(actual.isEmpty());
+    }
+    
+    @Test
+    void assertCreateClarificationFormWithSensitiveQuestion() {
+        Map<String, Object> payload = createClarifyingPayload(createClarifyingQuestion("custom_properties.access-token", "string", false, "Provide access token."));
+        Optional<MCPToolClarificationPolicy.ClarificationForm> actual = policy.createClarificationForm(payload,
+                createPlanningToolDescriptor("database_gateway_plan_encrypt_rule"));
+        assertTrue(actual.isEmpty());
+    }
+    
+    @Test
+    void assertCreateClarificationFormWithAmbiguousFieldBinding() {
+        Map<String, Object> payload = createClarifyingPayload(createClarifyingQuestion("requires_review", "boolean", false, "Require review?"));
+        Optional<MCPToolClarificationPolicy.ClarificationForm> actual = policy.createClarificationForm(payload,
+                createAmbiguousPlanningToolDescriptor("database_gateway_plan_encrypt_rule"));
+        assertTrue(actual.isEmpty());
+    }
 }
diff --git a/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPToolElicitationFallbackReasonTest.java b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPToolElicitationFallbackReasonTest.java
new file mode 100644
index 00000000000..4d5f3e750d1
--- /dev/null
+++ b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPToolElicitationFallbackReasonTest.java
@@ -0,0 +1,88 @@
+/*
+ * 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.server.McpSyncServerExchange;
+import io.modelcontextprotocol.spec.McpSchema;
+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 java.util.Collections;
+import java.util.stream.Stream;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class MCPToolElicitationFallbackReasonTest {
+    
+    @ParameterizedTest(name = "{0}")
+    @MethodSource("getFallbackReasons")
+    void assertGetters(final MCPToolElicitationFallbackReason fallbackReason, final String expectedValue, final String expectedSelectedInteraction) {
+        assertThat(fallbackReason.getValue(), is(expectedValue));
+        assertThat(fallbackReason.getSelectedInteraction(), is(expectedSelectedInteraction));
+    }
+    
+    @Test
+    void assertWithClientCapabilitiesUseUrlFallback() {
+        MCPToolElicitationFallbackReason actual = MCPToolElicitationFallbackReason.SENSITIVE_FORM_BLOCKED.withClientCapabilities(createClientCapabilities(createFormAndUrlClientCapabilities()));
+        assertThat(actual, is(MCPToolElicitationFallbackReason.URL_MODE_NOT_IMPLEMENTED));
+    }
+    
+    @Test
+    void assertWithClientCapabilitiesKeepSensitiveFallback() {
+        MCPToolElicitationFallbackReason actual =
+                MCPToolElicitationFallbackReason.SENSITIVE_FORM_BLOCKED.withClientCapabilities(createClientCapabilities(McpSchema.ClientCapabilities.builder().elicitation().build()));
+        assertThat(actual, is(MCPToolElicitationFallbackReason.SENSITIVE_FORM_BLOCKED));
+    }
+    
+    @Test
+    void assertWithClientCapabilitiesKeepStructuredFallback() {
+        MCPToolElicitationFallbackReason actual = MCPToolElicitationFallbackReason.AMBIGUOUS_FIELD_BINDING.withClientCapabilities(createClientCapabilities(createFormAndUrlClientCapabilities()));
+        assertThat(actual, is(MCPToolElicitationFallbackReason.AMBIGUOUS_FIELD_BINDING));
+    }
+    
+    private static Stream<Arguments> getFallbackReasons() {
+        return Stream.of(
+                Arguments.of(MCPToolElicitationFallbackReason.CLIENT_UNSUPPORTED, "client_unsupported", "structured_fallback"),
+                Arguments.of(MCPToolElicitationFallbackReason.REMOTE_IDENTITY_REQUIRED, "remote_identity_required", "structured_fallback"),
+                Arguments.of(MCPToolElicitationFallbackReason.MISSING_PLAN_ID, "missing_plan_id", "structured_fallback"),
+                Arguments.of(MCPToolElicitationFallbackReason.SENSITIVE_FORM_BLOCKED, "sensitive_form_blocked", "url_fallback"),
+                Arguments.of(MCPToolElicitationFallbackReason.URL_MODE_NOT_IMPLEMENTED, "url_mode_not_implemented", "url_fallback"),
+                Arguments.of(MCPToolElicitationFallbackReason.AMBIGUOUS_FIELD_BINDING, "ambiguous_field_binding", "structured_fallback"),
+                Arguments.of(MCPToolElicitationFallbackReason.ELICITATION_FAILED, "elicitation_failed", "structured_fallback"),
+                Arguments.of(MCPToolElicitationFallbackReason.MALFORMED_ELICITATION_RESULT, "malformed_elicitation_result", "structured_fallback"),
+                Arguments.of(MCPToolElicitationFallbackReason.INVALID_ELICITED_CONTENT, "invalid_elicited_content", "structured_fallback"),
+                Arguments.of(MCPToolElicitationFallbackReason.STALE_ELICITATION, "stale_elicitation", "structured_fallback"));
+    }
+    
+    private MCPClientElicitationCapabilities createClientCapabilities(final McpSchema.ClientCapabilities clientCapabilities) {
+        McpSyncServerExchange exchange = mock(McpSyncServerExchange.class);
+        when(exchange.getClientCapabilities()).thenReturn(clientCapabilities);
+        return MCPClientElicitationCapabilities.from(exchange);
+    }
+    
+    private McpSchema.ClientCapabilities createFormAndUrlClientCapabilities() {
+        return new McpSchema.ClientCapabilities(
+                Collections.emptyMap(), null, null,
+                new McpSchema.ClientCapabilities.Elicitation(new McpSchema.ClientCapabilities.Elicitation.Form(), new McpSchema.ClientCapabilities.Elicitation.Url()));
+    }
+}
diff --git a/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPToolElicitationFallbackResponseFactoryTest.java b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPToolElicitationFallbackResponseFactoryTest.java
new file mode 100644
index 00000000000..17c015a79aa
--- /dev/null
+++ b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPToolElicitationFallbackResponseFactoryTest.java
@@ -0,0 +1,78 @@
+/*
+ * 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.server.McpSyncServerExchange;
+import io.modelcontextprotocol.spec.McpSchema;
+import org.apache.shardingsphere.mcp.api.protocol.response.MCPResponse;
+import org.apache.shardingsphere.mcp.support.protocol.MCPPayloadFieldNames;
+import org.apache.shardingsphere.mcp.support.workflow.model.WorkflowFieldNames;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class MCPToolElicitationFallbackResponseFactoryTest extends AbstractMCPToolSpecificationFactoryTest {
+    
+    private final MCPToolElicitationFallbackResponseFactory factory = new MCPToolElicitationFallbackResponseFactory();
+    
+    @Test
+    void assertCreateStructuredFallback() {
+        MCPResponse actual = factory.create(Map.of(WorkflowFieldNames.PLAN_ID, "plan-1", "status", "clarifying"),
+                MCPToolElicitationFallbackReason.AMBIGUOUS_FIELD_BINDING, createClientCapabilities(McpSchema.ClientCapabilities.builder().elicitation().build()));
+        Map<String, Object> actualPayload = actual.toPayload();
+        assertThat(actualPayload.get("fallback_reason"), is("ambiguous_field_binding"));
+        Map<?, ?> actualSupport = (Map<?, ?>) actualPayload.get("elicitation_support");
+        assertTrue((Boolean) actualSupport.get("form_mode"));
+        assertFalse((Boolean) actualSupport.get("url_mode"));
+        assertThat(actualSupport.get("selected_interaction"), is("structured_fallback"));
+        assertFalse(actualPayload.containsKey(MCPPayloadFieldNames.NEXT_ACTIONS));
+    }
+    
+    @Test
+    void assertCreateSensitiveFallback() {
+        MCPResponse actual = factory.create(createClarifyingPayload(createClarifyingQuestion("primary_algorithm_properties.access-token", "string", false, "Provide access token.")),
+                MCPToolElicitationFallbackReason.SENSITIVE_FORM_BLOCKED, createClientCapabilities(McpSchema.ClientCapabilities.builder().elicitation().build()));
+        Map<String, Object> actualPayload = actual.toPayload();
+        assertThat(actualPayload.get("fallback_reason"), is("sensitive_form_blocked"));
+        assertSensitiveFallback(actualPayload);
+    }
+    
+    private void assertSensitiveFallback(final Map<String, Object> actualPayload) {
+        Map<?, ?> actualQuestion = (Map<?, ?>) ((List<?>) actualPayload.get(MCPPayloadFieldNames.CLARIFICATION_QUESTIONS)).get(0);
+        assertThat(actualQuestion.get(MCPPayloadFieldNames.INPUT_TYPE), is("secret"));
+        assertTrue((boolean) actualQuestion.get(MCPPayloadFieldNames.SECRET));
+        assertThat(actualQuestion.get(MCPPayloadFieldNames.MESSAGE), is("Sensitive input must be provided through configured secure channels before continuing the same planner."));
+        assertFalse(actualQuestion.containsKey(MCPPayloadFieldNames.DISPLAY_MESSAGE));
+        assertThat(((Map<?, ?>) ((List<?>) actualPayload.get(MCPPayloadFieldNames.NEXT_ACTIONS)).get(0)).get("type"), is("terminal"));
+        assertFalse(String.valueOf(actualPayload).contains("Provide access token."));
+    }
+    
+    private MCPClientElicitationCapabilities createClientCapabilities(final McpSchema.ClientCapabilities clientCapabilities) {
+        McpSyncServerExchange exchange = mock(McpSyncServerExchange.class);
+        when(exchange.getClientCapabilities()).thenReturn(clientCapabilities);
+        return MCPClientElicitationCapabilities.from(exchange);
+    }
+}
diff --git a/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPToolSpecificationElicitationFactoryTest.java b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPToolSpecificationElicitationFactoryTest.java
deleted file mode 100644
index 92dd1e233cf..00000000000
--- a/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPToolSpecificationElicitationFactoryTest.java
+++ /dev/null
@@ -1,397 +0,0 @@
-/*
- * 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.server.McpServerFeatures.SyncToolSpecification;
-import io.modelcontextprotocol.server.McpSyncServerExchange;
-import io.modelcontextprotocol.spec.McpSchema;
-import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
-import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
-import org.apache.shardingsphere.mcp.api.protocol.response.MCPResponse;
-import org.apache.shardingsphere.mcp.core.context.MCPRequestScope;
-import org.apache.shardingsphere.mcp.core.context.MCPRuntimeContext;
-import org.apache.shardingsphere.mcp.core.tool.handler.MCPToolDefinition;
-import org.apache.shardingsphere.mcp.core.tool.handler.ToolDefinitionRegistry;
-import org.apache.shardingsphere.mcp.support.descriptor.MCPShardingSphereMetadataKeys;
-import org.apache.shardingsphere.mcp.support.protocol.response.MCPMapResponse;
-import org.junit.jupiter.api.Test;
-import org.mockito.ArgumentCaptor;
-import org.mockito.MockedStatic;
-
-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.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 MCPToolSpecificationElicitationFactoryTest extends AbstractMCPToolSpecificationFactoryTest {
-    
-    @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);
-            SyncToolSpecification actualSpecification = new MCPToolSpecificationFactory(createRuntimeContext("stdio")).createToolSpecifications().get(0);
-            McpSyncServerExchange exchange = createElicitationExchange(new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT,
-                    Map.of("field_1", "foo_display", "field_2", true)), clientCapabilities);
-            CallToolResult actual = actualSpecification.callHandler().apply(exchange, new CallToolRequest(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);
-            SyncToolSpecification actualSpecification = new MCPToolSpecificationFactory(createRuntimeContext("stdio")).createToolSpecifications().get(0);
-            McpSyncServerExchange exchange = createElicitationExchange(new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT, Map.of()), createFormAndUrlClientCapabilities());
-            CallToolResult actual = actualSpecification.callHandler().apply(exchange, new CallToolRequest(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);
-            SyncToolSpecification actualSpecification = new MCPToolSpecificationFactory(createRuntimeContext("stdio")).createToolSpecifications().get(0);
-            McpSyncServerExchange exchange = createElicitationExchange(new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT, Map.of("field_1", true)));
-            CallToolResult actual = actualSpecification.callHandler().apply(exchange, new CallToolRequest(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);
-            SyncToolSpecification actualSpecification = new MCPToolSpecificationFactory(createRuntimeContext("stdio")).createToolSpecifications().get(0);
-            McpSyncServerExchange exchange = createUrlOnlyElicitationExchange();
-            CallToolResult actual = actualSpecification.callHandler().apply(exchange, new CallToolRequest(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);
-            SyncToolSpecification actualSpecification = new MCPToolSpecificationFactory(createRuntimeContext("http")).createToolSpecifications().get(0);
-            McpSyncServerExchange exchange = createElicitationExchange(new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT, Map.of("field_1", "foo_display")));
-            CallToolResult actual = actualSpecification.callHandler().apply(exchange, new CallToolRequest(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);
-            SyncToolSpecification actualSpecification = new MCPToolSpecificationFactory(createRuntimeContext("stdio")).createToolSpecifications().get(0);
-            McpSyncServerExchange exchange = createElicitationExchange(new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT, Map.of("custom_properties.display-name", "foo_display")));
-            CallToolResult actual = actualSpecification.callHandler().apply(exchange, new CallToolRequest(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);
-            SyncToolSpecification actualSpecification = new MCPToolSpecificationFactory(createRuntimeContext("stdio")).createToolSpecifications().get(0);
-            McpSyncServerExchange exchange = createElicitationExchange(new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT, Map.of("field_1", "foo_display")));
-            CallToolResult actual = actualSpecification.callHandler().apply(exchange, new CallToolRequest(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());
-            SyncToolSpecification actualSpecification = new MCPToolSpecificationFactory(runtimeContext).createToolSpecifications().get(0);
-            McpSyncServerExchange exchange = createElicitationExchange(new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT, Map.of()));
-            CallToolResult actual = actualSpecification.callHandler().apply(exchange, new CallToolRequest("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());
-            SyncToolSpecification actualSpecification = new MCPToolSpecificationFactory(runtimeContext).createToolSpecifications().get(0);
-            McpSyncServerExchange exchange = createElicitationExchange(new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT, Map.of()));
-            CallToolResult actual = actualSpecification.callHandler().apply(exchange, new CallToolRequest("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);
-            SyncToolSpecification actualSpecification = new MCPToolSpecificationFactory(createRuntimeContext("stdio")).createToolSpecifications().get(0);
-            McpSyncServerExchange exchange = mock(McpSyncServerExchange.class);
-            when(exchange.sessionId()).thenReturn("session-id");
-            CallToolResult actual = actualSpecification.callHandler().apply(exchange, new CallToolRequest("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);
-            SyncToolSpecification actualSpecification = new MCPToolSpecificationFactory(createRuntimeContext("stdio")).createToolSpecifications().get(0);
-            McpSyncServerExchange exchange = createThrowingElicitationExchange();
-            CallToolResult actual = actualSpecification.callHandler().apply(exchange, new CallToolRequest("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);
-            SyncToolSpecification actualSpecification = new MCPToolSpecificationFactory(createRuntimeContext("stdio")).createToolSpecifications().get(0);
-            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 = actualSpecification.callHandler().apply(exchange, new CallToolRequest("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);
-            SyncToolSpecification actualSpecification = new MCPToolSpecificationFactory(createRuntimeContext("stdio")).createToolSpecifications().get(0);
-            McpSyncServerExchange exchange = createElicitationExchange(new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT, elicitedContent));
-            CallToolResult actual = actualSpecification.callHandler().apply(exchange, new CallToolRequest("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);
-            SyncToolSpecification actualSpecification = new MCPToolSpecificationFactory(createRuntimeContext("stdio")).createToolSpecifications().get(0);
-            McpSyncServerExchange exchange = createElicitationExchange(elicitedResult);
-            CallToolResult actual = actualSpecification.callHandler().apply(exchange, new CallToolRequest("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 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);
-            SyncToolSpecification actualSpecification = new MCPToolSpecificationFactory(createRuntimeContext("stdio")).createToolSpecifications().get(0);
-            McpSyncServerExchange exchange = createElicitationExchange(new McpSchema.ElicitResult(action, Map.of()));
-            CallToolResult actual = actualSpecification.callHandler().apply(exchange, new CallToolRequest("database_gateway_plan_encrypt_rule", Map.of()));
-            assertThat(actual.structuredContent(), is(expectedPayload));
-            verify(exchange).createElicitation(any());
-        }
-    }
-}
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 fd59a5049c3..3e75d1e7d9d 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
@@ -31,6 +31,7 @@ import io.modelcontextprotocol.spec.McpSchema.TextContent;
 import org.apache.shardingsphere.mcp.api.protocol.response.MCPResponse;
 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;
@@ -40,8 +41,11 @@ import org.apache.shardingsphere.mcp.support.descriptor.MCPShardingSphereMetadat
 import org.apache.shardingsphere.mcp.support.protocol.MCPResourceHintUtils;
 import org.apache.shardingsphere.mcp.support.protocol.response.MCPMapResponse;
 import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
 import org.mockito.MockedStatic;
 
+import java.time.Clock;
+import java.time.Duration;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
@@ -54,9 +58,13 @@ 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 {
@@ -282,6 +290,362 @@ class MCPToolSpecificationFactoryTest extends AbstractMCPToolSpecificationFactor
         }
     }
     
+    @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);
+            SyncToolSpecification actualSpecification = new MCPToolSpecificationFactory(createRuntimeContext("stdio")).createToolSpecifications().get(0);
+            McpSyncServerExchange exchange = createElicitationExchange(new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT,
+                    Map.of("field_1", "foo_display", "field_2", true)), clientCapabilities);
+            CallToolResult actual = actualSpecification.callHandler().apply(exchange, new CallToolRequest(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);
+            SyncToolSpecification actualSpecification = new MCPToolSpecificationFactory(createRuntimeContext("stdio")).createToolSpecifications().get(0);
+            McpSyncServerExchange exchange = createElicitationExchange(new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT, Map.of()), createFormAndUrlClientCapabilities());
+            CallToolResult actual = actualSpecification.callHandler().apply(exchange, new CallToolRequest(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);
+            SyncToolSpecification actualSpecification = new MCPToolSpecificationFactory(createRuntimeContext("stdio")).createToolSpecifications().get(0);
+            McpSyncServerExchange exchange = createElicitationExchange(new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT, Map.of("field_1", true)));
+            CallToolResult actual = actualSpecification.callHandler().apply(exchange, new CallToolRequest(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);
+            SyncToolSpecification actualSpecification = new MCPToolSpecificationFactory(createRuntimeContext("stdio")).createToolSpecifications().get(0);
+            McpSyncServerExchange exchange = createUrlOnlyElicitationExchange();
+            CallToolResult actual = actualSpecification.callHandler().apply(exchange, new CallToolRequest(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);
+            SyncToolSpecification actualSpecification = new MCPToolSpecificationFactory(createRuntimeContext("http")).createToolSpecifications().get(0);
+            McpSyncServerExchange exchange = createElicitationExchange(new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT, Map.of("field_1", "foo_display")));
+            CallToolResult actual = actualSpecification.callHandler().apply(exchange, new CallToolRequest(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);
+            SyncToolSpecification actualSpecification = new MCPToolSpecificationFactory(createRuntimeContext("stdio")).createToolSpecifications().get(0);
+            McpSyncServerExchange exchange = createElicitationExchange(new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT, Map.of("custom_properties.display-name", "foo_display")));
+            CallToolResult actual = actualSpecification.callHandler().apply(exchange, new CallToolRequest(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);
+            SyncToolSpecification actualSpecification = new MCPToolSpecificationFactory(createRuntimeContext("stdio")).createToolSpecifications().get(0);
+            McpSyncServerExchange exchange = createElicitationExchange(new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT, Map.of("field_1", "foo_display")));
+            CallToolResult actual = actualSpecification.callHandler().apply(exchange, new CallToolRequest(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());
+            SyncToolSpecification actualSpecification = new MCPToolSpecificationFactory(runtimeContext).createToolSpecifications().get(0);
+            McpSyncServerExchange exchange = createElicitationExchange(new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT, Map.of()));
+            CallToolResult actual = actualSpecification.callHandler().apply(exchange, new CallToolRequest("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());
+            SyncToolSpecification actualSpecification = new MCPToolSpecificationFactory(runtimeContext).createToolSpecifications().get(0);
+            McpSyncServerExchange exchange = createElicitationExchange(new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT, Map.of()));
+            CallToolResult actual = actualSpecification.callHandler().apply(exchange, new CallToolRequest("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);
+            SyncToolSpecification actualSpecification = new MCPToolSpecificationFactory(createRuntimeContext("stdio")).createToolSpecifications().get(0);
+            McpSyncServerExchange exchange = mock(McpSyncServerExchange.class);
+            when(exchange.sessionId()).thenReturn("session-id");
+            CallToolResult actual = actualSpecification.callHandler().apply(exchange, new CallToolRequest("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);
+            SyncToolSpecification actualSpecification = new MCPToolSpecificationFactory(createRuntimeContext("stdio")).createToolSpecifications().get(0);
+            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 = actualSpecification.callHandler().apply(exchange, new CallToolRequest("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);
+            SyncToolSpecification actualSpecification = new MCPToolSpecificationFactory(createRuntimeContext("stdio")).createToolSpecifications().get(0);
+            McpSyncServerExchange exchange = createThrowingElicitationExchange();
+            CallToolResult actual = actualSpecification.callHandler().apply(exchange, new CallToolRequest("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);
+            SyncToolSpecification actualSpecification = new MCPToolSpecificationFactory(createRuntimeContext("stdio")).createToolSpecifications().get(0);
+            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 = actualSpecification.callHandler().apply(exchange, new CallToolRequest("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);
+            SyncToolSpecification actualSpecification = new MCPToolSpecificationFactory(createRuntimeContext("stdio")).createToolSpecifications().get(0);
+            McpSyncServerExchange exchange = createElicitationExchange(new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT, elicitedContent));
+            CallToolResult actual = actualSpecification.callHandler().apply(exchange, new CallToolRequest("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);
+            SyncToolSpecification actualSpecification = new MCPToolSpecificationFactory(createRuntimeContext("stdio")).createToolSpecifications().get(0);
+            McpSyncServerExchange exchange = createElicitationExchange(elicitedResult);
+            CallToolResult actual = actualSpecification.callHandler().apply(exchange, new CallToolRequest("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 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);
+            SyncToolSpecification actualSpecification = new MCPToolSpecificationFactory(createRuntimeContext("stdio")).createToolSpecifications().get(0);
+            McpSyncServerExchange exchange = createElicitationExchange(new McpSchema.ElicitResult(action, Map.of()));
+            CallToolResult actual = actualSpecification.callHandler().apply(exchange, new CallToolRequest("database_gateway_plan_encrypt_rule", Map.of()));
+            assertThat(actual.structuredContent(), is(expectedPayload));
+            verify(exchange).createElicitation(any());
+        }
+    }
+    
     @Test
     void assertToolOutputSchemaExamplesMatchSchemas() {
         JsonSchemaValidator validator = new DefaultJsonSchemaValidator();