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 11286753580 Enhance MCP model-facing guidance contract (#39015)
11286753580 is described below
commit 112867535801ca378c308ed3ae768159b2c7ad3b
Author: Liang Zhang <[email protected]>
AuthorDate: Mon Jul 6 00:18:57 2026 +0800
Enhance MCP model-facing guidance contract (#39015)
* Support metadata URL fallback for SQLException
* Enhance MCP model-facing guidance contract
- Add concise summaries, first-call routes, and transport security guidance
- Expose typed self_resource navigation while preserving self_uri
compatibility
- Promote recovery next_actions to the top level for easier model execution
- Sync MCP descriptors, LLM formatter coverage, and guidance baseline
---
.../tool/MCPResourceLinkCandidateCollector.java | 10 ++--
.../tool/MCPCallToolResultFactoryTest.java | 10 ++++
.../core/protocol/response/MCPErrorResponse.java | 6 ++-
.../handler/capability/RuntimeStatusHandler.java | 23 +++++++--
.../handler/metadata/MetadataResourceHandler.java | 26 ++++++++--
.../handler/execute/ExecuteUpdateToolHandler.java | 6 ++-
.../metadata/SearchMetadataToolHandler.java | 7 ++-
.../mcp-descriptors/mcp-descriptor-core.yaml | 46 ++++++++++++++++++
.../protocol/response/MCPErrorResponseTest.java | 9 ++++
.../capability/RuntimeStatusHandlerTest.java | 6 ++-
.../capability/ServerCapabilitiesHandlerTest.java | 8 ++++
.../metadata/MetadataResourceHandlerTest.java | 55 +++++++++++++++-------
.../execute/ExecuteUpdateToolHandlerTest.java | 1 +
.../metadata/SearchMetadataToolHandlerTest.java | 4 ++
.../response/RuntimeDatabaseValidationResult.java | 12 ++++-
.../tool/response/SQLExecutionResponse.java | 16 ++++++-
.../descriptor/MCPGuidancePayloadBuilder.java | 26 +++++++++-
.../protocol/MCPModelFacingPayloadContract.java | 6 +--
.../mcp/support/protocol/MCPPayloadFieldNames.java | 4 ++
.../RuntimeDatabaseValidationResultTest.java | 10 ++++
.../tool/response/SQLExecutionResponseTest.java | 11 +++--
.../descriptor/MCPGuidancePayloadBuilderTest.java | 9 ++++
.../MCPModelFacingPayloadContractTest.java | 2 +
...LLMMCPModelFacingToolResponseFormatterTest.java | 46 +++++++++---------
.../baseline-contract/model-contract/guidance.yaml | 17 +++++++
25 files changed, 314 insertions(+), 62 deletions(-)
diff --git
a/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPResourceLinkCandidateCollector.java
b/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPResourceLinkCandidateCollector.java
index e853568b067..2fede68eb3e 100644
---
a/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPResourceLinkCandidateCollector.java
+++
b/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPResourceLinkCandidateCollector.java
@@ -50,6 +50,7 @@ final class MCPResourceLinkCandidateCollector {
private void collectResourceLinkFields(final Map<?, ?> value, final
List<OrderedResourceLinkCandidate> candidates) {
collectResourceLinkValue(value.get(MCPPayloadFieldNames.RESOURCES_TO_READ),
MCPPayloadFieldNames.RESOURCES_TO_READ, candidates);
+
collectResourceLinkValue(value.get(MCPPayloadFieldNames.SELF_RESOURCE),
MCPPayloadFieldNames.SELF_RESOURCE, candidates);
collectResourceLinkValue(value.get(MCPPayloadFieldNames.RESOURCE),
MCPPayloadFieldNames.RESOURCE, candidates);
collectResourceLinkValue(value.get(MCPPayloadFieldNames.PARENT_RESOURCE),
MCPPayloadFieldNames.PARENT_RESOURCE, candidates);
collectResourceLinkValue(value.get(MCPPayloadFieldNames.NEXT_RESOURCES),
MCPPayloadFieldNames.NEXT_RESOURCES, candidates);
@@ -125,13 +126,16 @@ final class MCPResourceLinkCandidateCollector {
if (MCPPayloadFieldNames.RESOURCES_TO_READ.equals(sourceField)) {
return 0;
}
- if (MCPPayloadFieldNames.RESOURCE.equals(sourceField)) {
+ if (MCPPayloadFieldNames.SELF_RESOURCE.equals(sourceField)) {
return 1;
}
- if (MCPPayloadFieldNames.PARENT_RESOURCE.equals(sourceField)) {
+ if (MCPPayloadFieldNames.RESOURCE.equals(sourceField)) {
return 2;
}
- return MCPPayloadFieldNames.NEXT_RESOURCES.equals(sourceField) ? 3 : 4;
+ if (MCPPayloadFieldNames.PARENT_RESOURCE.equals(sourceField)) {
+ return 3;
+ }
+ return MCPPayloadFieldNames.NEXT_RESOURCES.equals(sourceField) ? 4 : 5;
}
private Map<String, OrderedResourceLinkCandidate>
deduplicateCandidates(final List<OrderedResourceLinkCandidate> candidates) {
diff --git
a/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPCallToolResultFactoryTest.java
b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPCallToolResultFactoryTest.java
index 80a5d9a28ac..9841268eec8 100644
---
a/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPCallToolResultFactoryTest.java
+++
b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/capability/tool/MCPCallToolResultFactoryTest.java
@@ -135,6 +135,16 @@ class MCPCallToolResultFactoryTest extends
AbstractMCPToolSpecificationFactoryTe
assertThat(actualLink.mimeType(), is("application/json"));
}
+ @Test
+ void assertCreateToolSpecificationsHandleSelfResourceLink() {
+ Map<String, Object> payload = Map.of("self_resource",
+
MCPResourceHintUtils.create("shardingsphere://databases/logic_db",
"logical-database", "inspect_self", "Read logical database.", "self_resource"));
+ CallToolResult actual = createCallToolResult("fixture_ping", new
MCPMapResponse(payload));
+ assertThat(actual.content().get(1), isA(ResourceLink.class));
+ assertThat(((ResourceLink) actual.content().get(1)).uri(),
is("shardingsphere://databases/logic_db"));
+ assertThat(((Map<?, ?>)
actual.content().get(1).meta()).get(MCPShardingSphereMetadataKeys.SOURCE_FIELD),
is("self_resource"));
+ }
+
@Test
void assertCreateToolSpecificationsHandleItemResourceLinks() {
Map<String, Object> payload = Map.of("items", List.of(Map.of(
diff --git
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/response/MCPErrorResponse.java
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/response/MCPErrorResponse.java
index fef201617ef..114f1ac18d6 100644
---
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/response/MCPErrorResponse.java
+++
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/response/MCPErrorResponse.java
@@ -50,12 +50,16 @@ public final class MCPErrorResponse implements MCPResponse {
@Override
public Map<String, Object> toPayload() {
- Map<String, Object> result = new LinkedHashMap<>(4, 1F);
+ Map<String, Object> result = new LinkedHashMap<>(6, 1F);
result.put("response_mode", MCPResponseMode.RECOVERY);
+ result.put(MCPPayloadFieldNames.SUMMARY, message.isEmpty() ? "Recovery
guidance is available." : message);
result.put("request_id", requestId);
result.put(MCPPayloadFieldNames.MESSAGE, message);
if (!recovery.isEmpty()) {
result.put(MCPPayloadFieldNames.RECOVERY, createRecoveryPayload());
+ if (recovery.containsKey(MCPPayloadFieldNames.NEXT_ACTIONS)) {
+ result.put(MCPPayloadFieldNames.NEXT_ACTIONS,
recovery.get(MCPPayloadFieldNames.NEXT_ACTIONS));
+ }
}
return result;
}
diff --git
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/resource/handler/capability/RuntimeStatusHandler.java
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/resource/handler/capability/RuntimeStatusHandler.java
index 6abfca627c9..9ba72864b20 100644
---
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/resource/handler/capability/RuntimeStatusHandler.java
+++
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/resource/handler/capability/RuntimeStatusHandler.java
@@ -58,12 +58,14 @@ public final class RuntimeStatusHandler implements
MCPResourceHandler<MCPDatabas
public MCPResponse handle(final MCPDatabaseHandlerContext handlerContext,
final MCPUriVariables uriVariables) {
List<MCPDatabaseMetadata> databases =
handlerContext.getMetadataQueryFacade().queryDatabases();
boolean hasConfiguredDatabase = !databases.isEmpty();
- Map<String, Object> result = new LinkedHashMap<>(13, 1F);
+ Map<String, Object> result = new LinkedHashMap<>(15, 1F);
result.put("response_mode", MCPResponseMode.RUNTIME);
+ result.put(MCPPayloadFieldNames.SUMMARY,
createSummary(hasConfiguredDatabase, databases.size()));
result.put("server_status", hasConfiguredDatabase ? "ready" :
"configuration_required");
result.put("status", hasConfiguredDatabase ? "available" :
"configuration_required");
result.put("transport", handlerContext.getActiveTransport());
result.put("active_transport", handlerContext.getActiveTransport());
+ result.put("transport_security_summary",
createTransportSecuritySummary(handlerContext.getActiveTransport()));
result.put("configured_database_count", databases.size());
result.put("databases", databases.stream().map(each ->
createDatabaseStatus(handlerContext, each)).toList());
result.put("readiness", createReadiness(hasConfiguredDatabase));
@@ -75,6 +77,21 @@ public final class RuntimeStatusHandler implements
MCPResourceHandler<MCPDatabas
return new MCPMapResponse(result);
}
+ private String createSummary(final boolean hasConfiguredDatabase, final
int configuredDatabaseCount) {
+ return hasConfiguredDatabase
+ ? String.format("Runtime is ready with %d configured logical
database(s).", configuredDatabaseCount)
+ : "Runtime requires at least one configured logical database
before metadata discovery or SQL execution.";
+ }
+
+ private Map<String, Object> createTransportSecuritySummary(final String
activeTransport) {
+ Map<String, Object> result = new LinkedHashMap<>(4, 1F);
+ result.put("transport", activeTransport);
+ result.put("authentication", "http".equalsIgnoreCase(activeTransport)
? "not_enabled_by_mcp_transport" : "local_client_process");
+ result.put("recommended_exposure",
"http".equalsIgnoreCase(activeTransport) ? "loopback_or_trusted_gateway" :
"local_stdio_session");
+ result.put("model_action", "Do not request or echo JDBC URLs,
credentials, raw environment variables, or stack traces.");
+ return result;
+ }
+
private Map<String, Object> createReadiness(final boolean
hasConfiguredDatabase) {
Map<String, Object> result = new LinkedHashMap<>(hasConfiguredDatabase
? 3 : 4, 1F);
result.put("ready", hasConfiguredDatabase);
@@ -139,10 +156,10 @@ public final class RuntimeStatusHandler implements
MCPResourceHandler<MCPDatabas
}
private List<Map<String, Object>> createNextActions(final boolean
hasConfiguredDatabase) {
- Map<String, Object> capabilityAction =
MCPNextActionUtils.readResource("shardingsphere://capabilities", "Read the full
capability catalog before choosing tools.");
if (hasConfiguredDatabase) {
- return List.of();
+ return
List.of(MCPNextActionUtils.readResource("shardingsphere://databases", "Read
logical databases before choosing a database scope."));
}
+ Map<String, Object> capabilityAction =
MCPNextActionUtils.readResource("shardingsphere://capabilities", "Read the full
capability catalog before choosing tools.");
return MCPNextActionUtils.ordered(capabilityAction,
MCPNextActionUtils.dependsOn(MCPNextActionUtils.askUser(
"Ask the operator to configure at least one runtimeDatabases
entry before metadata discovery or SQL execution.",
List.of("runtimeDatabases")), 1));
}
diff --git
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/resource/handler/metadata/MetadataResourceHandler.java
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/resource/handler/metadata/MetadataResourceHandler.java
index 05db85dacbb..ff76baecabc 100644
---
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/resource/handler/metadata/MetadataResourceHandler.java
+++
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/resource/handler/metadata/MetadataResourceHandler.java
@@ -78,6 +78,7 @@ public final class MetadataResourceHandler implements
MCPResourceHandler<MCPData
}
List<?> returnedItems = capListItems(items);
appendListSizeMetadata(navigationPayload, items.size(),
returnedItems.size());
+ navigationPayload.put(MCPPayloadFieldNames.SUMMARY,
createListSummary(metadata, items.size(), returnedItems.size()));
if (items.isEmpty()) {
appendEmptyStateGuidance(navigationPayload, metadata,
databaseContext, uriVariables);
} else if (isTruncated(items, returnedItems)) {
@@ -105,8 +106,9 @@ public final class MetadataResourceHandler implements
MCPResourceHandler<MCPData
}
private Map<String, Object> createDetailPayload(final
ShardingSphereMCPResourceMetadata descriptor, final List<?> items, final
Map<String, Object> navigationPayload) {
- Map<String, Object> result = new
LinkedHashMap<>(navigationPayload.size() + 6, 1F);
+ Map<String, Object> result = new
LinkedHashMap<>(navigationPayload.size() + 7, 1F);
result.put("response_mode", MCPResponseMode.DETAIL);
+ result.put(MCPPayloadFieldNames.SUMMARY,
createDetailSummary(descriptor, items));
result.put(MCPPayloadFieldNames.RESOURCE_KIND, "detail");
if (null != descriptor.getObjectScope()) {
result.put("object_scope", descriptor.getObjectScope());
@@ -121,6 +123,20 @@ public final class MetadataResourceHandler implements
MCPResourceHandler<MCPData
return result;
}
+ private String createListSummary(final ShardingSphereMCPResourceMetadata
descriptor, final int totalCount, final int returnedCount) {
+ return String.format("Returned %d of %d %s metadata entries.",
returnedCount, totalCount, resolveSummaryScope(descriptor));
+ }
+
+ private String createDetailSummary(final ShardingSphereMCPResourceMetadata
descriptor, final List<?> items) {
+ return items.isEmpty()
+ ? String.format("No %s detail item matched this resource
URI.", resolveSummaryScope(descriptor))
+ : String.format("Returned %s detail for this resource URI.",
resolveSummaryScope(descriptor));
+ }
+
+ private String resolveSummaryScope(final ShardingSphereMCPResourceMetadata
descriptor) {
+ return null == descriptor.getObjectScope() ?
descriptor.getResourceKind() : descriptor.getObjectScope().replace('_', '-');
+ }
+
private void appendEmptyStateGuidance(final Map<String, Object> payload,
final ShardingSphereMCPResourceMetadata descriptor,
final MCPDatabaseHandlerContext
databaseContext, final MCPUriVariables uriVariables) {
Map<String, Object> emptyState = new LinkedHashMap<>(4, 1F);
@@ -270,10 +286,14 @@ public final class MetadataResourceHandler implements
MCPResourceHandler<MCPData
}
private Map<String, Object> createNavigationPayload(final
MCPResourceDescriptor descriptor, final MCPUriVariables uriVariables) {
- Map<String, Object> result = new LinkedHashMap<>(3, 1F);
+ Map<String, Object> result = new LinkedHashMap<>(4, 1F);
String uriTemplate = descriptor.getUriTemplate();
Optional<String> selfUri = new
MCPUriTemplate(uriTemplate).expandIfComplete(uriVariables);
- selfUri.ifPresent(optional -> result.put("self_uri", optional));
+ selfUri.ifPresent(uri -> {
+ result.put("self_uri", uri);
+ result.put(MCPPayloadFieldNames.SELF_RESOURCE,
+ MCPResourceHintUtils.create(uri, resolveResourceKind(uri),
"inspect_self", "Read this metadata resource.",
MCPPayloadFieldNames.SELF_RESOURCE));
+ });
String parentUri = createParentUri(selfUri.orElse(""));
if (!parentUri.isEmpty()) {
result.put(MCPPayloadFieldNames.PARENT_RESOURCE,
MCPResourceHintUtils.create(parentUri, resolveResourceKind(parentUri),
"inspect_parent",
diff --git
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteUpdateToolHandler.java
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteUpdateToolHandler.java
index 8fda589efc0..6c287fa9cd4 100644
---
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteUpdateToolHandler.java
+++
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteUpdateToolHandler.java
@@ -103,7 +103,7 @@ public final class ExecuteUpdateToolHandler implements
MCPToolHandler<MCPDatabas
}
private MCPResponse createPreviewResponse(final MCPToolArguments
toolArguments, final ClassificationResult classificationResult) {
- Map<String, Object> result = new LinkedHashMap<>(16, 1F);
+ Map<String, Object> result = new LinkedHashMap<>(17, 1F);
result.put("response_mode", MCPResponseMode.PREVIEW);
result.put("result_kind", RESULT_KIND_PREVIEW);
result.put(MCPPayloadFieldNames.EXECUTION_MODE,
EXECUTION_MODE_PREVIEW);
@@ -118,7 +118,9 @@ public final class ExecuteUpdateToolHandler implements
MCPToolHandler<MCPDatabas
classificationResult.getTargetObjectName().ifPresent(optional ->
result.put("target_object", optional));
classificationResult.getSavepointName().ifPresent(optional ->
result.put("savepoint", optional));
result.put("review_guidance", PREVIEW_REVIEW_GUIDANCE);
- result.put("review_summary",
createReviewSummary(classificationResult));
+ String reviewSummary = createReviewSummary(classificationResult);
+ result.put(MCPPayloadFieldNames.SUMMARY, reviewSummary);
+ result.put("review_summary", reviewSummary);
Map<String, Object> suggestedArguments =
createSuggestedArguments(toolArguments, classificationResult);
result.put("suggested_arguments", suggestedArguments);
result.put(MCPPayloadFieldNames.RESOURCES_TO_READ,
createResourcesToRead(toolArguments));
diff --git
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/metadata/SearchMetadataToolHandler.java
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/metadata/SearchMetadataToolHandler.java
index f17566f8ead..491a2cf1635 100644
---
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/metadata/SearchMetadataToolHandler.java
+++
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/metadata/SearchMetadataToolHandler.java
@@ -74,7 +74,8 @@ public final class SearchMetadataToolHandler implements
MCPToolHandler<MCPDataba
}
private Map<String, Object> createSearchPayloadMetadata(final
MCPDatabaseHandlerContext databaseContext, final MetadataSearchRequest request,
final MetadataSearchResult searchResult) {
- Map<String, Object> result = new LinkedHashMap<>(8, 1F);
+ Map<String, Object> result = new LinkedHashMap<>(9, 1F);
+ result.put(MCPPayloadFieldNames.SUMMARY, createSummary(searchResult));
result.put("search_context", searchResult.getSearchContext());
result.put("total_match_count", searchResult.getTotalMatchCount());
result.put("returned_count", searchResult.getReturnedCount());
@@ -98,6 +99,10 @@ public final class SearchMetadataToolHandler implements
MCPToolHandler<MCPDataba
return result;
}
+ private String createSummary(final MetadataSearchResult searchResult) {
+ return String.format("Metadata search returned %d of %d matches.",
searchResult.getReturnedCount(), searchResult.getTotalMatchCount());
+ }
+
private Map<String, Object> createLargeResultGuidance(final
MetadataSearchResult searchResult) {
Map<String, Object> result = new LinkedHashMap<>(4, 1F);
result.put("state", "metadata_search_result_truncated");
diff --git
a/mcp/core/src/main/resources/META-INF/shardingsphere-mcp/mcp-descriptors/mcp-descriptor-core.yaml
b/mcp/core/src/main/resources/META-INF/shardingsphere-mcp/mcp-descriptors/mcp-descriptor-core.yaml
index 2d530077ec8..9e71ad67b87 100644
---
a/mcp/core/src/main/resources/META-INF/shardingsphere-mcp/mcp-descriptors/mcp-descriptor-core.yaml
+++
b/mcp/core/src/main/resources/META-INF/shardingsphere-mcp/mcp-descriptors/mcp-descriptor-core.yaml
@@ -884,6 +884,9 @@ tools:
description: "Stable response mode marker for metadata search
payloads."
enum:
- search
+ summary:
+ type: string
+ description: "Short model-facing metadata search result summary."
items:
type: array
description: "Matched metadata objects with logical path fields,
object type, and direct resource navigation hints when derivable."
@@ -1036,6 +1039,7 @@ tools:
description: "Action order values that must complete before
this action."
examples:
- response_mode: search
+ summary: Metadata search returned 1 of 1 matches.
items:
- database: logic_db
schema: public
@@ -1109,6 +1113,9 @@ tools:
description: "Stable response mode marker for runtime database
validation payloads."
enum:
- validation
+ summary:
+ type: string
+ description: "Short model-facing runtime database validation
summary."
status:
type: string
description: "Overall preflight status: ready when all required
checks passed, failed otherwise."
@@ -1147,8 +1154,39 @@ tools:
recovery:
type: object
description: "Structured runtime recovery payload. Empty when status
is ready."
+ next_actions:
+ type: array
+ description: "Top-level structured follow-up actions copied from
recovery when validation fails."
+ items:
+ type: object
+ properties:
+ order:
+ type: integer
+ description: "1-based action order."
+ type:
+ type: string
+ description: "Canonical action type such as tool_call,
resource_read, ask_user, completion, or terminal."
+ title:
+ type: string
+ description: "Short model-facing action title."
+ resource_uri:
+ type: string
+ description: "Canonical resource URI when type is
resource_read."
+ question:
+ type: string
+ description: "Question to ask the user when type is ask_user."
+ required_inputs:
+ type: array
+ description: "User inputs needed before continuing."
+ reason:
+ type: string
+ description: "Why the model should take this action."
+ depends_on:
+ type: array
+ description: "Action order values that must complete before
this action."
examples:
- response_mode: validation
+ summary: Runtime database `logic_db` passed validation.
status: ready
database: logic_db
checks:
@@ -1229,6 +1267,9 @@ tools:
description: "Stable response mode marker for read-only SQL results."
enum:
- query
+ summary:
+ type: string
+ description: "Short model-facing SQL execution result summary."
result_kind:
type: string
description: "Result kind. database_gateway_execute_query returns
result_set for row results."
@@ -1305,6 +1346,7 @@ tools:
description: "User inputs needed before continuing."
examples:
- response_mode: query
+ summary: Executed SELECT statement and returned 0 row(s).
result_kind: result_set
statement_class: query
statement_type: SELECT
@@ -1392,6 +1434,9 @@ tools:
enum:
- preview
- executed
+ summary:
+ type: string
+ description: "Short model-facing SQL preview or execution result
summary."
result_kind:
type: string
description: "Result kind: preview, result_set, update_count, or
statement_ack."
@@ -1521,6 +1566,7 @@ tools:
description: "Whether returned rows were truncated."
examples:
- response_mode: preview
+ summary: Previewed UPDATE statement with side-effect scope
physical-data. It has not been executed.
result_kind: preview
execution_mode: preview
preview_semantics: classification_only
diff --git
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/protocol/response/MCPErrorResponseTest.java
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/protocol/response/MCPErrorResponseTest.java
index 13ff992aa9c..9a79c0de615 100644
---
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/protocol/response/MCPErrorResponseTest.java
+++
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/protocol/response/MCPErrorResponseTest.java
@@ -19,6 +19,7 @@ package org.apache.shardingsphere.mcp.core.protocol.response;
import org.junit.jupiter.api.Test;
+import java.util.List;
import java.util.Map;
import static org.hamcrest.MatcherAssert.assertThat;
@@ -35,6 +36,7 @@ class MCPErrorResponseTest {
assertNotNull(actual.get("request_id"));
assertFalse(actual.containsKey("recovery"));
assertThat(actual.get("response_mode"), is("recovery"));
+ assertThat(actual.get("summary"), is("foo_message"));
assertThat(actual.get("message"), is("foo_message"));
}
@@ -48,4 +50,11 @@ class MCPErrorResponseTest {
assertTrue((Boolean) actualRecovery.get("recoverable"));
assertThat(actualRecovery.get("request_id"),
is(actual.get("request_id")));
}
+
+ @Test
+ void assertToPayloadWithTopLevelNextActions() {
+ Map<String, Object> actual = new MCPErrorResponse("",
Map.of("next_actions", List.of(Map.of("order", 1, "type", "terminal", "title",
"Stop")))).toPayload();
+ assertThat(actual.get("summary"), is("Recovery guidance is
available."));
+ assertThat(actual.get("next_actions"), is(List.of(Map.of("order", 1,
"type", "terminal", "title", "Stop"))));
+ }
}
diff --git
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/capability/RuntimeStatusHandlerTest.java
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/capability/RuntimeStatusHandlerTest.java
index 0e6d2da70ea..c24f47d759c 100644
---
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/capability/RuntimeStatusHandlerTest.java
+++
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/capability/RuntimeStatusHandlerTest.java
@@ -38,10 +38,12 @@ class RuntimeStatusHandlerTest {
try (MCPRequestScope requestScope = new
MCPRequestScope(ResourceTestDataFactory.createRuntimeContext(ResourceTestDataFactory.createDatabaseMetadata(),
"http"))) {
Map<String, Object> actual = new
RuntimeStatusHandler().handle(requestScope, new
MCPUriVariables(Map.of())).toPayload();
assertThat(actual.get("response_mode"), is("runtime"));
+ assertThat(actual.get("summary"), is("Runtime is ready with 3
configured logical database(s)."));
assertThat(actual.get("server_status"), is("ready"));
assertThat(actual.get("status"), is("available"));
assertThat(actual.get("transport"), is("http"));
assertThat(actual.get("active_transport"), is("http"));
+ assertThat(((Map<?, ?>)
actual.get("transport_security_summary")).get("recommended_exposure"),
is("loopback_or_trusted_gateway"));
assertThat(actual.get("configured_database_count"), is(3));
assertTrue(((List<?>) actual.get("databases")).stream().map(each
-> ((Map<?, ?>) each).get("database")).anyMatch("logic_db"::equals));
assertThat(((Map<?, ?>)
actual.get("redaction_summary")).get("marker"), is("******"));
@@ -50,7 +52,7 @@ class RuntimeStatusHandlerTest {
assertFalse(actual.containsKey("capability_fingerprint"));
assertRuntimeCapability((List<?>) actual.get("databases"),
"logic_db");
assertThat(extractResourceUris((List<?>)
actual.get("resources_to_read")), is(List.of("shardingsphere://capabilities",
"shardingsphere://databases")));
- assertThat(actual.get("next_actions"), is(List.of()));
+ assertThat(((Map<?, ?>) ((List<?>)
actual.get("next_actions")).getFirst()).get("resource_uri"),
is("shardingsphere://databases"));
}
}
@@ -60,6 +62,7 @@ class RuntimeStatusHandlerTest {
Map<String, Object> actual = new
RuntimeStatusHandler().handle(requestScope, new
MCPUriVariables(Map.of())).toPayload();
assertThat(actual.get("transport"), is("stdio"));
assertThat(actual.get("active_transport"), is("stdio"));
+ assertThat(((Map<?, ?>)
actual.get("transport_security_summary")).get("recommended_exposure"),
is("local_stdio_session"));
}
}
@@ -68,6 +71,7 @@ class RuntimeStatusHandlerTest {
try (MCPRequestScope requestScope = new
MCPRequestScope(ResourceTestDataFactory.createRuntimeContext(List.of(),
"http"))) {
Map<String, Object> actual = new
RuntimeStatusHandler().handle(requestScope, new
MCPUriVariables(Map.of())).toPayload();
assertThat(actual.get("server_status"),
is("configuration_required"));
+ assertThat(actual.get("summary"), is("Runtime requires at least
one configured logical database before metadata discovery or SQL execution."));
assertThat(actual.get("status"), is("configuration_required"));
assertThat(actual.get("configured_database_count"), is(0));
assertThat(((List<?>) actual.get("databases")).size(), is(0));
diff --git
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/capability/ServerCapabilitiesHandlerTest.java
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/capability/ServerCapabilitiesHandlerTest.java
index 8df63f7df58..fb64372c4bf 100644
---
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/capability/ServerCapabilitiesHandlerTest.java
+++
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/capability/ServerCapabilitiesHandlerTest.java
@@ -122,6 +122,10 @@ class ServerCapabilitiesHandlerTest {
is("shardingsphere://guidance complements MCP list methods
with ShardingSphere domain guidance, workflow guidance, and side-effect
notes."));
assertThat(actual.get("guidance_resource"),
is("shardingsphere://guidance"));
assertFalse(actual.containsKey("safe_first_resource"));
+ Map<?, ?> firstRoute = findByKey((List<?>)
actual.get("first_call_routes"), "intent", "inspect_metadata");
+ assertThat(firstRoute.get("first_action"), is("read_resource
shardingsphere://databases"));
+ Map<?, ?> recoveryRoute = findByKey((List<?>)
actual.get("first_call_routes"), "intent", "recover_error");
+ assertThat(recoveryRoute.get("first_action"), is("follow top-level
next_actions"));
Map<?, ?> metadataRule = (Map<?, ?>) actual.get("metadata_rule");
assertThat(metadataRule.get("first_resource"),
is("shardingsphere://databases"));
assertThat(metadataRule.get("search_tool"),
is("database_gateway_search_metadata"));
@@ -247,6 +251,7 @@ class ServerCapabilitiesHandlerTest {
private void assertCoreToolSchemas(final Map<String, Object> capabilities)
{
Map<?, ?> searchMetadataTool = findTool(capabilities,
"database_gateway_search_metadata");
Map<?, ?> searchMetadataOutputProperties = (Map<?, ?>) ((Map<?, ?>)
searchMetadataTool.get("outputSchema")).get("properties");
+ assertTrue(searchMetadataOutputProperties.containsKey("summary"));
assertTrue(searchMetadataOutputProperties.containsKey("total_match_count"));
assertTrue(searchMetadataOutputProperties.containsKey("returned_count"));
assertTrue(searchMetadataOutputProperties.containsKey("truncated"));
@@ -256,13 +261,16 @@ class ServerCapabilitiesHandlerTest {
Map<?, ?> validateRuntimeDatabaseTool = findTool(capabilities,
"database_gateway_validate_runtime_database");
Map<?, ?> validateRuntimeDatabaseOutputProperties = (Map<?, ?>)
((Map<?, ?>) validateRuntimeDatabaseTool.get("outputSchema")).get("properties");
assertThat(getInputFieldNames(validateRuntimeDatabaseTool),
is(List.of("database")));
+
assertTrue(validateRuntimeDatabaseOutputProperties.containsKey("summary"));
assertTrue(validateRuntimeDatabaseOutputProperties.containsKey("status"));
assertTrue(validateRuntimeDatabaseOutputProperties.containsKey("checks"));
assertTrue(validateRuntimeDatabaseOutputProperties.containsKey("category"));
assertTrue(validateRuntimeDatabaseOutputProperties.containsKey("recovery"));
+
assertTrue(validateRuntimeDatabaseOutputProperties.containsKey("next_actions"));
Map<?, ?> executeUpdateTool = findTool(capabilities,
"database_gateway_execute_update");
Map<?, ?> executeUpdateOutputProperties = (Map<?, ?>) ((Map<?, ?>)
executeUpdateTool.get("outputSchema")).get("properties");
assertTrue(executeUpdateOutputProperties.containsKey("response_mode"));
+ assertTrue(executeUpdateOutputProperties.containsKey("summary"));
assertTrue(((List<?>) ((Map<?, ?>)
executeUpdateOutputProperties.get("result_kind")).get("enum")).containsAll(List.of("preview",
"result_set", "update_count", "statement_ack")));
assertTrue(executeUpdateOutputProperties.containsKey("preview_semantics"));
assertTrue(executeUpdateOutputProperties.containsKey("review_summary"));
diff --git
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/metadata/MetadataResourceHandlerTest.java
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/metadata/MetadataResourceHandlerTest.java
index 78c12abaa59..5b6f43c21bf 100644
---
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/metadata/MetadataResourceHandlerTest.java
+++
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/metadata/MetadataResourceHandlerTest.java
@@ -53,8 +53,20 @@ class MetadataResourceHandlerTest {
MetadataResourceHandler handler = new
MetadataResourceHandler("shardingsphere://databases",
(requestContext, uriVariables) -> List.of(Map.of("database",
"logic_db")));
MCPResponse actual =
handler.handle(mock(MCPDatabaseHandlerContext.class), new
MCPUriVariables(Map.of()));
- assertThat(actual.toPayload(), is(Map.of("response_mode", "list",
"items", List.of(Map.of("database", "logic_db")), "count", 1, "has_more", false,
- "continuation_mode", "none", "self_uri",
"shardingsphere://databases", "total_count", 1, "returned_count", 1,
"truncated", false)));
+ Map<String, Object> actualPayload = actual.toPayload();
+ assertThat(actualPayload.get("response_mode"), is("list"));
+ assertThat(actualPayload.get("summary"), is("Returned 1 of 1
logical-database metadata entries."));
+ assertThat(actualPayload.get("items"), is(List.of(Map.of("database",
"logic_db"))));
+ assertThat(actualPayload.get("count"), is(1));
+ assertFalse((Boolean) actualPayload.get("has_more"));
+ assertThat(actualPayload.get("continuation_mode"), is("none"));
+ assertThat(actualPayload.get("self_uri"),
is("shardingsphere://databases"));
+ assertThat(((Map<?, ?>)
actualPayload.get("self_resource")).get("uri"),
is("shardingsphere://databases"));
+ assertThat(((Map<?, ?>)
actualPayload.get("self_resource")).get("resource_kind"),
is("logical-database"));
+ assertThat(((Map<?, ?>)
actualPayload.get("self_resource")).get("source_field"), is("self_resource"));
+ assertThat(actualPayload.get("total_count"), is(1));
+ assertThat(actualPayload.get("returned_count"), is(1));
+ assertFalse((Boolean) actualPayload.get("truncated"));
}
@Test
@@ -66,6 +78,7 @@ class MetadataResourceHandlerTest {
assertThat(actualPayload.get("count"), is(100));
assertThat(actualPayload.get("total_count"), is(101));
assertThat(actualPayload.get("returned_count"), is(100));
+ assertThat(actualPayload.get("summary"), is("Returned 100 of 101
logical-database metadata entries."));
assertTrue((Boolean) actualPayload.get("truncated"));
assertTrue((Boolean) actualPayload.get("has_more"));
assertThat(actualPayload.get("continuation_mode"),
is("metadata_search"));
@@ -137,26 +150,36 @@ class MetadataResourceHandlerTest {
(requestContext, uriVariables) -> List.of(Map.of("database",
uriVariables.getValue("database"))));
MCPUriVariables uriVariables = new MCPUriVariables(Map.of("database",
"逻辑 库/2026?"));
MCPResponse actual =
handler.handle(mock(MCPDatabaseHandlerContext.class), uriVariables);
- assertThat(actual.toPayload(), is(Map.of("response_mode", "detail",
"resource_kind", "detail", "object_scope", "logical-database", "found", true,
- "items", List.of(Map.of("database", "逻辑 库/2026?")), "count",
1, "item", Map.of("database", "逻辑 库/2026?"),
- "self_uri",
"shardingsphere://databases/%E9%80%BB%E8%BE%91%20%E5%BA%93%2F2026%3F",
- "parent_resource", Map.of("uri", "shardingsphere://databases",
"resource_kind", "logical-database", "purpose", "inspect_parent",
- "reason", "Read the parent metadata resource before
broadening or correcting the request.", "source_field", "parent_resource"),
- "next_resources", List.of(Map.of("uri",
"shardingsphere://databases/%E9%80%BB%E8%BE%91%20%E5%BA%93%2F2026%3F/schemas",
"resource_kind", "schema", "purpose", "inspect_detail",
- "reason", "List schemas after choosing a logical
database.", "source_field", "next_resources"),
- Map.of("uri",
"shardingsphere://databases/%E9%80%BB%E8%BE%91%20%E5%BA%93%2F2026%3F/storage-units",
"resource_kind", "storage-unit",
- "purpose", "inspect_detail", "reason", "List
storage units after choosing a logical database.", "source_field",
"next_resources"),
- Map.of("uri",
"shardingsphere://databases/%E9%80%BB%E8%BE%91%20%E5%BA%93%2F2026%3F/single-tables",
"resource_kind", "single-table",
- "purpose", "inspect_detail", "reason", "List
single table mappings after choosing a logical database.", "source_field",
"next_resources"),
- Map.of("uri",
"shardingsphere://databases/%E9%80%BB%E8%BE%91%20%E5%BA%93%2F2026%3F/single-table/default-storage-unit",
- "resource_kind", "single-table", "purpose",
"inspect_detail",
- "reason", "Read the default single table
storage unit after choosing a logical database.", "source_field",
"next_resources")))));
+ Map<String, Object> actualPayload = actual.toPayload();
+ assertThat(actualPayload.get("response_mode"), is("detail"));
+ assertThat(actualPayload.get("summary"), is("Returned logical-database
detail for this resource URI."));
+ assertThat(actualPayload.get("resource_kind"), is("detail"));
+ assertThat(actualPayload.get("object_scope"), is("logical-database"));
+ assertTrue((Boolean) actualPayload.get("found"));
+ assertThat(actualPayload.get("items"), is(List.of(Map.of("database",
"逻辑 库/2026?"))));
+ assertThat(actualPayload.get("count"), is(1));
+ assertThat(actualPayload.get("item"), is(Map.of("database", "逻辑
库/2026?")));
+ String expectedSelfUri =
"shardingsphere://databases/%E9%80%BB%E8%BE%91%20%E5%BA%93%2F2026%3F";
+ assertThat(actualPayload.get("self_uri"), is(expectedSelfUri));
+ assertThat(((Map<?, ?>)
actualPayload.get("self_resource")).get("uri"), is(expectedSelfUri));
+ Map<?, ?> actualParentResource = (Map<?, ?>)
actualPayload.get("parent_resource");
+ assertThat(actualParentResource.get("uri"),
is("shardingsphere://databases"));
+ assertThat(actualParentResource.get("source_field"),
is("parent_resource"));
+ List<?> actualNextResources = (List<?>)
actualPayload.get("next_resources");
+ assertThat(actualNextResources.size(), is(4));
+ List<String> actualNextResourceUris =
actualNextResources.stream().map(each -> (String) ((Map<?, ?>)
each).get("uri")).toList();
+ assertThat(actualNextResourceUris, is(List.of(
+ expectedSelfUri + "/schemas",
+ expectedSelfUri + "/storage-units",
+ expectedSelfUri + "/single-tables",
+ expectedSelfUri + "/single-table/default-storage-unit")));
}
@Test
void assertHandleMissingDetailResource() {
MetadataResourceHandler handler = new
MetadataResourceHandler("shardingsphere://databases/{database}",
(requestContext, uriVariables) -> List.of());
MCPResponse actual =
handler.handle(mock(MCPDatabaseHandlerContext.class),
mock(MCPUriVariables.class));
+ assertThat(actual.toPayload().get("summary"), is("No logical-database
detail item matched this resource URI."));
assertFalse((Boolean) actual.toPayload().get("found"));
assertThat(((Map<?, ?>)
actual.toPayload().get("empty_state")).get("reason"), is("logical-database
detail resource was not found for this URI."));
assertThat(((Map<?, ?>)
actual.toPayload().get("recovery")).get("recovery_category"), is("not_found"));
diff --git
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteUpdateToolHandlerTest.java
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteUpdateToolHandlerTest.java
index eced0f748b7..cf636605810 100644
---
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteUpdateToolHandlerTest.java
+++
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/execute/ExecuteUpdateToolHandlerTest.java
@@ -113,6 +113,7 @@ class ExecuteUpdateToolHandlerTest {
assertThat(actual.toPayload().get("status"), is("PREVIEWED"));
assertThat(actual.toPayload().get("statement_class"), is("dml"));
assertThat(actual.toPayload().get("side_effect_scope"),
is(List.of("physical-data")));
+ assertThat(actual.toPayload().get("summary"), is("Previewed UPDATE
statement with side-effect scope physical-data. It has not been executed."));
assertThat(actual.toPayload().get("review_summary"), is("Previewed
UPDATE statement with side-effect scope physical-data. It has not been
executed."));
assertThat(actual.toPayload().get("review_guidance"),
is("Review normalized_sql and side_effect_scope before
execution. "
diff --git
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/metadata/SearchMetadataToolHandlerTest.java
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/metadata/SearchMetadataToolHandlerTest.java
index c6cede663a6..f0f645f65cd 100644
---
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/metadata/SearchMetadataToolHandlerTest.java
+++
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/metadata/SearchMetadataToolHandlerTest.java
@@ -70,6 +70,7 @@ class SearchMetadataToolHandlerTest {
assertTrue(actualItemProperties.containsKey("matched_fields"));
assertTrue(actualItemProperties.containsKey("matched_value"));
assertTrue(actualProperties.containsKey("search_context"));
+ assertTrue(actualProperties.containsKey("summary"));
assertTrue(actualProperties.containsKey("total_match_count"));
assertTrue(actualProperties.containsKey("returned_count"));
assertTrue(actualProperties.containsKey("truncated"));
@@ -88,6 +89,7 @@ class SearchMetadataToolHandlerTest {
Map<String, Object> actualPayload = actual.toPayload();
assertThat(actual, isA(MCPItemsResponse.class));
assertThat(((List<?>) actualPayload.get("items")).size(), is(1));
+ assertThat(actualPayload.get("summary"), is("Metadata search
returned 1 of 1 matches."));
assertThat(actualPayload.get("total_match_count"), is(1));
assertThat(actualPayload.get("returned_count"), is(1));
assertFalse((Boolean) actualPayload.get("truncated"));
@@ -164,6 +166,7 @@ class SearchMetadataToolHandlerTest {
Map.of("database", "large_db", "object_types",
List.of(SupportedMCPMetadataObjectType.TABLE.name()))));
Map<String, Object> actualPayload = actual.toPayload();
assertThat(((List<?>) actualPayload.get("items")).size(), is(100));
+ assertThat(actualPayload.get("summary"), is("Metadata search
returned 100 of 101 matches."));
assertThat(actualPayload.get("total_match_count"), is(101));
assertThat(actualPayload.get("returned_count"), is(100));
assertTrue((Boolean) actualPayload.get("truncated"));
@@ -204,6 +207,7 @@ class SearchMetadataToolHandlerTest {
try (MCPRequestScope requestContext = new
MCPRequestScope(createSearchRuntimeContext())) {
MCPResponse actual = new
SearchMetadataToolHandler().handle(requestContext, new MCPToolCall("session-1",
Map.of("database", "logic_db", "query", "missing")));
Map<String, Object> actualPayload = actual.toPayload();
+ assertThat(actualPayload.get("summary"), is("Metadata search
returned 0 of 0 matches."));
assertThat(((Map<?, ?>)
actualPayload.get("empty_state")).get("state"), is("no_match"));
assertThat(((Map<?, ?>)
actualPayload.get("empty_state")).get("category"), is("object_not_visible"));
Map<?, ?> actualNextAction = (Map<?, ?>) ((List<?>)
actualPayload.get("next_actions")).getFirst();
diff --git
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/response/RuntimeDatabaseValidationResult.java
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/response/RuntimeDatabaseValidationResult.java
index 10eb4cf62e3..d689df4addb 100644
---
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/response/RuntimeDatabaseValidationResult.java
+++
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/response/RuntimeDatabaseValidationResult.java
@@ -73,16 +73,26 @@ public final class RuntimeDatabaseValidationResult
implements MCPResponse {
@Override
public Map<String, Object> toPayload() {
- Map<String, Object> result = new LinkedHashMap<>(6, 1F);
+ Map<String, Object> result = new LinkedHashMap<>(8, 1F);
result.put("response_mode", MCPResponseMode.VALIDATION);
+ result.put(MCPPayloadFieldNames.SUMMARY, createSummary());
result.put("status", status);
result.put("database", database);
result.put("checks", createChecksPayload());
result.put("category", category);
result.put(MCPPayloadFieldNames.RECOVERY, recovery);
+ if (recovery.containsKey(MCPPayloadFieldNames.NEXT_ACTIONS)) {
+ result.put(MCPPayloadFieldNames.NEXT_ACTIONS,
recovery.get(MCPPayloadFieldNames.NEXT_ACTIONS));
+ }
return result;
}
+ private String createSummary() {
+ return "ready".equals(status)
+ ? String.format("Runtime database `%s` passed validation.",
database)
+ : String.format("Runtime database `%s` failed validation with
category `%s`.", database, category);
+ }
+
private List<Map<String, Object>> createChecksPayload() {
List<Map<String, Object>> result = new LinkedList<>();
for (RuntimeDatabaseValidationCheckResult each : checks) {
diff --git
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/response/SQLExecutionResponse.java
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/response/SQLExecutionResponse.java
index 9a3ee3a0ddb..6f7492f0ba4 100644
---
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/response/SQLExecutionResponse.java
+++
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/response/SQLExecutionResponse.java
@@ -174,7 +174,7 @@ public final class SQLExecutionResponse implements
MCPResponse {
@Override
public Map<String, Object> toPayload() {
- Map<String, Object> result = new LinkedHashMap<>(32, 1F);
+ Map<String, Object> result = new LinkedHashMap<>(33, 1F);
result.put("response_mode", responseMode);
result.put("result_kind",
resultKind.name().toLowerCase(Locale.ENGLISH));
if (!executionMode.isEmpty()) {
@@ -183,6 +183,7 @@ public final class SQLExecutionResponse implements
MCPResponse {
result.put("statement_class",
statementClass.name().toLowerCase(Locale.ENGLISH));
result.put("statement_type", statementType);
result.put("status", status);
+ result.put(MCPPayloadFieldNames.SUMMARY, createSummary());
if (!normalizedSql.isEmpty()) {
result.put("normalized_sql", normalizedSql);
}
@@ -205,6 +206,19 @@ public final class SQLExecutionResponse implements
MCPResponse {
return result;
}
+ private String createSummary() {
+ return switch (resultKind) {
+ case RESULT_SET -> createResultSetSummary();
+ case UPDATE_COUNT -> String.format("Executed %s statement and
affected %d row(s).", statementType, affectedRows);
+ case STATEMENT_ACK -> message.isEmpty() ? String.format("Executed
%s statement.", statementType) : message;
+ };
+ }
+
+ private String createResultSetSummary() {
+ String result = String.format("Executed %s statement and returned %d
row(s).", statementType, rows.size());
+ return truncated ? result + " Result was truncated." : result;
+ }
+
private List<Map<String, Object>> createNextActions() {
if (ExecuteQueryResultKind.RESULT_SET == resultKind && truncated) {
return List.of(MCPNextActionUtils.askUser("The result was
truncated by max_rows. Ask for a narrower SELECT, stronger WHERE clause, or
smaller projection before retrying.",
diff --git
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPGuidancePayloadBuilder.java
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPGuidancePayloadBuilder.java
index d17f550048d..8f013b469a7 100644
---
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPGuidancePayloadBuilder.java
+++
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPGuidancePayloadBuilder.java
@@ -66,11 +66,12 @@ final class MCPGuidancePayloadBuilder {
}
Map<String, Object> createModelFirstSummary() {
- Map<String, Object> result = new LinkedHashMap<>(11, 1F);
+ Map<String, Object> result = new LinkedHashMap<>(12, 1F);
result.put("official_discovery_methods",
createOfficialDiscoveryMethods());
result.put("argument_completion_method", ARGUMENT_COMPLETION_METHOD);
result.put("guidance_resource_role", GUIDANCE_RESOURCE_URI + "
complements MCP list methods with ShardingSphere domain guidance, workflow
guidance, and side-effect notes.");
result.put("guidance_resource", GUIDANCE_RESOURCE_URI);
+ result.put("first_call_routes", createFirstCallRoutes());
result.put("metadata_rule", createMetadataRule());
result.put("preflight_rule", createPreflightRule());
result.put("sql_tool_selection", createSqlToolSelection());
@@ -81,6 +82,29 @@ final class MCPGuidancePayloadBuilder {
return result;
}
+ private List<Map<String, Object>> createFirstCallRoutes() {
+ return List.of(
+ createFirstCallRoute("inspect_metadata", "read_resource
shardingsphere://databases", "call_tool database_gateway_search_metadata or
read returned resource.uri",
+ "Stop after the requested detail resource is read."),
+ createFirstCallRoute("validate_runtime", "read_resource
shardingsphere://runtime", "call_tool
database_gateway_validate_runtime_database with a configured database",
+ "Follow top-level next_actions when validation
fails."),
+ createFirstCallRoute("read_only_sql", "read_resource
shardingsphere://databases/{database}/capabilities", "call_tool
database_gateway_execute_query",
+ "Stop after reporting the result rows."),
+ createFirstCallRoute("side_effect_sql", "call_tool
database_gateway_execute_update execution_mode=preview", "call_tool
database_gateway_execute_update execution_mode=execute",
+ "Execute only after preview review confirms the
intended side effect."),
+ createFirstCallRoute("recover_error", "follow top-level
next_actions", "fallback to recovery.next_actions when top-level actions are
absent",
+ "Ask the user only when no deterministic resource,
completion, or tool action is available."));
+ }
+
+ private Map<String, Object> createFirstCallRoute(final String intent,
final String firstAction, final String nextStep, final String stopRule) {
+ Map<String, Object> result = new LinkedHashMap<>(4, 1F);
+ result.put("intent", intent);
+ result.put("first_action", firstAction);
+ result.put("next_step", nextStep);
+ result.put("stop_rule", stopRule);
+ return result;
+ }
+
Map<String, Object> createModelContract() {
Map<String, Object> result = new LinkedHashMap<>(12, 1F);
result.put("public_surface_source", MCP_LIST_METHODS_SOURCE);
diff --git
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/protocol/MCPModelFacingPayloadContract.java
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/protocol/MCPModelFacingPayloadContract.java
index 16d5f697dc7..6ed8fc9099a 100644
---
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/protocol/MCPModelFacingPayloadContract.java
+++
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/protocol/MCPModelFacingPayloadContract.java
@@ -50,9 +50,9 @@ public final class MCPModelFacingPayloadContract {
private static final Collection<String> NEXT_ACTION_SCHEMA_ALLOWED_FIELDS
= createNextActionSchemaAllowedFields();
private static final Collection<String> MODEL_CRITICAL_FIELD_NAMES =
List.of(
- MCPPayloadFieldNames.NEXT_ACTIONS,
MCPPayloadFieldNames.RESOURCES_TO_READ, MCPPayloadFieldNames.RESOURCE,
MCPPayloadFieldNames.PARENT_RESOURCE,
- MCPPayloadFieldNames.NEXT_RESOURCES, "manual_artifact_summary",
"manual_follow_up", "empty_state", "ambiguity_state",
MCPPayloadFieldNames.RECOVERY, "recovery_guidance",
- "remediation");
+ MCPPayloadFieldNames.SUMMARY, MCPPayloadFieldNames.NEXT_ACTIONS,
MCPPayloadFieldNames.RESOURCES_TO_READ, MCPPayloadFieldNames.RESOURCE,
+ MCPPayloadFieldNames.SELF_RESOURCE,
MCPPayloadFieldNames.PARENT_RESOURCE, MCPPayloadFieldNames.NEXT_RESOURCES,
"manual_artifact_summary", "manual_follow_up",
+ "empty_state", "ambiguity_state", MCPPayloadFieldNames.RECOVERY,
"recovery_guidance", "remediation");
private MCPModelFacingPayloadContract() {
}
diff --git
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/protocol/MCPPayloadFieldNames.java
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/protocol/MCPPayloadFieldNames.java
index 8b13f8e774c..84ca83eb46d 100644
---
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/protocol/MCPPayloadFieldNames.java
+++
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/protocol/MCPPayloadFieldNames.java
@@ -58,8 +58,12 @@ public final class MCPPayloadFieldNames {
public static final String SECRET = "secret";
+ public static final String SELF_RESOURCE = "self_resource";
+
public static final String SOURCE_FIELD = "source_field";
+ public static final String SUMMARY = "summary";
+
public static final String URI = "uri";
private MCPPayloadFieldNames() {
diff --git
a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/response/RuntimeDatabaseValidationResultTest.java
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/response/RuntimeDatabaseValidationResultTest.java
index 42a1007701e..2a42235c6cb 100644
---
a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/response/RuntimeDatabaseValidationResultTest.java
+++
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/response/RuntimeDatabaseValidationResultTest.java
@@ -53,6 +53,7 @@ class RuntimeDatabaseValidationResultTest {
List.of(RuntimeDatabaseValidationCheckResult.failed("metadata_read",
"connection_failed", "Failed to read metadata.")),
"connection_failed", Map.of("category",
"connection_failed")).toPayload();
assertThat(actual.get("response_mode"), is("validation"));
+ assertThat(actual.get("summary"), is("Runtime database `logic_db`
failed validation with category `connection_failed`."));
assertThat(actual.get("status"), is("failed"));
assertThat(actual.get("database"), is("logic_db"));
assertThat(actual.get("category"), is("connection_failed"));
@@ -63,4 +64,13 @@ class RuntimeDatabaseValidationResultTest {
"category", "connection_failed",
"message", "Failed to read metadata."))));
}
+
+ @Test
+ void assertToPayloadWithTopLevelNextActions() {
+ Map<String, Object> recovery = Map.of("category", "connection_failed",
"next_actions", List.of(Map.of("order", 1, "type", "resource_read", "title",
"Read resource",
+ "resource_uri", "shardingsphere://runtime")));
+ Map<String, Object> actual =
RuntimeDatabaseValidationResult.failed("logic_db",
+
List.of(RuntimeDatabaseValidationCheckResult.failed("metadata_read",
"connection_failed", "Failed to read metadata.")), "connection_failed",
recovery).toPayload();
+ assertThat(actual.get("next_actions"),
is(recovery.get("next_actions")));
+ }
}
diff --git
a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/response/SQLExecutionResponseTest.java
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/response/SQLExecutionResponseTest.java
index a909764871d..2e19a2620cb 100644
---
a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/response/SQLExecutionResponseTest.java
+++
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/response/SQLExecutionResponseTest.java
@@ -127,26 +127,27 @@ class SQLExecutionResponseTest {
Arguments.of("result set with rows",
(Supplier<SQLExecutionResponse>) () -> SQLExecutionResponse.resultSet(columns,
rows, true).withExecutionHints(10, 5000),
Map.ofEntries(
Map.entry("result_kind", "result_set"),
Map.entry("statement_class", "query"), Map.entry("statement_type", "SELECT"),
Map.entry("status", "OK"),
- Map.entry("response_mode", "query"),
+ Map.entry("response_mode", "query"),
Map.entry("summary", "Executed SELECT statement and returned 1 row(s). Result
was truncated."),
Map.entry("columns", columns),
Map.entry("rows", rows), Map.entry("row_object_status", "available"),
Map.entry("row_objects", rowObjects),
Map.entry("returned_row_count", 1),
Map.entry("applied_max_rows", 10), Map.entry("applied_timeout_ms", 5000),
Map.entry("truncated", true),
Map.entry("next_actions",
truncatedResultSetNextActions))),
Arguments.of("result set with null rows",
(Supplier<SQLExecutionResponse>) () ->
SQLExecutionResponse.resultSet(List.of(), null, false),
Map.ofEntries(
Map.entry("result_kind", "result_set"),
Map.entry("statement_class", "query"), Map.entry("statement_type", "SELECT"),
Map.entry("status", "OK"),
- Map.entry("response_mode", "query"),
+ Map.entry("response_mode", "query"),
Map.entry("summary", "Executed SELECT statement and returned 0 row(s)."),
Map.entry("columns", List.of()),
Map.entry("rows", List.of()), Map.entry("row_object_status", "available"),
Map.entry("row_objects", List.of()),
Map.entry("returned_row_count", 0),
Map.entry("applied_max_rows", 0), Map.entry("applied_timeout_ms", 0),
Map.entry("truncated", false),
Map.entry("next_actions",
resultSetNextActions))),
Arguments.of("update count", (Supplier<SQLExecutionResponse>)
() -> SQLExecutionResponse.updateCount("UPDATE", 2),
Map.ofEntries(Map.entry("response_mode", "executed"),
Map.entry("result_kind", "update_count"), Map.entry("statement_class", "dml"),
- Map.entry("statement_type", "UPDATE"),
Map.entry("status", "OK"), Map.entry("affected_rows", 2),
Map.entry("truncated", false),
+ Map.entry("statement_type", "UPDATE"),
Map.entry("status", "OK"), Map.entry("summary", "Executed UPDATE statement and
affected 2 row(s)."),
+ Map.entry("affected_rows", 2),
Map.entry("truncated", false),
Map.entry("applied_max_rows", 0),
Map.entry("applied_timeout_ms", 0), Map.entry("next_actions",
executionNextActions))),
Arguments.of("statement acknowledgement",
(Supplier<SQLExecutionResponse>) () ->
SQLExecutionResponse.statementAck(SupportedMCPStatement.TRANSACTION_CONTROL,
"COMMIT", "Transaction committed."),
Map.ofEntries(Map.entry("response_mode", "executed"),
Map.entry("result_kind", "statement_ack"), Map.entry("statement_class",
"transaction_control"),
- Map.entry("statement_type", "COMMIT"),
Map.entry("status", "OK"), Map.entry("message", "Transaction committed."),
Map.entry("truncated", false),
- Map.entry("applied_max_rows", 0),
Map.entry("applied_timeout_ms", 0), Map.entry("next_actions",
executionNextActions))));
+ Map.entry("statement_type", "COMMIT"),
Map.entry("status", "OK"), Map.entry("summary", "Transaction committed."),
Map.entry("message", "Transaction committed."),
+ Map.entry("truncated", false),
Map.entry("applied_max_rows", 0), Map.entry("applied_timeout_ms", 0),
Map.entry("next_actions", executionNextActions))));
}
private static List<Map<String, Object>> createNextActions(final String
reason) {
diff --git
a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/descriptor/MCPGuidancePayloadBuilderTest.java
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/descriptor/MCPGuidancePayloadBuilderTest.java
index 945f2a9acb8..be5ee710d52 100644
---
a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/descriptor/MCPGuidancePayloadBuilderTest.java
+++
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/descriptor/MCPGuidancePayloadBuilderTest.java
@@ -51,6 +51,10 @@ class MCPGuidancePayloadBuilderTest {
is("shardingsphere://guidance complements MCP list methods
with ShardingSphere domain guidance, workflow guidance, and side-effect
notes."));
assertThat(actual.get("guidance_resource"),
is("shardingsphere://guidance"));
assertFalse(actual.containsKey("safe_first_resource"));
+ Map<?, ?> actualMetadataRoute =
findByKey(castToRouteList(actual.get("first_call_routes")), "intent",
"inspect_metadata");
+ assertThat(actualMetadataRoute.get("first_action"), is("read_resource
shardingsphere://databases"));
+ Map<?, ?> actualRecoveryRoute =
findByKey(castToRouteList(actual.get("first_call_routes")), "intent",
"recover_error");
+ assertThat(actualRecoveryRoute.get("first_action"), is("follow
top-level next_actions"));
assertThat(((Map<?, ?>) actual.get("preflight_rule")).get("tool"),
is("database_gateway_validate_runtime_database"));
assertThat(castToMap(castToMap(actual.get("sql_tool_selection")).get("side_effecting")).get("execute_requires"),
is("execution_mode=execute"));
Map<?, ?> actualWorkflowRule = castToMap(actual.get("workflow_rule"));
@@ -129,6 +133,11 @@ class MCPGuidancePayloadBuilderTest {
return (Map<?, ?>) value;
}
+ @SuppressWarnings("unchecked")
+ private List<Map<String, Object>> castToRouteList(final Object value) {
+ return (List<Map<String, Object>>) value;
+ }
+
private Map<String, Object> createOfficialDiscoveryMethods() {
return Map.of("tools", "tools/list", "resources", "resources/list",
"resource_templates", "resources/templates/list", "prompts", "prompts/list");
}
diff --git
a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/protocol/MCPModelFacingPayloadContractTest.java
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/protocol/MCPModelFacingPayloadContractTest.java
index 09707bbec39..38ffd711f56 100644
---
a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/protocol/MCPModelFacingPayloadContractTest.java
+++
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/protocol/MCPModelFacingPayloadContractTest.java
@@ -60,7 +60,9 @@ class MCPModelFacingPayloadContractTest {
@Test
void assertGetModelCriticalFieldNames() {
Collection<String> actual =
MCPModelFacingPayloadContract.getModelCriticalFieldNames();
+ assertTrue(actual.contains(MCPPayloadFieldNames.SUMMARY));
assertTrue(actual.contains(MCPPayloadFieldNames.NEXT_ACTIONS));
+ assertTrue(actual.contains(MCPPayloadFieldNames.SELF_RESOURCE));
assertTrue(actual.contains("manual_follow_up"));
}
}
diff --git
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPModelFacingToolResponseFormatterTest.java
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPModelFacingToolResponseFormatterTest.java
index ce053954a62..9dd345a363b 100644
---
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPModelFacingToolResponseFormatterTest.java
+++
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/llm/conversation/LLMMCPModelFacingToolResponseFormatterTest.java
@@ -90,27 +90,31 @@ class LLMMCPModelFacingToolResponseFormatterTest {
@Test
void assertFormatWithModelCriticalFields() {
- Map<String, Object> actual = format(Map.of(
- "resources_to_read", List.of(Map.of("uri",
"shardingsphere://capabilities")),
- "resource", Map.of("uri", "shardingsphere://databases"),
- "parent_resource", Map.of("uri", "shardingsphere://databases"),
- "next_resources", List.of(Map.of("uri",
"shardingsphere://databases/logic_db/schemas")),
- "manual_artifact_summary", "Review DistSQL.",
- "manual_follow_up", "Validate runtime state.",
- "empty_state", Map.of("state", "no_match"),
- "recovery_guidance", "Read metadata before retrying.",
- "remediation", "Fix the mismatch.",
- "ignored", "value"));
- assertThat(actual, is(Map.of(
- "resources_to_read", List.of(Map.of("uri",
"shardingsphere://capabilities")),
- "resource", Map.of("uri", "shardingsphere://databases"),
- "parent_resource", Map.of("uri", "shardingsphere://databases"),
- "next_resources", List.of(Map.of("uri",
"shardingsphere://databases/logic_db/schemas")),
- "manual_artifact_summary", "Review DistSQL.",
- "manual_follow_up", "Validate runtime state.",
- "empty_state", Map.of("state", "no_match"),
- "recovery_guidance", "Read metadata before retrying.",
- "remediation", "Fix the mismatch.")));
+ Map<String, Object> actual = format(Map.ofEntries(
+ Map.entry("summary", "Read logical databases first."),
+ Map.entry("resources_to_read", List.of(Map.of("uri",
"shardingsphere://capabilities"))),
+ Map.entry("resource", Map.of("uri",
"shardingsphere://databases")),
+ Map.entry("self_resource", Map.of("uri",
"shardingsphere://databases")),
+ Map.entry("parent_resource", Map.of("uri",
"shardingsphere://databases")),
+ Map.entry("next_resources", List.of(Map.of("uri",
"shardingsphere://databases/logic_db/schemas"))),
+ Map.entry("manual_artifact_summary", "Review DistSQL."),
+ Map.entry("manual_follow_up", "Validate runtime state."),
+ Map.entry("empty_state", Map.of("state", "no_match")),
+ Map.entry("recovery_guidance", "Read metadata before
retrying."),
+ Map.entry("remediation", "Fix the mismatch."),
+ Map.entry("ignored", "value")));
+ assertThat(actual, is(Map.ofEntries(
+ Map.entry("summary", "Read logical databases first."),
+ Map.entry("resources_to_read", List.of(Map.of("uri",
"shardingsphere://capabilities"))),
+ Map.entry("resource", Map.of("uri",
"shardingsphere://databases")),
+ Map.entry("self_resource", Map.of("uri",
"shardingsphere://databases")),
+ Map.entry("parent_resource", Map.of("uri",
"shardingsphere://databases")),
+ Map.entry("next_resources", List.of(Map.of("uri",
"shardingsphere://databases/logic_db/schemas"))),
+ Map.entry("manual_artifact_summary", "Review DistSQL."),
+ Map.entry("manual_follow_up", "Validate runtime state."),
+ Map.entry("empty_state", Map.of("state", "no_match")),
+ Map.entry("recovery_guidance", "Read metadata before
retrying."),
+ Map.entry("remediation", "Fix the mismatch."))));
}
@Test
diff --git
a/test/e2e/mcp/src/test/resources/baseline-contract/model-contract/guidance.yaml
b/test/e2e/mcp/src/test/resources/baseline-contract/model-contract/guidance.yaml
index 7ddad2afebd..b227826ebaa 100644
---
a/test/e2e/mcp/src/test/resources/baseline-contract/model-contract/guidance.yaml
+++
b/test/e2e/mcp/src/test/resources/baseline-contract/model-contract/guidance.yaml
@@ -24,6 +24,23 @@ model_first_summary:
guidance_resource_role: shardingsphere://guidance complements MCP list
methods with
ShardingSphere domain guidance, workflow guidance, and side-effect notes.
guidance_resource: shardingsphere://guidance
+ first_call_routes:
+ - {intent: inspect_metadata, first_action: read_resource
shardingsphere://databases,
+ next_step: call_tool database_gateway_search_metadata or read returned
resource.uri,
+ stop_rule: Stop after the requested detail resource is read.}
+ - {intent: validate_runtime, first_action: read_resource
shardingsphere://runtime,
+ next_step: call_tool database_gateway_validate_runtime_database with a
configured
+ database, stop_rule: Follow top-level next_actions when validation
fails.}
+ - {intent: read_only_sql, first_action: 'read_resource
shardingsphere://databases/{database}/capabilities',
+ next_step: call_tool database_gateway_execute_query, stop_rule: Stop after
reporting
+ the result rows.}
+ - {intent: side_effect_sql, first_action: call_tool
database_gateway_execute_update
+ execution_mode=preview, next_step: call_tool
database_gateway_execute_update
+ execution_mode=execute, stop_rule: Execute only after preview review
confirms
+ the intended side effect.}
+ - {intent: recover_error, first_action: follow top-level next_actions,
next_step: fallback
+ to recovery.next_actions when top-level actions are absent, stop_rule:
'Ask the
+ user only when no deterministic resource, completion, or tool action is
available.'}
metadata_rule: {first_resource: 'shardingsphere://databases', search_tool:
database_gateway_search_metadata,
detail_rule: Read the returned resource.uri when the list or search
response points
to a detail resource.}