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 35a1ce7295f Improve MCP discovery bridge and fallback contracts 
(#39011)
35a1ce7295f is described below

commit 35a1ce7295fcba0d3bf882c2cedec210b354b038
Author: Liang Zhang <[email protected]>
AuthorDate: Sun Jul 5 21:49:45 2026 +0800

    Improve MCP discovery bridge and fallback contracts (#39011)
    
    * Refactor BootstrapInitializerTest
    
    * Improve MCP discovery bridge and fallback contracts
    
    - Add LLM bridge actions for tools/list and resources/templates/list
    - Preserve JSON-RPC error.data recovery payloads in MCP E2E helpers
    - Use structured_fallback for unimplemented URL elicitation fallback
    - Update MCP descriptors and protocol-contract scenario coverage
---
 .../tool/MCPToolElicitationFallbackReason.java     |  4 +--
 .../tool/MCPToolElicitationFallbackReasonTest.java |  6 ++---
 .../tool/MCPToolElicitationFlowTest.java           |  8 +++---
 .../mcp-descriptors/mcp-descriptor-encrypt.yaml    |  1 -
 .../mcp-descriptors/mcp-descriptor-mask.yaml       |  1 -
 .../mcp/llm/conversation/LLMMCPActionExecutor.java |  6 +++++
 .../llm/conversation/LLMMCPActionExecutorTest.java | 18 +++++++++++--
 .../conversation/LLMMCPConversationRunnerTest.java | 31 +++++++++++++++-------
 .../LLMMCPConversationTurnPlanner.java             |  2 ++
 .../LLMMCPConversationTurnPlannerTest.java         | 11 ++++++++
 .../conversation/LLMMCPToolDefinitionFactory.java  | 20 ++++++++++++++
 .../LLMMCPToolDefinitionFactoryTest.java           |  4 +++
 .../llm/conversation/LLMMCPTraceRecordFactory.java |  8 ++++++
 .../scenario/LLMUsabilityScenarioCatalog.java      | 10 ++++---
 .../scenario/LLMUsabilityScenarioCatalogTest.java  | 11 ++++++--
 .../transport/MCPInteractionActionNames.java       |  8 ++++++
 .../support/transport/MCPInteractionPayloads.java  | 10 ++++---
 .../transport/MCPInteractionPayloadsTest.java      | 14 ++++++++++
 18 files changed, 142 insertions(+), 31 deletions(-)

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
index 7a80a06a190..fa5e2488ab5 100644
--- 
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
@@ -33,9 +33,9 @@ public enum MCPToolElicitationFallbackReason {
     
     MISSING_PLAN_ID("missing_plan_id", "structured_fallback"),
     
-    SENSITIVE_FORM_BLOCKED("sensitive_form_blocked", "url_fallback"),
+    SENSITIVE_FORM_BLOCKED("sensitive_form_blocked", "structured_fallback"),
     
-    URL_MODE_NOT_IMPLEMENTED("url_mode_not_implemented", "url_fallback"),
+    URL_MODE_NOT_IMPLEMENTED("url_mode_not_implemented", 
"structured_fallback"),
     
     AMBIGUOUS_FIELD_BINDING("ambiguous_field_binding", "structured_fallback"),
     
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
index 4d5f3e750d1..95faa8f1e5d 100644
--- 
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
@@ -42,7 +42,7 @@ class MCPToolElicitationFallbackReasonTest {
     }
     
     @Test
-    void assertWithClientCapabilitiesUseUrlFallback() {
+    void assertWithClientCapabilitiesUseUrlModeNotImplementedFallback() {
         MCPToolElicitationFallbackReason actual = 
MCPToolElicitationFallbackReason.SENSITIVE_FORM_BLOCKED.withClientCapabilities(createClientCapabilities(createFormAndUrlClientCapabilities()));
         assertThat(actual, 
is(MCPToolElicitationFallbackReason.URL_MODE_NOT_IMPLEMENTED));
     }
@@ -65,8 +65,8 @@ class MCPToolElicitationFallbackReasonTest {
                 
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.SENSITIVE_FORM_BLOCKED, 
"sensitive_form_blocked", "structured_fallback"),
+                
Arguments.of(MCPToolElicitationFallbackReason.URL_MODE_NOT_IMPLEMENTED, 
"url_mode_not_implemented", "structured_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"),
diff --git 
a/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPToolElicitationFlowTest.java
 
b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPToolElicitationFlowTest.java
index 250484461bb..117ccd9d91a 100644
--- 
a/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPToolElicitationFlowTest.java
+++ 
b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPToolElicitationFlowTest.java
@@ -113,7 +113,7 @@ class MCPToolElicitationFlowTest extends 
AbstractMCPToolSpecificationFactoryTest
             mockToolDispatch(mockedToolDefinitionRegistry, toolDefinition, 
Map.of(), response);
             McpSyncServerExchange exchange = createElicitationExchange(new 
McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT, Map.of()), 
createFormAndUrlClientCapabilities());
             CallToolResult actual = callTool(createToolSpecification("stdio"), 
exchange, toolName, Map.of());
-            assertStructuredFallback(actual, "url_mode_not_implemented", true, 
true, "url_fallback");
+            assertStructuredFallback(actual, "url_mode_not_implemented", true, 
true, "structured_fallback");
             assertSanitizedSensitiveFallback(actual);
             verify(exchange, never()).createElicitation(any());
         }
@@ -133,7 +133,7 @@ class MCPToolElicitationFlowTest extends 
AbstractMCPToolSpecificationFactoryTest
                         "field", "primary_algorithm_properties.props",
                         "input_type", "string",
                         "display_message", "Provide props."))),
