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());