-                "sensitive_form_blocked", "url_fallback");
+                "sensitive_form_blocked", "structured_fallback");
     }
     
     @Test
@@ -189,7 +189,7 @@ class MCPToolElicitationFlowTest extends 
AbstractMCPToolSpecificationFactoryTest
             mockToolDispatch(mockedToolDefinitionRegistry, toolDefinition, 
Map.of(), response);
             McpSyncServerExchange exchange = createElicitationExchange(new 
McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT, 
Map.of("custom_properties.display-name", "foo_display")));
             CallToolResult actual = callTool(createToolSpecification("stdio"), 
exchange, toolName, Map.of());
-            assertStructuredFallback(actual, "sensitive_form_blocked", true, 
false, "url_fallback");
+            assertStructuredFallback(actual, "sensitive_form_blocked", true, 
false, "structured_fallback");
             assertSanitizedSensitiveFallback(actual);
             verify(exchange, never()).createElicitation(any());
         }
@@ -205,7 +205,7 @@ class MCPToolElicitationFlowTest extends 
AbstractMCPToolSpecificationFactoryTest
             McpSyncServerExchange exchange = createElicitationExchange(new 
McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT, Map.of("field_1", 
"foo_display")));
             CallToolResult actual = callTool(createToolSpecification("stdio"), 
exchange, toolName, Map.of());
             assertStructuredFallback(actual, expectedReason, true, false, 
expectedInteraction);
-            if ("url_fallback".equals(expectedInteraction)) {
+            if ("sensitive_form_blocked".equals(expectedReason) || 
"url_mode_not_implemented".equals(expectedReason)) {
                 assertSanitizedSensitiveFallback(actual);
             }
             verify(exchange, never()).createElicitation(any());
diff --git 
a/mcp/features/encrypt/src/main/resources/META-INF/shardingsphere-mcp/mcp-descriptors/mcp-descriptor-encrypt.yaml
 
b/mcp/features/encrypt/src/main/resources/META-INF/shardingsphere-mcp/mcp-descriptors/mcp-descriptor-encrypt.yaml
index 7eb8b1c1129..36338f2b9ea 100644
--- 
a/mcp/features/encrypt/src/main/resources/META-INF/shardingsphere-mcp/mcp-descriptors/mcp-descriptor-encrypt.yaml
+++ 
b/mcp/features/encrypt/src/main/resources/META-INF/shardingsphere-mcp/mcp-descriptors/mcp-descriptor-encrypt.yaml
@@ -304,7 +304,6 @@ tools:
               description: "Fallback interaction selected by the server."
               enum:
                 - structured_fallback
-                - url_fallback
         fallback_reason:
           type: string
           description: "Machine-readable reason explaining why the planner 
returned structured fallback instead of automatic form continuation."
diff --git 
a/mcp/features/mask/src/main/resources/META-INF/shardingsphere-mcp/mcp-descriptors/mcp-descriptor-mask.yaml
 
b/mcp/features/mask/src/main/resources/META-INF/shardingsphere-mcp/mcp-descriptors/mcp-descriptor-mask.yaml
index 5cd698f7ba7..e082b3571f8 100644
--- 
a/mcp/features/mask/src/main/resources/META-INF/shardingsphere-mcp/mcp-descriptors/mcp-descriptor-mask.yaml
+++ 
b/mcp/features/mask/src/main/resources/META-INF/shardingsphere-mcp/mcp-descriptors/mcp-descriptor-mask.yaml
@@ -262,7 +262,6 @@ tools:
               description: "Fallback interaction selected by the server."
               enum:
                 - structured_fallback
-                - url_fallback
         fallback_reason:
           type: string
           description: "Machine-readable reason explaining why the planner 
returned structured fallback instead of automatic form continuation."
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPActionExecutor.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPActionExecutor.java
index e6fcb84f2c4..89866c29ac3 100644
--- 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPActionExecutor.java
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPActionExecutor.java
@@ -41,9 +41,15 @@ final class LLMMCPActionExecutor {
     }
     
     private Map<String, Object> execute(final String actionName, final 
Map<String, Object> args) throws IOException, InterruptedException {
+        if (MCPInteractionActionNames.LIST_TOOLS.equals(actionName)) {
+            return Map.of("tools", mcpInteractionClient.listTools());
+        }
         if (MCPInteractionActionNames.LIST_RESOURCES.equals(actionName)) {
             return mcpInteractionClient.listResources();
         }
+        if 
(MCPInteractionActionNames.LIST_RESOURCE_TEMPLATES.equals(actionName)) {
+            return mcpInteractionClient.listResourceTemplates();
+        }
         if (MCPInteractionActionNames.READ_RESOURCE.equals(actionName)) {
             return readResource(args);
         }
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPActionExecutorTest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPActionExecutorTest.java
index d5269b67e27..583536e0c87 100644
--- 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPActionExecutorTest.java
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPActionExecutorTest.java
@@ -32,12 +32,26 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
 
 class LLMMCPActionExecutorTest {
     
+    @Test
+    void assertExecuteSafelyWithListTools() throws InterruptedException {
+        FakeMCPInteractionClient client = new FakeMCPInteractionClient();
+        assertThat(new 
LLMMCPActionExecutor(client).executeSafely(MCPInteractionActionNames.LIST_TOOLS,
 Map.of()),
+                is(Map.of("tools", List.of(Map.of("name", "fixture_ping")))));
+    }
+    
     @Test
     void assertExecuteSafelyWithListResources() throws InterruptedException {
         FakeMCPInteractionClient client = new FakeMCPInteractionClient();
         assertThat(new 
LLMMCPActionExecutor(client).executeSafely(MCPInteractionActionNames.LIST_RESOURCES,
 Map.of()), is(Map.of("resources", List.of())));
     }
     
+    @Test
+    void assertExecuteSafelyWithListResourceTemplates() throws 
InterruptedException {
+        FakeMCPInteractionClient client = new FakeMCPInteractionClient();
+        assertThat(new 
LLMMCPActionExecutor(client).executeSafely(MCPInteractionActionNames.LIST_RESOURCE_TEMPLATES,
 Map.of()),
+                is(Map.of("resourceTemplates", List.of(Map.of("uriTemplate", 
"shardingsphere://databases/{database}")))));
+    }
+    
     @Test
     void assertExecuteSafelyWithReadResource() throws InterruptedException {
         FakeMCPInteractionClient client = new FakeMCPInteractionClient();
@@ -180,7 +194,7 @@ class LLMMCPActionExecutorTest {
         
         @Override
         public List<Map<String, Object>> listTools() {
-            throw new UnsupportedOperationException("tools/list is not 
supported.");
+            return List.of(Map.of("name", "fixture_ping"));
         }
         
         @Override
@@ -193,7 +207,7 @@ class LLMMCPActionExecutorTest {
         
         @Override
         public Map<String, Object> listResourceTemplates() {
-            throw new UnsupportedOperationException("resources/templates/list 
is not supported.");
+            return Map.of("resourceTemplates", List.of(Map.of("uriTemplate", 
"shardingsphere://databases/{database}")));
         }
         
         @Override
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPConversationRunnerTest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPConversationRunnerTest.java
index d7bb729ca18..bb07bb06201 100644
--- 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPConversationRunnerTest.java
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPConversationRunnerTest.java
@@ -174,17 +174,26 @@ class LLMMCPConversationRunnerTest extends 
AbstractLLMMCPConversationRunnerTest
     
     @Test
     void assertRunWithResourceBridgeSequence() throws IOException, 
InterruptedException {
-        List<String> actualToolNames = 
List.of(MCPInteractionActionNames.LIST_RESOURCES, 
MCPInteractionActionNames.READ_RESOURCE, "database_gateway_execute_query");
+        List<String> actualToolNames = List.of(
+                MCPInteractionActionNames.LIST_TOOLS,
+                MCPInteractionActionNames.LIST_RESOURCES,
+                MCPInteractionActionNames.LIST_RESOURCE_TEMPLATES,
+                MCPInteractionActionNames.READ_RESOURCE,
+                "database_gateway_execute_query");
         LLME2EScenario actualScenario = createScenario(actualToolNames);
         LLMMCPConversationRunner actualRunner = createRunner(3);
         Map<String, Object> executeQueryArguments = 
createExecuteQueryArguments(QUERY);
         when(getLLMChatClient().complete(anyList(), anyList(), eq("required"), 
eq(false))).thenReturn(new LLMChatCompletion("",
                 List.of(
-                        new LLMToolCall("tool-1", 
MCPInteractionActionNames.LIST_RESOURCES, "{}"),
-                        new LLMToolCall("tool-2", 
MCPInteractionActionNames.READ_RESOURCE, JsonUtils.toJsonString(Map.of("uri", 
RESOURCE_URI))),
-                        new LLMToolCall("tool-3", 
"database_gateway_execute_query", 
JsonUtils.toJsonString(executeQueryArguments))),
+                        new LLMToolCall("tool-1", 
MCPInteractionActionNames.LIST_TOOLS, "{}"),
+                        new LLMToolCall("tool-2", 
MCPInteractionActionNames.LIST_RESOURCES, "{}"),
+                        new LLMToolCall("tool-3", 
MCPInteractionActionNames.LIST_RESOURCE_TEMPLATES, "{}"),
+                        new LLMToolCall("tool-4", 
MCPInteractionActionNames.READ_RESOURCE, JsonUtils.toJsonString(Map.of("uri", 
RESOURCE_URI))),
+                        new LLMToolCall("tool-5", 
"database_gateway_execute_query", 
JsonUtils.toJsonString(executeQueryArguments))),
                 "tool-call-response"));
+        
when(getMCPInteractionClient().listTools()).thenReturn(List.of(Map.of("name", 
"database_gateway_execute_query")));
         
when(getMCPInteractionClient().listResources()).thenReturn(Map.of("resources", 
List.of(Map.of("uri", RESOURCE_URI))));
+        
when(getMCPInteractionClient().listResourceTemplates()).thenReturn(Map.of("resourceTemplates",
 List.of(Map.of("uriTemplate", "shardingsphere://databases/{database}"))));
         
when(getMCPInteractionClient().readResource(RESOURCE_URI)).thenReturn(Map.of("supportedTools",
 List.of("database_gateway_search_metadata", 
"database_gateway_execute_query")));
         when(getMCPInteractionClient().call("database_gateway_execute_query", 
executeQueryArguments)).thenReturn(createResultSetPayload("2"));
         when(getLLMChatClient().complete(anyList(), eq(List.of()), eq("none"), 
eq(true))).thenReturn(
@@ -193,13 +202,17 @@ class LLMMCPConversationRunnerTest extends 
AbstractLLMMCPConversationRunnerTest
         LLME2EArtifactBundle actual = actualRunner.run(actualScenario);
         
         assertTrue(actual.getAssertionReport().isSuccess());
-        assertThat(actual.getInteractionTrace().size(), is(3));
-        assertThat(actual.getInteractionTrace().getFirst().getActionKind(), 
is(MCPInteractionActionNames.RESOURCE_LIST_KIND));
+        assertThat(actual.getInteractionTrace().size(), is(5));
+        assertThat(actual.getInteractionTrace().getFirst().getActionKind(), 
is(MCPInteractionActionNames.TOOL_LIST_KIND));
         assertThat(actual.getInteractionTrace().getFirst().getActionOrigin(), 
is(MCPInteractionTraceRecord.PROTOCOL_BRIDGE_ORIGIN));
-        assertThat(actual.getInteractionTrace().get(1).getActionKind(), 
is(MCPInteractionActionNames.RESOURCE_READ_KIND));
-        assertThat(actual.getInteractionTrace().get(2).getTargetName(), 
is("database_gateway_execute_query"));
-        assertThat(actual.getInteractionTrace().get(2).getActionOrigin(), 
is(MCPInteractionTraceRecord.MODEL_TOOL_CALL_ORIGIN));
+        assertThat(actual.getInteractionTrace().get(1).getActionKind(), 
is(MCPInteractionActionNames.RESOURCE_LIST_KIND));
+        assertThat(actual.getInteractionTrace().get(2).getActionKind(), 
is(MCPInteractionActionNames.RESOURCE_TEMPLATE_LIST_KIND));
+        assertThat(actual.getInteractionTrace().get(3).getActionKind(), 
is(MCPInteractionActionNames.RESOURCE_READ_KIND));
+        assertThat(actual.getInteractionTrace().get(4).getTargetName(), 
is("database_gateway_execute_query"));
+        assertThat(actual.getInteractionTrace().get(4).getActionOrigin(), 
is(MCPInteractionTraceRecord.MODEL_TOOL_CALL_ORIGIN));
+        verify(getMCPInteractionClient()).listTools();
         verify(getMCPInteractionClient()).listResources();
+        verify(getMCPInteractionClient()).listResourceTemplates();
         verify(getMCPInteractionClient()).readResource(RESOURCE_URI);
         
verify(getMCPInteractionClient()).call("database_gateway_execute_query", 
executeQueryArguments);
     }
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPConversationTurnPlanner.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPConversationTurnPlanner.java
index e6a8c80b4d3..0bb9b4825c0 100644
--- 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPConversationTurnPlanner.java
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPConversationTurnPlanner.java
@@ -46,7 +46,9 @@ final class LLMMCPConversationTurnPlanner {
     
     private static Set<String> createReadOnlyToolNames() {
         Set<String> result = new LinkedHashSet<>();
+        result.add(MCPInteractionActionNames.LIST_TOOLS);
         result.add(MCPInteractionActionNames.LIST_RESOURCES);
+        result.add(MCPInteractionActionNames.LIST_RESOURCE_TEMPLATES);
         result.add(MCPInteractionActionNames.READ_RESOURCE);
         result.add(MCPInteractionActionNames.LIST_PROMPTS);
         result.add(MCPInteractionActionNames.GET_PROMPT);
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPConversationTurnPlannerTest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPConversationTurnPlannerTest.java
index 374e9e3ab4a..ab62d92a90c 100644
--- 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPConversationTurnPlannerTest.java
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPConversationTurnPlannerTest.java
@@ -76,6 +76,17 @@ class LLMMCPConversationTurnPlannerTest {
         assertThat(actual, 
is(List.of("database_gateway_validate_runtime_database")));
     }
     
+    @Test
+    void 
assertCreateTurnToolNamesPrefersDiscoveryBridgeAfterSideEffectNextAction() {
+        LLMMCPConversationInstructionFactory instructionFactory = new 
LLMMCPConversationInstructionFactory();
+        LLMMCPConversationTurnPlanner planner = new 
LLMMCPConversationTurnPlanner(instructionFactory);
+        Map<String, Object> nextAction = Map.of("type", "tool_call", 
"tool_name", "database_gateway_execute_update", "arguments", 
Map.of("execution_mode", "execute"));
+        List<String> actual = 
planner.createTurnToolNames(createScenario(List.of("database_gateway_execute_update",
 MCPInteractionActionNames.LIST_TOOLS),
+                List.of("database_gateway_execute_update", 
MCPInteractionActionNames.LIST_TOOLS)),
+                List.of(createTraceRecord("database_gateway_execute_update", 
Map.of("next_actions", List.of(nextAction)))));
+        assertThat(actual, is(List.of(MCPInteractionActionNames.LIST_TOOLS)));
+    }
+    
     @Test
     void assertCreateToolChoiceWithMissingCoverage() {
         LLMMCPConversationInstructionFactory instructionFactory = new 
LLMMCPConversationInstructionFactory();
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPToolDefinitionFactory.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPToolDefinitionFactory.java
index 3ea035dcb01..bb589037605 100644
--- 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPToolDefinitionFactory.java
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPToolDefinitionFactory.java
@@ -38,9 +38,15 @@ final class LLMMCPToolDefinitionFactory {
     }
     
     private Map<String, Object> createToolDefinition(final String toolName) {
+        if (MCPInteractionActionNames.LIST_TOOLS.equals(toolName)) {
+            return createListToolsToolDefinition();
+        }
         if (MCPInteractionActionNames.LIST_RESOURCES.equals(toolName)) {
             return createListResourcesToolDefinition();
         }
+        if 
(MCPInteractionActionNames.LIST_RESOURCE_TEMPLATES.equals(toolName)) {
+            return createListResourceTemplatesToolDefinition();
+        }
         if (MCPInteractionActionNames.READ_RESOURCE.equals(toolName)) {
             return createReadResourceToolDefinition();
         }
@@ -53,6 +59,13 @@ final class LLMMCPToolDefinitionFactory {
         return MCPInteractionActionNames.COMPLETE.equals(toolName) ? 
createCompleteToolDefinition() : createOfficialToolDefinition(toolName);
     }
     
+    private Map<String, Object> createListToolsToolDefinition() {
+        return Map.of("type", "function", "function", Map.of(
+                "name", MCPInteractionActionNames.LIST_TOOLS,
+                "description", "Bridge to MCP tools/list for 
application-driven tool discovery.",
+                "parameters", createEmptyObjectSchema()));
+    }
+    
     private Map<String, Object> createListResourcesToolDefinition() {
         return Map.of("type", "function", "function", Map.of(
                 "name", MCPInteractionActionNames.LIST_RESOURCES,
@@ -60,6 +73,13 @@ final class LLMMCPToolDefinitionFactory {
                 "parameters", createEmptyObjectSchema()));
     }
     
+    private Map<String, Object> createListResourceTemplatesToolDefinition() {
+        return Map.of("type", "function", "function", Map.of(
+                "name", MCPInteractionActionNames.LIST_RESOURCE_TEMPLATES,
+                "description", "Bridge to MCP resources/templates/list for 
parameterized resource discovery.",
+                "parameters", createEmptyObjectSchema()));
+    }
+    
     private Map<String, Object> createReadResourceToolDefinition() {
         return Map.of("type", "function", "function", Map.of(
                 "name", MCPInteractionActionNames.READ_RESOURCE,
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPToolDefinitionFactoryTest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPToolDefinitionFactoryTest.java
index 78ef1719049..d75c9e628ce 100644
--- 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPToolDefinitionFactoryTest.java
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPToolDefinitionFactoryTest.java
@@ -46,14 +46,18 @@ class LLMMCPToolDefinitionFactoryTest {
     @Test
     void assertProtocolBridgeToolDefinitionsKeepBridgeSchemas() {
         List<String> bridgeToolNames = List.of(
+                MCPInteractionActionNames.LIST_TOOLS,
                 MCPInteractionActionNames.LIST_RESOURCES,
+                MCPInteractionActionNames.LIST_RESOURCE_TEMPLATES,
                 MCPInteractionActionNames.READ_RESOURCE,
                 MCPInteractionActionNames.LIST_PROMPTS,
                 MCPInteractionActionNames.GET_PROMPT,
                 MCPInteractionActionNames.COMPLETE);
         List<Map<String, Object>> actual = new 
LLMMCPToolDefinitionFactory().create(bridgeToolNames);
         assertThat(getToolNames(actual), is(bridgeToolNames));
+        assertEmptyObjectSchema(getParameters(findTool(actual, 
MCPInteractionActionNames.LIST_TOOLS)));
         assertEmptyObjectSchema(getParameters(findTool(actual, 
MCPInteractionActionNames.LIST_RESOURCES)));
+        assertEmptyObjectSchema(getParameters(findTool(actual, 
MCPInteractionActionNames.LIST_RESOURCE_TEMPLATES)));
         assertReadResourceBridgeSchema(getParameters(findTool(actual, 
MCPInteractionActionNames.READ_RESOURCE)));
         assertEmptyObjectSchema(getParameters(findTool(actual, 
MCPInteractionActionNames.LIST_PROMPTS)));
         assertGetPromptBridgeSchema(getParameters(findTool(actual, 
MCPInteractionActionNames.GET_PROMPT)));
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPTraceRecordFactory.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPTraceRecordFactory.java
index 1c168ff70bd..8453da17865 100644
--- 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPTraceRecordFactory.java
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPTraceRecordFactory.java
@@ -28,10 +28,18 @@ final class LLMMCPTraceRecordFactory {
     MCPInteractionTraceRecord createTraceRecord(final int sequence, final 
String actionName, final String actionOrigin, final Map<String, Object> args,
                                                 final Map<String, Object> 
structuredContent, final long latencyMillis) {
         String bridgeActionOrigin = 
MCPInteractionTraceRecord.PROTOCOL_BRIDGE_ORIGIN;
+        if (MCPInteractionActionNames.LIST_TOOLS.equals(actionName)) {
+            return new MCPInteractionTraceRecord(sequence, 
MCPInteractionActionNames.TOOL_LIST_KIND, bridgeActionOrigin, 
MCPInteractionActionNames.LIST_TOOLS,
+                    Map.of(), structuredContent, true, latencyMillis);
+        }
         if (MCPInteractionActionNames.LIST_RESOURCES.equals(actionName)) {
             return new MCPInteractionTraceRecord(sequence, 
MCPInteractionActionNames.RESOURCE_LIST_KIND, bridgeActionOrigin, 
MCPInteractionActionNames.LIST_RESOURCES,
                     Map.of(), structuredContent, true, latencyMillis);
         }
+        if 
(MCPInteractionActionNames.LIST_RESOURCE_TEMPLATES.equals(actionName)) {
+            return new MCPInteractionTraceRecord(sequence, 
MCPInteractionActionNames.RESOURCE_TEMPLATE_LIST_KIND, bridgeActionOrigin, 
MCPInteractionActionNames.LIST_RESOURCE_TEMPLATES,
+                    Map.of(), structuredContent, true, latencyMillis);
+        }
         if (MCPInteractionActionNames.READ_RESOURCE.equals(actionName)) {
             return new MCPInteractionTraceRecord(sequence, 
MCPInteractionActionNames.RESOURCE_READ_KIND, bridgeActionOrigin, 
MCPInteractionActionNames.READ_RESOURCE,
                     Map.of("uri", Objects.toString(args.get("uri"), 
"").trim()), structuredContent, true, latencyMillis);
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/suite/usability/scenario/LLMUsabilityScenarioCatalog.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/suite/usability/scenario/LLMUsabilityScenarioCatalog.java
index 5efc912680b..a5d35659f38 100644
--- 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/suite/usability/scenario/LLMUsabilityScenarioCatalog.java
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/suite/usability/scenario/LLMUsabilityScenarioCatalog.java
@@ -155,12 +155,14 @@ public final class LLMUsabilityScenarioCatalog {
         result.add(createScenario("extended-resource-list-discovery-" + 
runtimeKind, LLMUsabilityDimension.RESOURCE, runtimeKind,
                 List.of(LLMUsabilityScenario.PROTOCOL_CONTRACT_TAG, 
"extended", "resource-discovery"),
                 new LLME2EScenario("extended-resource-list-discovery-" + 
runtimeKind, SYSTEM_PROMPT,
-                        "Discover the available metadata resources, read exact 
table resource `" + tableResourceUri
+                        "Discover the available MCP tools, resource templates, 
and metadata resources, read exact table resource `" + tableResourceUri
                                 + "`, and verify `" + query + "`. Do not read 
placeholder URI text from tool schema descriptions." + toolContext,
                         createAnswer(databaseName, schemaName, tableName, 
query, totalOrders),
-                        List.of(MCPInteractionActionNames.LIST_RESOURCES, 
MCPInteractionActionNames.READ_RESOURCE, "database_gateway_execute_query"),
-                        List.of(MCPInteractionActionNames.LIST_RESOURCES, 
MCPInteractionActionNames.READ_RESOURCE, "database_gateway_execute_query")),
-                List.of(MCPInteractionActionNames.LIST_RESOURCES), 
List.of(tableResourceUri), true, false));
+                        List.of(MCPInteractionActionNames.LIST_TOOLS, 
MCPInteractionActionNames.LIST_RESOURCES, 
MCPInteractionActionNames.LIST_RESOURCE_TEMPLATES,
+                                MCPInteractionActionNames.READ_RESOURCE, 
"database_gateway_execute_query"),
+                        List.of(MCPInteractionActionNames.LIST_TOOLS, 
MCPInteractionActionNames.LIST_RESOURCES, 
MCPInteractionActionNames.LIST_RESOURCE_TEMPLATES,
+                                MCPInteractionActionNames.READ_RESOURCE, 
"database_gateway_execute_query")),
+                List.of(MCPInteractionActionNames.LIST_TOOLS), 
List.of(tableResourceUri), true, false));
         result.add(createScenario("extended-runtime-status-" + runtimeKind, 
LLMUsabilityDimension.RESOURCE, runtimeKind,
                 List.of(LLMUsabilityScenario.PROTOCOL_CONTRACT_TAG, 
"extended", "runtime-diagnostics"),
                 new LLME2EScenario("extended-runtime-status-" + runtimeKind, 
SYSTEM_PROMPT,
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/suite/usability/scenario/LLMUsabilityScenarioCatalogTest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/suite/usability/scenario/LLMUsabilityScenarioCatalogTest.java
index 2826c596c56..1a8d49b669e 100644
--- 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/suite/usability/scenario/LLMUsabilityScenarioCatalogTest.java
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/suite/usability/scenario/LLMUsabilityScenarioCatalogTest.java
@@ -17,6 +17,7 @@
 
 package org.apache.shardingsphere.test.e2e.mcp.llm.suite.usability.scenario;
 
+import 
org.apache.shardingsphere.test.e2e.mcp.support.transport.MCPInteractionActionNames;
 import org.junit.jupiter.api.Test;
 
 import java.util.List;
@@ -74,8 +75,14 @@ class LLMUsabilityScenarioCatalogTest {
         List<LLMUsabilityScenario> actual = new 
LLMUsabilityScenarioCatalog().createExtendedScore("mysql", "logic_db", 
"logic_db", "orders",
                 "SELECT COUNT(*) AS total_orders FROM orders", 2);
         Map<String, LLMUsabilityScenario> actualScenarios = 
actual.stream().collect(Collectors.toMap(LLMUsabilityScenario::getScenarioId, 
each -> each));
-        assertThat(actualScenarios.keySet(), 
hasItems("extended-prompt-completion-inspect-mysql", 
"extended-runtime-status-mysql", "extended-recovery-missing-database-mysql",
-                "extended-recovery-bad-resource-mysql"));
+        assertThat(actualScenarios.keySet(), 
hasItems("extended-prompt-completion-inspect-mysql", 
"extended-resource-list-discovery-mysql", "extended-runtime-status-mysql",
+                "extended-recovery-missing-database-mysql", 
"extended-recovery-bad-resource-mysql"));
+        
assertThat(actualScenarios.get("extended-resource-list-discovery-mysql").getExpectedFirstActionNames(),
 is(List.of(MCPInteractionActionNames.LIST_TOOLS)));
+        List<String> expectedDiscoveryToolNames = List.of(
+                MCPInteractionActionNames.LIST_TOOLS, 
MCPInteractionActionNames.LIST_RESOURCES, 
MCPInteractionActionNames.LIST_RESOURCE_TEMPLATES,
+                MCPInteractionActionNames.READ_RESOURCE, 
"database_gateway_execute_query");
+        
assertThat(actualScenarios.get("extended-resource-list-discovery-mysql").getLlmScenario().getAllowedToolNames(),
 is(expectedDiscoveryToolNames));
+        
assertThat(actualScenarios.get("extended-resource-list-discovery-mysql").getLlmScenario().getRequiredToolNames(),
 is(expectedDiscoveryToolNames));
         
assertThat(actualScenarios.get("extended-runtime-status-mysql").getExpectedResourceUris(),
 is(List.of("shardingsphere://runtime")));
         
assertFalse(actualScenarios.get("extended-recovery-missing-database-mysql").isRecoveryExpected());
         
assertTrue(actualScenarios.get("extended-recovery-bad-resource-mysql").isRecoveryExpected());
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/transport/MCPInteractionActionNames.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/transport/MCPInteractionActionNames.java
index 6a6aae611ae..12b590c9dcf 100644
--- 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/transport/MCPInteractionActionNames.java
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/transport/MCPInteractionActionNames.java
@@ -26,8 +26,12 @@ import lombok.NoArgsConstructor;
 @NoArgsConstructor(access = AccessLevel.PRIVATE)
 public final class MCPInteractionActionNames {
     
+    public static final String LIST_TOOLS = "mcp_list_tools";
+    
     public static final String LIST_RESOURCES = "mcp_list_resources";
     
+    public static final String LIST_RESOURCE_TEMPLATES = 
"mcp_list_resource_templates";
+    
     public static final String READ_RESOURCE = "mcp_read_resource";
     
     public static final String LIST_PROMPTS = "mcp_list_prompts";
@@ -36,8 +40,12 @@ public final class MCPInteractionActionNames {
     
     public static final String COMPLETE = "mcp_complete";
     
+    public static final String TOOL_LIST_KIND = "tool_list";
+    
     public static final String RESOURCE_LIST_KIND = "resource_list";
     
+    public static final String RESOURCE_TEMPLATE_LIST_KIND = 
"resource_template_list";
+    
     public static final String RESOURCE_READ_KIND = "resource_read";
     
     public static final String PROMPT_LIST_KIND = "prompt_list";
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/transport/MCPInteractionPayloads.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/transport/MCPInteractionPayloads.java
index fc9e9087104..d1aee5045d4 100644
--- 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/transport/MCPInteractionPayloads.java
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/transport/MCPInteractionPayloads.java
@@ -23,6 +23,7 @@ import lombok.AccessLevel;
 import lombok.NoArgsConstructor;
 
 import java.io.IOException;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -166,9 +167,12 @@ public final class MCPInteractionPayloads {
     
     private static Map<String, Object> createJsonRpcErrorPayload(final Object 
rawError) {
         Map<String, Object> error = castToMap(rawError);
-        return Map.of(
-                "error_code", "json_rpc_error",
-                "message", String.valueOf(error.getOrDefault("message", 
"Unknown JSON-RPC error.")));
+        Map<String, Object> data = error.get("data") instanceof Map ? 
castToMap(error.get("data")) : Map.of();
+        Map<String, Object> result = new LinkedHashMap<>(data.size() + 2, 1F);
+        result.putAll(data);
+        result.put("error_code", "json_rpc_error");
+        result.put("message", String.valueOf(error.getOrDefault("message", 
"Unknown JSON-RPC error.")));
+        return result;
     }
     
     private static String normalizeJsonBody(final String responseBody) {
diff --git 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/transport/MCPInteractionPayloadsTest.java
 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/transport/MCPInteractionPayloadsTest.java
index bf84ae0fd6a..54af0dd4b43 100644
--- 
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/transport/MCPInteractionPayloadsTest.java
+++ 
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/transport/MCPInteractionPayloadsTest.java
@@ -126,6 +126,20 @@ class MCPInteractionPayloadsTest {
                 is(Map.of("error_code", "json_rpc_error", "message", "Tool not 
found")));
     }
     
+    @Test
+    void assertGetJsonRpcErrorPayloadPreservesErrorData() {
+        Map<String, Object> actual = 
MCPInteractionPayloads.getJsonRpcErrorPayload(Map.of("error", Map.of(
+                "message", "Tool not found",
+                "data", Map.of(
+                        "message", "Nested recovery message",
+                        "response_mode", "recovery",
+                        "recovery", Map.of("next_actions", 
List.of(Map.of("type", "tool_call", "tool_name", 
"database_gateway_search_metadata")))))));
+        assertThat(actual.get("error_code"), is("json_rpc_error"));
+        assertThat(actual.get("message"), is("Tool not found"));
+        assertThat(actual.get("response_mode"), is("recovery"));
+        assertThat(actual.get("recovery"), is(Map.of("next_actions", 
List.of(Map.of("type", "tool_call", "tool_name", 
"database_gateway_search_metadata")))));
+    }
+    
     @Test
     void assertGetJsonRpcErrorPayloadWithoutError() {
         
assertTrue(MCPInteractionPayloads.getJsonRpcErrorPayload(Map.of()).isEmpty());


Reply via email to