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 5868bfb0cb9 Add proxy preflight validation tool (#38771)
5868bfb0cb9 is described below
commit 5868bfb0cb9fa0ac556fc31f69d9335bf5e6f238
Author: Liang Zhang <[email protected]>
AuthorDate: Tue Jun 2 16:11:10 2026 +0800
Add proxy preflight validation tool (#38771)
* Add proxy preflight validation tool
Add a new database_gateway_validate_proxy_connectivity tool for
ShardingSphere-Proxy preflight validation. Introduce the minimal request
and response models, wire the core handler and descriptor contract, and
reuse runtime connection recovery metadata for validation failures.
Also document the new protocol surface and add focused support/core tests
for request validation, execution flow, descriptor checks, capabilities,
and recovery payload behavior.
* Add proxy preflight validation tool
Add a new database_gateway_validate_proxy_connectivity tool for
ShardingSphere-Proxy preflight validation. Introduce the minimal request
and response models, wire the core handler and descriptor contract, and
reuse runtime connection recovery metadata for validation failures.
Also document the new protocol surface and add focused support/core tests
for request validation, execution flow, descriptor checks, capabilities,
and recovery payload behavior.
---
.../content/reference/mcp/protocol-surface.cn.md | 8 +
.../content/reference/mcp/protocol-surface.en.md | 8 +
.../shardingsphere-mcp/capabilities.cn.md | 16 ++
.../shardingsphere-mcp/capabilities.en.md | 16 ++
.../shardingsphere-mcp/configuration.cn.md | 5 +
.../shardingsphere-mcp/configuration.en.md | 5 +
.../mcp/core/context/MCPRequestScope.java | 13 +-
.../descriptor/CoreToolDescriptorValidator.java | 52 +++-
.../mcp/core/handler/core/CoreToolHandlers.java | 2 +
.../error/MCPBasicRecoveryPayloadFactory.java | 3 +
.../protocol/error/MCPRecoveryPayloadSupport.java | 3 +-
.../ValidateProxyConnectivityToolHandler.java | 67 +++++
.../mcp-descriptors/mcp-descriptor-core.yaml | 97 +++++++
.../mcp/core/context/MCPRequestScopeTest.java | 12 +
.../CoreToolDescriptorValidatorTest.java | 29 +++
.../core/handler/core/CoreHandlerProviderTest.java | 2 +-
.../mcp/core/protocol/MCPErrorConverterTest.java | 10 +
.../capability/ServerCapabilitiesHandlerTest.java | 18 +-
.../tool/handler/ToolDefinitionRegistryTest.java | 28 ++-
.../ValidateProxyConnectivityToolHandlerTest.java | 69 +++++
.../database/MCPDatabaseHandlerContext.java | 11 +
.../jdbc/RuntimeDatabaseConnectionException.java | 13 +
.../request/ProxyPreflightValidationRequest.java | 50 ++++
.../tool/response/ProxyPreflightCheckResult.java | 95 +++++++
.../response/ProxyPreflightValidationResult.java | 93 +++++++
.../service/ProxyPreflightValidationService.java | 206 +++++++++++++++
.../MCPModelFirstContractPayloadBuilder.java | 22 +-
.../RuntimeDatabaseConnectionExceptionTest.java | 7 +
.../ProxyPreflightValidationRequestTest.java | 41 ++-
.../response/ProxyPreflightCheckResultTest.java | 65 +++++
.../ProxyPreflightValidationResultTest.java | 66 +++++
.../ProxyPreflightValidationServiceTest.java | 280 +++++++++++++++++++++
.../AbstractProductionMySQLRuntimeE2ETest.java | 2 +
.../production/PackagedDistributionE2ETest.java | 12 +-
.../production/ProductionMySQLRuntimeE2ETest.java | 1 +
.../programmatic/HttpTransportContractE2ETest.java | 2 +-
.../test/e2e/mcp/support/OfficialMCPToolNames.java | 1 +
.../model-contract/capabilities.yaml | 19 +-
38 files changed, 1395 insertions(+), 54 deletions(-)
diff --git a/docs/document/content/reference/mcp/protocol-surface.cn.md
b/docs/document/content/reference/mcp/protocol-surface.cn.md
index 4b85016806f..ae8952199d8 100644
--- a/docs/document/content/reference/mcp/protocol-surface.cn.md
+++ b/docs/document/content/reference/mcp/protocol-surface.cn.md
@@ -42,6 +42,14 @@ ShardingSphere-MCP 不要求 roots,也不会发送 `sampling/createMessage`
- 可按 `database`、`schema`、`query`、`object_types` 收窄范围。
- `object_types` 支持
`database`、`schema`、`table`、`view`、`column`、`index`、`sequence`。
+`database_gateway_validate_proxy_connectivity`
+
+- 在正式接入前校验已配置的运行时数据库。
+- 必填输入为 `database`。
+- 使用管理员已配置的运行时数据库连接信息;JDBC URL、用户名、密码和驱动类名不是工具输入。
+- 返回 `status`、有序 `checks`、整体 `category` 和结构化 `recovery` 对象。
+- 常见失败分类包括
`missing_jdbc_driver`、`authentication_failed`、`authorization_failed`、`connection_timeout`、`invalid_configuration`、`database_unavailable`、`connection_failed`
和 `database_not_visible`。
+
`database_gateway_execute_query`
- 执行一个 classifier 允许的 `SELECT` 或 `EXPLAIN ANALYZE`。
diff --git a/docs/document/content/reference/mcp/protocol-surface.en.md
b/docs/document/content/reference/mcp/protocol-surface.en.md
index 732af9b40d8..54a1e0d8371 100644
--- a/docs/document/content/reference/mcp/protocol-surface.en.md
+++ b/docs/document/content/reference/mcp/protocol-surface.en.md
@@ -42,6 +42,14 @@ ShardingSphere-MCP does not require roots and does not send
`sampling/createMess
- Narrows scope by `database`, `schema`, `query`, and `object_types`.
- `object_types` supports `database`, `schema`, `table`, `view`, `column`,
`index`, and `sequence`.
+`database_gateway_validate_proxy_connectivity`
+
+- Validates a configured runtime database before formal onboarding.
+- Required input is `database`.
+- Uses the administrator-configured runtime database connection details; JDBC
URL, username, password, and driver class are not tool inputs.
+- Returns `status`, ordered `checks`, overall `category`, and a structured
`recovery` object.
+- Common failure categories include `missing_jdbc_driver`,
`authentication_failed`, `authorization_failed`, `connection_timeout`,
`invalid_configuration`, `database_unavailable`, `connection_failed`, and
`database_not_visible`.
+
`database_gateway_execute_query`
- Executes one classifier-approved `SELECT` or `EXPLAIN ANALYZE`.
diff --git
a/docs/document/content/user-manual/shardingsphere-mcp/capabilities.cn.md
b/docs/document/content/user-manual/shardingsphere-mcp/capabilities.cn.md
index 4a844247e38..98b18daf76e 100644
--- a/docs/document/content/user-manual/shardingsphere-mcp/capabilities.cn.md
+++ b/docs/document/content/user-manual/shardingsphere-mcp/capabilities.cn.md
@@ -79,6 +79,7 @@ weight = 2
| 工具 | 用途 | 自然语言示例 | 副作用 |
| --- | --- | --- | --- |
| `database_gateway_search_metadata` | 按名称片段和对象类型搜索运行时数据库元数据,并返回后续资源读取提示。 |
“查找名字包含 `order` 的表。” | 无。 |
+| `database_gateway_validate_proxy_connectivity` |
在正式接入前校验已配置的运行时数据库,包括驱动加载、JDBC 连通性、metadata 可读性和数据库可见性。 | “先检查已配置的 `logic_db`
能不能接入,再注册。” | 无。 |
| `database_gateway_execute_query` | 执行一个已判定为查询类的 `SELECT` 或 `EXPLAIN
ANALYZE`。 | “查询 `orders` 表前 10 行。” | 无;拒绝 DML、DDL、DCL、事务控制、savepoint 和其他有副作用
SQL。 |
| `database_gateway_execute_update` | 预览或执行一个可能修改数据、元数据、规则或事务状态的 SQL。 |
“预览这条变更 SQL,先不要执行。” | 有;应先预览并确认。 |
| `database_gateway_apply_workflow` | 预览、执行或导出功能插件生成的治理变更计划。 | “先预览刚才的加密规则计划。”
| 取决于执行方式;预览和人工执行包不修改运行时状态。 |
@@ -86,6 +87,21 @@ weight = 2
插件工具在对应插件页面说明。
+### Proxy 预检结果
+
+`database_gateway_validate_proxy_connectivity` 返回固定结构的校验结果,顶层字段包括:
+
+- `response_mode`
+- `status`
+- `database`
+- `checks`
+- `category`
+- `recovery`
+
+常见失败分类包括
`missing_jdbc_driver`、`authentication_failed`、`authorization_failed`、`connection_timeout`、`invalid_configuration`、`database_unavailable`、`connection_failed`
和 `database_not_visible`。
+`recovery` 字段沿用运行时数据库连接失败的 secret-safe 恢复风格。
+该工具只接受已配置的 `database` 名称。JDBC URL、用户名、密码和驱动类名等连接细节保留在运行时配置中。
+
## 提示
提示用于任务引导,例如先读取哪些信息、如何处理 SQL 执行边界、如何从失败中恢复。
diff --git
a/docs/document/content/user-manual/shardingsphere-mcp/capabilities.en.md
b/docs/document/content/user-manual/shardingsphere-mcp/capabilities.en.md
index c99d027dfbf..0ee5ae7f94d 100644
--- a/docs/document/content/user-manual/shardingsphere-mcp/capabilities.en.md
+++ b/docs/document/content/user-manual/shardingsphere-mcp/capabilities.en.md
@@ -79,6 +79,7 @@ Actions with side effects should be previewed or reviewed
first.
| Tool | Purpose | Natural language example | Side effects |
| --- | --- | --- | --- |
| `database_gateway_search_metadata` | Search runtime database metadata by
name fragment and object type, and return resource hints for follow-up reads. |
"Find tables whose names contain `order`." | None. |
+| `database_gateway_validate_proxy_connectivity` | Validate a configured
runtime database, including driver loading, JDBC connectivity, metadata
readability, and database visibility before formal onboarding. | "Check whether
configured database `logic_db` is ready before we register it." | None. |
| `database_gateway_execute_query` | Execute exactly one classifier-approved
`SELECT` or `EXPLAIN ANALYZE` statement. | "Query the first 10 rows from
`orders`." | None; rejects DML, DDL, DCL, transaction control, savepoints, and
other side-effecting SQL. |
| `database_gateway_execute_update` | Preview or execute one SQL statement
that may mutate data, metadata, rules, or transaction state. | "Preview this
change SQL without executing it." | Yes; preview and confirmation are
recommended first. |
| `database_gateway_apply_workflow` | Preview, execute, or export a governance
change plan created by a feature plugin. | "Preview the previous encryption
rule plan first." | Depends on the execution choice; preview and manual
packages do not change runtime state. |
@@ -86,6 +87,21 @@ Actions with side effects should be previewed or reviewed
first.
Plugin tools are documented on the corresponding plugin pages.
+### Proxy preflight validation output
+
+`database_gateway_validate_proxy_connectivity` returns a structured validation
payload with these top-level fields:
+
+- `response_mode`
+- `status`
+- `database`
+- `checks`
+- `category`
+- `recovery`
+
+Common failure categories include `missing_jdbc_driver`,
`authentication_failed`, `authorization_failed`, `connection_timeout`,
`invalid_configuration`, `database_unavailable`, `connection_failed`, and
`database_not_visible`.
+The `recovery` field follows the same secret-safe runtime recovery style used
by runtime database connection failures.
+The tool only accepts the configured `database` name. Connection details such
as JDBC URL, username, password, and driver class stay in the runtime
configuration.
+
## Prompts
Prompts provide task guidance, such as which information to read first, how to
handle SQL execution boundaries, or how to recover from failure.
diff --git
a/docs/document/content/user-manual/shardingsphere-mcp/configuration.cn.md
b/docs/document/content/user-manual/shardingsphere-mcp/configuration.cn.md
index d15d0da3326..00c4b495964 100644
--- a/docs/document/content/user-manual/shardingsphere-mcp/configuration.cn.md
+++ b/docs/document/content/user-manual/shardingsphere-mcp/configuration.cn.md
@@ -59,6 +59,11 @@ runtimeDatabases:
| `password (?)` | 连接运行时数据库的密码。 |
| `driverClassName (+)` | JDBC 驱动类名,例如 MySQL 驱动使用 `com.mysql.cj.jdbc.Driver`。 |
+说明:
+
+- `(+)` 表示必填项。
+- `(?)` 表示可选项。
+
注意事项:
- 连接 ShardingSphere-Proxy 时,MCP 资源暴露的是 ShardingSphere 逻辑库,不是底层物理存储单元。
diff --git
a/docs/document/content/user-manual/shardingsphere-mcp/configuration.en.md
b/docs/document/content/user-manual/shardingsphere-mcp/configuration.en.md
index 11c17836613..e6b337e9e91 100644
--- a/docs/document/content/user-manual/shardingsphere-mcp/configuration.en.md
+++ b/docs/document/content/user-manual/shardingsphere-mcp/configuration.en.md
@@ -59,6 +59,11 @@ runtimeDatabases:
| `password (?)` | Password for the runtime database. |
| `driverClassName (+)` | JDBC driver class name, such as
`com.mysql.cj.jdbc.Driver` for the MySQL driver. |
+Legend:
+
+- `(+)` means required.
+- `(?)` means optional.
+
Notes:
- When the target is ShardingSphere-Proxy, MCP resources expose ShardingSphere
logical databases, not physical storage units.
diff --git
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/context/MCPRequestScope.java
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/context/MCPRequestScope.java
index 647277fcf16..60c8cc3ed9f 100644
---
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/context/MCPRequestScope.java
+++
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/context/MCPRequestScope.java
@@ -25,6 +25,7 @@ import
org.apache.shardingsphere.mcp.core.tool.handler.execute.MCPSQLExecutionFa
import org.apache.shardingsphere.mcp.core.workflow.WorkflowProxyQueryService;
import
org.apache.shardingsphere.mcp.support.database.MCPDatabaseHandlerContext;
import
org.apache.shardingsphere.mcp.support.database.capability.MCPDatabaseCapabilityProvider;
+import
org.apache.shardingsphere.mcp.support.database.metadata.jdbc.RuntimeDatabaseConfiguration;
import
org.apache.shardingsphere.mcp.support.database.metadata.context.RequestScopedMetadataContext;
import
org.apache.shardingsphere.mcp.support.database.metadata.query.MetadataQueryService;
import
org.apache.shardingsphere.mcp.support.database.spi.MCPFeatureCapabilityFacade;
@@ -34,6 +35,7 @@ import
org.apache.shardingsphere.mcp.support.database.spi.MCPMetadataQueryFacade
import
org.apache.shardingsphere.mcp.support.workflow.MCPWorkflowHandlerContext;
import org.apache.shardingsphere.mcp.support.workflow.WorkflowSessionContext;
+import java.util.Map;
import java.util.Optional;
/**
@@ -47,6 +49,9 @@ public final class MCPRequestScope implements
MCPServiceHandlerContext, MCPDatab
@Getter(AccessLevel.NONE)
private final MCPDatabaseCapabilityProvider databaseCapabilityProvider;
+ @Getter(AccessLevel.NONE)
+ private final Map<String, RuntimeDatabaseConfiguration> runtimeDatabases;
+
@Getter(AccessLevel.NONE)
private final RequestScopedMetadataContext metadataContext;
@@ -79,7 +84,8 @@ public final class MCPRequestScope implements
MCPServiceHandlerContext, MCPDatab
MCPSessionManager sessionManager = runtimeContext.getSessionManager();
activeTransport = runtimeContext.getActiveTransport();
databaseCapabilityProvider =
runtimeContext.getDatabaseCapabilityProvider();
- metadataContext = new
RequestScopedMetadataContext(sessionManager.getTransactionResourceManager().getRuntimeDatabases(),
databaseCapabilityProvider);
+ runtimeDatabases =
sessionManager.getTransactionResourceManager().getRuntimeDatabases();
+ metadataContext = new RequestScopedMetadataContext(runtimeDatabases,
databaseCapabilityProvider);
sessionAttribution = sessionManager.findSessionAttribution(sessionId);
workflowSessionContext = runtimeContext.getWorkflowSessionContext();
metadataQueryFacade = new
MetadataQueryService(databaseCapabilityProvider, metadataContext);
@@ -97,6 +103,11 @@ public final class MCPRequestScope implements
MCPServiceHandlerContext, MCPDatab
return databaseCapabilityProvider;
}
+ @Override
+ public Optional<RuntimeDatabaseConfiguration>
findRuntimeDatabaseConfiguration(final String databaseName) {
+ return Optional.ofNullable(runtimeDatabases.get(databaseName));
+ }
+
@Override
public Optional<MCPSessionAttribution> findSessionAttribution() {
return sessionAttribution;
diff --git
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/descriptor/CoreToolDescriptorValidator.java
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/descriptor/CoreToolDescriptorValidator.java
index 5f74312d0c7..fac0c532ac3 100644
---
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/descriptor/CoreToolDescriptorValidator.java
+++
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/descriptor/CoreToolDescriptorValidator.java
@@ -40,7 +40,9 @@ public final class CoreToolDescriptorValidator implements
MCPToolDescriptorValid
private static final String EXECUTE_UPDATE =
"database_gateway_execute_update";
- private static final Set<String> SUPPORTED_TOOLS = Set.of(SEARCH_METADATA,
EXECUTE_QUERY, EXECUTE_UPDATE);
+ private static final String VALIDATE_PROXY_CONNECTIVITY =
"database_gateway_validate_proxy_connectivity";
+
+ private static final Set<String> SUPPORTED_TOOLS = Set.of(SEARCH_METADATA,
VALIDATE_PROXY_CONNECTIVITY, EXECUTE_QUERY, EXECUTE_UPDATE);
@Override
public boolean supports(final MCPToolDescriptor toolDescriptor) {
@@ -60,6 +62,12 @@ public final class CoreToolDescriptorValidator implements
MCPToolDescriptorValid
"applied_max_rows", "applied_timeout_ms", "truncated",
MCPPayloadFieldNames.NEXT_ACTIONS));
return;
}
+ if (VALIDATE_PROXY_CONNECTIVITY.equals(toolDescriptor.getName())) {
+
MCPToolDescriptorValidationUtils.validateRequiredOutputFields(toolDescriptor,
List.of("response_mode", "status", "database", "checks", "category",
MCPPayloadFieldNames.RECOVERY));
+ validateProxyConnectivityContract(toolDescriptor);
+ validateProxyConnectivityChecks(toolDescriptor);
+ return;
+ }
MCPToolDescriptorValidationUtils.validateRequiredOutputFields(toolDescriptor,
List.of("response_mode", "result_kind", "statement_class", "statement_type",
"status", "returned_row_count",
"applied_max_rows", "applied_timeout_ms",
"suggested_arguments", MCPPayloadFieldNames.NEXT_ACTIONS));
validateExecuteUpdateDescriptor(toolDescriptor);
@@ -103,6 +111,39 @@ public final class CoreToolDescriptorValidator implements
MCPToolDescriptorValid
() -> new IllegalStateException("Tool
`database_gateway_execute_update` execution_mode must allow execute and
preview."));
}
+ private void validateProxyConnectivityContract(final MCPToolDescriptor
descriptor) {
+ Map<?, ?> responseMode = findToolOutputProperty(descriptor,
"response_mode").orElseThrow(
+ () -> new IllegalStateException("Tool
`database_gateway_validate_proxy_connectivity` must declare response_mode."));
+ Object responseModes = responseMode.get("enum");
+ ShardingSpherePreconditions.checkState(responseModes instanceof
Collection && ((Collection<?>) responseModes).contains("validation"),
+ () -> new IllegalStateException("Tool
`database_gateway_validate_proxy_connectivity` response_mode must allow
validation."));
+
ShardingSpherePreconditions.checkState(findToolInputProperty(descriptor,
"database").isPresent(),
+ () -> new IllegalStateException("Tool
`database_gateway_validate_proxy_connectivity` must declare `database`."));
+ ShardingSpherePreconditions.checkState(isRequiredToolInput(descriptor,
"database"),
+ () -> new IllegalStateException("Tool
`database_gateway_validate_proxy_connectivity` database must be required."));
+ for (String each : List.of("databaseType", "jdbcUrl", "username",
"password", "driverClassName")) {
+
ShardingSpherePreconditions.checkState(findToolInputProperty(descriptor,
each).isEmpty(),
+ () -> new IllegalStateException(String.format("Tool
`database_gateway_validate_proxy_connectivity` must not expose `%s`.", each)));
+ }
+ }
+
+ private void validateProxyConnectivityChecks(final MCPToolDescriptor
descriptor) {
+ Map<?, ?> properties = (Map<?, ?>)
descriptor.getOutputSchema().get("properties");
+ Object checks = properties.get("checks");
+ ShardingSpherePreconditions.checkState(checks instanceof Map,
+ () -> new IllegalStateException("Tool
`database_gateway_validate_proxy_connectivity` outputSchema property `checks`
must be an object."));
+ Object checkItemSchema = ((Map<?, ?>) checks).get("items");
+ ShardingSpherePreconditions.checkState(checkItemSchema instanceof Map,
+ () -> new IllegalStateException("Tool
`database_gateway_validate_proxy_connectivity` outputSchema property
`checks.items` must be an object."));
+ Object checkItemProperties = ((Map<?, ?>)
checkItemSchema).get("properties");
+ ShardingSpherePreconditions.checkState(checkItemProperties instanceof
Map && !((Map<?, ?>) checkItemProperties).isEmpty(),
+ () -> new IllegalStateException("Tool
`database_gateway_validate_proxy_connectivity` outputSchema property
`checks.items.properties` must declare properties."));
+ for (String each : List.of("name", "status", "category", "message")) {
+ ShardingSpherePreconditions.checkState(((Map<?, ?>)
checkItemProperties).containsKey(each),
+ () -> new IllegalStateException(String.format("Tool
`database_gateway_validate_proxy_connectivity` outputSchema check item must
declare `%s`.", each)));
+ }
+ }
+
private Optional<Map<?, ?>> findToolInputProperty(final MCPToolDescriptor
descriptor, final String fieldName) {
Object properties = descriptor.getInputSchema().get("properties");
if (!(properties instanceof Map)) {
@@ -112,6 +153,15 @@ public final class CoreToolDescriptorValidator implements
MCPToolDescriptorValid
return property instanceof Map ? Optional.of((Map<?, ?>) property) :
Optional.empty();
}
+ private Optional<Map<?, ?>> findToolOutputProperty(final MCPToolDescriptor
descriptor, final String fieldName) {
+ Object properties = descriptor.getOutputSchema().get("properties");
+ if (!(properties instanceof Map)) {
+ return Optional.empty();
+ }
+ Object property = ((Map<?, ?>) properties).get(fieldName);
+ return property instanceof Map ? Optional.of((Map<?, ?>) property) :
Optional.empty();
+ }
+
private boolean isRequiredToolInput(final MCPToolDescriptor descriptor,
final String fieldName) {
Object required = descriptor.getInputSchema().get("required");
return required instanceof Collection && ((Collection<?>)
required).contains(fieldName);
diff --git
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/handler/core/CoreToolHandlers.java
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/handler/core/CoreToolHandlers.java
index e476812cbd6..eaf79b953a5 100644
---
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/handler/core/CoreToolHandlers.java
+++
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/handler/core/CoreToolHandlers.java
@@ -24,6 +24,7 @@ import
org.apache.shardingsphere.mcp.core.workflow.WorkflowRuntimeDefinitionRegi
import
org.apache.shardingsphere.mcp.core.tool.handler.execute.ExecuteQueryToolHandler;
import
org.apache.shardingsphere.mcp.core.tool.handler.execute.ExecuteUpdateToolHandler;
import
org.apache.shardingsphere.mcp.core.tool.handler.metadata.SearchMetadataToolHandler;
+import
org.apache.shardingsphere.mcp.core.tool.handler.metadata.ValidateProxyConnectivityToolHandler;
import
org.apache.shardingsphere.mcp.core.tool.handler.workflow.WorkflowExecutionToolHandler;
import
org.apache.shardingsphere.mcp.core.tool.handler.workflow.WorkflowValidationToolHandler;
@@ -40,6 +41,7 @@ final class CoreToolHandlers {
WorkflowRuntimeDefinitionRegistry workflowRuntimeDefinitionRegistry =
WorkflowRuntimeDefinitionRegistry.load();
return List.of(
new SearchMetadataToolHandler(),
+ new ValidateProxyConnectivityToolHandler(),
new ExecuteQueryToolHandler(),
new ExecuteUpdateToolHandler(),
new
WorkflowExecutionToolHandler(workflowRuntimeDefinitionRegistry),
diff --git
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPBasicRecoveryPayloadFactory.java
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPBasicRecoveryPayloadFactory.java
index 7a5ed3eb673..e83882bbf77 100644
---
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPBasicRecoveryPayloadFactory.java
+++
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPBasicRecoveryPayloadFactory.java
@@ -225,6 +225,9 @@ final class MCPBasicRecoveryPayloadFactory {
if
(RuntimeDatabaseConnectionException.CATEGORY_INVALID_CONFIGURATION.equals(cause.getCategory()))
{
return "Fix the MCP runtime database configuration outside MCP,
then retry.";
}
+ if
(RuntimeDatabaseConnectionException.CATEGORY_DATABASE_NOT_VISIBLE.equals(cause.getCategory()))
{
+ return "Connect to the intended logical database or update the
expected database name before retrying.";
+ }
return "Check the runtime database availability and configuration,
then retry.";
}
}
diff --git
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPRecoveryPayloadSupport.java
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPRecoveryPayloadSupport.java
index 67760f1324c..daa138c5186 100644
---
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPRecoveryPayloadSupport.java
+++
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/protocol/error/MCPRecoveryPayloadSupport.java
@@ -87,6 +87,7 @@ final class MCPRecoveryPayloadSupport {
return
RuntimeDatabaseConnectionException.CATEGORY_MISSING_JDBC_DRIVER.equals(category)
||
RuntimeDatabaseConnectionException.CATEGORY_AUTHENTICATION_FAILED.equals(category)
||
RuntimeDatabaseConnectionException.CATEGORY_AUTHORIZATION_FAILED.equals(category)
||
RuntimeDatabaseConnectionException.CATEGORY_CONNECTION_TIMEOUT.equals(category)
||
RuntimeDatabaseConnectionException.CATEGORY_INVALID_CONFIGURATION.equals(category)
- ||
RuntimeDatabaseConnectionException.CATEGORY_DATABASE_UNAVAILABLE.equals(category)
||
RuntimeDatabaseConnectionException.CATEGORY_CONNECTION_FAILED.equals(category);
+ ||
RuntimeDatabaseConnectionException.CATEGORY_DATABASE_UNAVAILABLE.equals(category)
+ ||
RuntimeDatabaseConnectionException.CATEGORY_CONNECTION_FAILED.equals(category);
}
}
diff --git
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/metadata/ValidateProxyConnectivityToolHandler.java
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/metadata/ValidateProxyConnectivityToolHandler.java
new file mode 100644
index 00000000000..4c07259e434
--- /dev/null
+++
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/tool/handler/metadata/ValidateProxyConnectivityToolHandler.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.mcp.core.tool.handler.metadata;
+
+import lombok.RequiredArgsConstructor;
+import org.apache.shardingsphere.mcp.api.protocol.response.MCPResponse;
+import org.apache.shardingsphere.mcp.api.tool.MCPToolCall;
+import org.apache.shardingsphere.mcp.api.tool.MCPToolHandler;
+import org.apache.shardingsphere.mcp.core.protocol.error.MCPErrorConverter;
+import
org.apache.shardingsphere.mcp.support.database.MCPDatabaseHandlerContext;
+import
org.apache.shardingsphere.mcp.support.database.metadata.jdbc.RuntimeDatabaseConnectionException;
+import
org.apache.shardingsphere.mcp.support.database.tool.request.ProxyPreflightValidationRequest;
+import
org.apache.shardingsphere.mcp.support.database.tool.service.ProxyPreflightValidationService;
+import org.apache.shardingsphere.mcp.support.protocol.MCPPayloadFieldNames;
+
+import java.util.Map;
+
+/**
+ * Handler for proxy connectivity validation tool.
+ */
+@RequiredArgsConstructor
+public final class ValidateProxyConnectivityToolHandler implements
MCPToolHandler<MCPDatabaseHandlerContext> {
+
+ public static final String TOOL_NAME =
"database_gateway_validate_proxy_connectivity";
+
+ private final ProxyPreflightValidationService validationService;
+
+ public ValidateProxyConnectivityToolHandler() {
+ this(new ProxyPreflightValidationService());
+ }
+
+ @Override
+ public Class<MCPDatabaseHandlerContext> getContextType() {
+ return MCPDatabaseHandlerContext.class;
+ }
+
+ @Override
+ public String getToolName() {
+ return TOOL_NAME;
+ }
+
+ @Override
+ public MCPResponse handle(final MCPDatabaseHandlerContext databaseContext,
final MCPToolCall toolCall) {
+ return
validationService.validate(ProxyPreflightValidationRequest.from(toolCall.getArguments()),
databaseContext::findRuntimeDatabaseConfiguration,
this::createRecoveryPayload);
+ }
+
+ @SuppressWarnings("unchecked")
+ private Map<String, Object> createRecoveryPayload(final
RuntimeDatabaseConnectionException cause) {
+ Object result =
MCPErrorConverter.convert(cause).toPayload().get(MCPPayloadFieldNames.RECOVERY);
+ return result instanceof Map ? (Map<String, Object>) result : Map.of();
+ }
+}
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 8283e9bb597..e724b9f3cbb 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
@@ -795,6 +795,103 @@ tools:
org.apache.shardingsphere/related-resource-uris:
- shardingsphere://databases
- "shardingsphere://databases/{database}/schemas"
+ - name: database_gateway_validate_proxy_connectivity
+ title: Validate Proxy Connectivity
+ description: >-
+ Validate one configured ShardingSphere-MCP runtime database before
formal onboarding. Use this to preflight the configured database type,
+ JDBC driver, JDBC connectivity, metadata readability, and database
visibility without executing SQL tools.
+ inputSchema:
+ type: object
+ properties:
+ database:
+ type: string
+ description: "Configured runtime database name to validate."
+ required:
+ - database
+ additionalProperties: false
+ outputSchema:
+ type: object
+ properties:
+ response_mode:
+ type: string
+ description: "Stable response mode marker for proxy preflight
validation payloads."
+ enum:
+ - validation
+ status:
+ type: string
+ description: "Overall preflight status: ready when all required
checks passed, failed otherwise."
+ enum:
+ - ready
+ - failed
+ database:
+ type: string
+ description: "Validated runtime database name."
+ checks:
+ type: array
+ description: "Ordered per-step validation results for configuration
lookup, driver loading, JDBC connectivity, metadata readability, and database
visibility."
+ items:
+ type: object
+ description: "One preflight validation check result."
+ properties:
+ name:
+ type: string
+ description: "Stable validation step name."
+ status:
+ type: string
+ description: "Per-check result status."
+ enum:
+ - passed
+ - failed
+ - skipped
+ category:
+ type: string
+ description: "Ready, skipped, or a runtime connection failure
category reused from the runtime recovery contract."
+ message:
+ type: string
+ description: "Human-readable summary of the check result."
+ category:
+ type: string
+ description: "Overall result category. ready indicates success;
other values reuse runtime database connection failure categories."
+ recovery:
+ type: object
+ description: "Structured runtime recovery payload. Empty when status
is ready."
+ examples:
+ - response_mode: validation
+ status: ready
+ database: logic_db
+ checks:
+ - name: configuration
+ status: passed
+ category: ready
+ message: Resolved the configured runtime database.
+ - name: jdbc_driver
+ status: passed
+ category: ready
+ message: Loaded the configured JDBC driver.
+ - name: jdbc_connectivity
+ status: passed
+ category: ready
+ message: Opened a JDBC connection and validated the configured
database type.
+ - name: metadata_read
+ status: passed
+ category: ready
+ message: Read metadata through the configured JDBC connection.
+ - name: database_visibility
+ status: passed
+ category: ready
+ message: Validated the requested database name against visible
JDBC metadata and connection context.
+ category: ready
+ recovery: {}
+ annotations:
+ title: Validate Proxy Connectivity
+ readOnlyHint: true
+ destructiveHint: false
+ idempotentHint: true
+ openWorldHint: true
+ meta:
+ org.apache.shardingsphere/related-resource-uris:
+ - shardingsphere://runtime
+ - shardingsphere://capabilities
- name: database_gateway_execute_query
title: Execute Query SQL
description: >-
diff --git
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/context/MCPRequestScopeTest.java
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/context/MCPRequestScopeTest.java
index 572c7c19149..01ffcdd768c 100644
---
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/context/MCPRequestScopeTest.java
+++
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/context/MCPRequestScopeTest.java
@@ -20,6 +20,7 @@ package org.apache.shardingsphere.mcp.core.context;
import org.apache.shardingsphere.mcp.api.session.MCPSessionAttribution;
import org.apache.shardingsphere.mcp.core.session.MCPSessionManager;
import
org.apache.shardingsphere.mcp.support.database.capability.MCPDatabaseCapabilityProvider;
+import
org.apache.shardingsphere.mcp.support.database.metadata.jdbc.RuntimeDatabaseConfiguration;
import org.junit.jupiter.api.Test;
import java.util.Map;
@@ -49,4 +50,15 @@ class MCPRequestScopeTest {
assertThat(requestScope.findSessionAttribution(),
is(Optional.empty()));
}
}
+
+ @Test
+ void assertFindRuntimeDatabaseConfiguration() {
+ RuntimeDatabaseConfiguration runtimeDatabaseConfig = new
RuntimeDatabaseConfiguration("MySQL", "jdbc:test:profile", "demo", "",
"com.mysql.cj.jdbc.Driver");
+ MCPRuntimeContext runtimeContext = new MCPRuntimeContext(new
MCPSessionManager(Map.of("logic_db", runtimeDatabaseConfig)),
+ new MCPDatabaseCapabilityProvider(Map.of()), "http");
+ try (MCPRequestScope requestScope = new
MCPRequestScope(runtimeContext)) {
+
assertThat(requestScope.findRuntimeDatabaseConfiguration("logic_db"),
is(Optional.of(runtimeDatabaseConfig)));
+
assertThat(requestScope.findRuntimeDatabaseConfiguration("missing_db"),
is(Optional.empty()));
+ }
+ }
}
diff --git
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/descriptor/CoreToolDescriptorValidatorTest.java
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/descriptor/CoreToolDescriptorValidatorTest.java
index cd4075c0707..130677517d3 100644
---
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/descriptor/CoreToolDescriptorValidatorTest.java
+++
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/descriptor/CoreToolDescriptorValidatorTest.java
@@ -22,6 +22,7 @@ import
org.apache.shardingsphere.mcp.support.descriptor.MCPDescriptorCatalogInde
import org.junit.jupiter.api.Test;
import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
import static org.hamcrest.MatcherAssert.assertThat;
@@ -36,6 +37,11 @@ class CoreToolDescriptorValidatorTest {
assertTrue(new
CoreToolDescriptorValidator().supports(MCPDescriptorCatalogIndex.getRequiredToolDescriptor("database_gateway_search_metadata")));
}
+ @Test
+ void assertSupportsValidateProxyConnectivity() {
+ assertTrue(new
CoreToolDescriptorValidator().supports(MCPDescriptorCatalogIndex.getRequiredToolDescriptor("database_gateway_validate_proxy_connectivity")));
+ }
+
@Test
void assertSearchMetadataDocumentsCompleteSearchResult() {
MCPToolDescriptor descriptor =
MCPDescriptorCatalogIndex.getRequiredToolDescriptor("database_gateway_search_metadata");
@@ -76,6 +82,29 @@ class CoreToolDescriptorValidatorTest {
assertThat(actual.getMessage(), is("Tool
`database_gateway_execute_update` must declare execution_mode."));
}
+ @Test
+ @SuppressWarnings("unchecked")
+ void assertValidateRejectsExposedProxyConnectivityJdbcUrl() {
+ MCPToolDescriptor descriptor =
MCPDescriptorCatalogIndex.getRequiredToolDescriptor("database_gateway_validate_proxy_connectivity");
+ Map<String, Object> inputSchema = new
LinkedHashMap<>(descriptor.getInputSchema());
+ Map<String, Object> properties = new LinkedHashMap<>((Map<String,
Object>) inputSchema.get("properties"));
+ properties.put("jdbcUrl", Map.of("type", "string", "description",
"JDBC URL."));
+ inputSchema.put("properties", properties);
+ IllegalStateException actual =
assertThrows(IllegalStateException.class, () -> new
CoreToolDescriptorValidator().validate(new MCPToolDescriptor(
+ descriptor.getName(), descriptor.getTitle(),
descriptor.getDescription(), inputSchema, descriptor.getOutputSchema(),
descriptor.getAnnotations(), descriptor.getMeta())));
+ assertThat(actual.getMessage(), is("Tool
`database_gateway_validate_proxy_connectivity` must not expose `jdbcUrl`."));
+ }
+
+ @Test
+ void assertValidateRejectsOptionalProxyConnectivityDatabase() {
+ MCPToolDescriptor descriptor =
MCPDescriptorCatalogIndex.getRequiredToolDescriptor("database_gateway_validate_proxy_connectivity");
+ Map<String, Object> inputSchema = new
LinkedHashMap<>(descriptor.getInputSchema());
+ inputSchema.put("required", List.of());
+ IllegalStateException actual =
assertThrows(IllegalStateException.class, () -> new
CoreToolDescriptorValidator().validate(new MCPToolDescriptor(
+ descriptor.getName(), descriptor.getTitle(),
descriptor.getDescription(), inputSchema, descriptor.getOutputSchema(),
descriptor.getAnnotations(), descriptor.getMeta())));
+ assertThat(actual.getMessage(), is("Tool
`database_gateway_validate_proxy_connectivity` database must be required."));
+ }
+
private Map<?, ?> getInputProperties(final MCPToolDescriptor
toolDescriptor) {
return (Map<?, ?>) toolDescriptor.getInputSchema().get("properties");
}
diff --git
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/handler/core/CoreHandlerProviderTest.java
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/handler/core/CoreHandlerProviderTest.java
index a4673c71ec8..23c3e580f99 100644
---
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/handler/core/CoreHandlerProviderTest.java
+++
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/handler/core/CoreHandlerProviderTest.java
@@ -34,7 +34,7 @@ class CoreHandlerProviderTest {
void assertGetToolHandlers() {
Collection<MCPToolHandler<?>> actual = new
CoreHandlerProvider().getToolHandlers();
assertThat(actual.stream().map(MCPToolHandler::getToolName).toList(),
- is(List.of("database_gateway_search_metadata",
"database_gateway_execute_query", "database_gateway_execute_update",
+ is(List.of("database_gateway_search_metadata",
"database_gateway_validate_proxy_connectivity",
"database_gateway_execute_query", "database_gateway_execute_update",
"database_gateway_apply_workflow",
"database_gateway_validate_workflow")));
}
diff --git
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/protocol/MCPErrorConverterTest.java
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/protocol/MCPErrorConverterTest.java
index 4bf465ac7f6..715a2609ddd 100644
---
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/protocol/MCPErrorConverterTest.java
+++
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/protocol/MCPErrorConverterTest.java
@@ -374,6 +374,16 @@ class MCPErrorConverterTest {
assertThat(actualRecovery.get("model_action"), is("Check runtime
database account privileges outside MCP, then retry."));
}
+ @Test
+ void assertConvertRuntimeDatabaseNotVisibleWithRecovery() {
+ Map<String, Object> actual =
MCPErrorConverter.convert(RuntimeDatabaseConnectionException.databaseNotVisible("logic_db",
new IllegalStateException("not visible"))).toPayload();
+ Map<?, ?> actualRecovery = (Map<?, ?>) actual.get("recovery");
+ assertThat(actualRecovery.get("category"), is("database_not_visible"));
+ assertThat(actualRecovery.get("recovery_category"), is("validation"));
+ assertThat(actualRecovery.get("database"), is("logic_db"));
+ assertThat(actualRecovery.get("model_action"), is("Connect to the
intended logical database or update the expected database name before
retrying."));
+ }
+
@Test
void assertConvertToolCallLimitExceededWithRecovery() {
Map<String, Object> actual = MCPErrorConverter.convert(new
MCPToolCallLimitExceededException("session-1",
"database_gateway_search_metadata", 1)).toPayload();
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 be4601db195..171fa3702aa 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
@@ -41,8 +41,9 @@ class ServerCapabilitiesHandlerTest {
Map<String, Object> actual = createCapabilitiesPayload();
assertBaselineTopLevelKeys(actual);
assertTrue(((Collection<?>)
actual.get("supportedResources")).contains("shardingsphere://capabilities"));
- assertTrue(((Collection<?>)
actual.get("supportedTools")).containsAll(List.of("database_gateway_search_metadata",
"database_gateway_execute_query",
- "database_gateway_execute_update",
"database_gateway_apply_workflow", "database_gateway_validate_workflow")));
+ assertTrue(
+ ((Collection<?>)
actual.get("supportedTools")).containsAll(List.of("database_gateway_search_metadata",
"database_gateway_validate_proxy_connectivity",
"database_gateway_execute_query",
+ "database_gateway_execute_update",
"database_gateway_apply_workflow", "database_gateway_validate_workflow")));
assertFalse(((List<?>) actual.get("resources")).isEmpty());
assertFalse(((List<?>) actual.get("resourceTemplates")).isEmpty());
assertFalse(((List<?>) actual.get("tools")).isEmpty());
@@ -106,6 +107,7 @@ class ServerCapabilitiesHandlerTest {
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"));
+ assertThat(((Map<?, ?>) actual.get("preflight_rule")).get("tool"),
is("database_gateway_validate_proxy_connectivity"));
Map<?, ?> sqlToolSelection = (Map<?, ?>)
actual.get("sql_tool_selection");
assertThat(((Map<?, ?>)
sqlToolSelection.get("read_only")).get("tool"),
is("database_gateway_execute_query"));
assertThat(((Map<?, ?>)
sqlToolSelection.get("side_effecting")).get("first_mode"), is("preview"));
@@ -126,6 +128,7 @@ class ServerCapabilitiesHandlerTest {
assertThat(actual.get("optional_catalog_resource"),
is("shardingsphere://capabilities"));
assertFalse(actual.containsKey("safe_first_resource"));
assertThat(actual.get("metadata_first_resource"),
is("shardingsphere://databases"));
+
assertTrue(String.valueOf(actual.get("preflight_rule")).contains("database_gateway_validate_proxy_connectivity"));
assertTrue(((Map<?, ?>)
actual.get("sql_tool_selection")).containsKey("side_effecting"));
assertTrue(actual.containsKey("workflow_session_rule"));
assertTrue(actual.containsKey("next_action_rule"));
@@ -138,6 +141,7 @@ class ServerCapabilitiesHandlerTest {
assertThat(actual.get("argument_completion_method"),
is("completion/complete"));
assertThat(actual.get("optional_catalog_resource"),
is("shardingsphere://capabilities"));
assertThat(actual.get("metadata_search_tool"),
is("database_gateway_search_metadata"));
+ assertThat(actual.get("preflight_validation_tool"),
is("database_gateway_validate_proxy_connectivity"));
assertThat(actual.get("side_effect_sql_tool"),
is("database_gateway_execute_update"));
}
@@ -169,6 +173,9 @@ class ServerCapabilitiesHandlerTest {
Map<?, ?> inspectMetadata = findByKey((List<?>)
capabilities.get("common_flows"), "flow_id", "inspect_metadata");
assertTrue(((List<?>)
inspectMetadata.get("steps")).containsAll(List.of("resources/list",
"resources/templates/list", "call_tool database_gateway_search_metadata")));
assertReferencedFlowEntries(inspectMetadata, supportedTools,
supportedResources);
+ Map<?, ?> validateRuntimeDatabase = findByKey((List<?>)
capabilities.get("common_flows"), "flow_id", "validate_runtime_database");
+ assertTrue(((List<?>)
validateRuntimeDatabase.get("steps")).contains("call_tool
database_gateway_validate_proxy_connectivity"));
+ assertReferencedFlowEntries(validateRuntimeDatabase, supportedTools,
supportedResources);
Map<?, ?> sideEffectingSql = findByKey((List<?>)
capabilities.get("common_flows"), "flow_id", "side_effecting_sql");
assertTrue(((List<?>)
sideEffectingSql.get("steps")).contains("call_tool
database_gateway_execute_update execution_mode=preview"));
assertTrue(((List<?>)
sideEffectingSql.get("steps")).contains("call_tool
database_gateway_execute_update execution_mode=execute"));
@@ -242,6 +249,13 @@ class ServerCapabilitiesHandlerTest {
assertTrue(searchMetadataOutputProperties.containsKey("total_match_count"));
Map<?, ?> objectTypesSchema = findInputSchema(searchMetadataTool,
"object_types");
assertTrue(((List<?>) ((Map<?, ?>)
objectTypesSchema.get("items")).get("enum")).containsAll(List.of("database",
"schema", "table", "view", "column", "index", "sequence")));
+ Map<?, ?> validateProxyConnectivityTool = findTool(capabilities,
"database_gateway_validate_proxy_connectivity");
+ Map<?, ?> validateProxyConnectivityOutputProperties = (Map<?, ?>)
((Map<?, ?>)
validateProxyConnectivityTool.get("outputSchema")).get("properties");
+ assertThat(getInputFieldNames(validateProxyConnectivityTool),
is(List.of("database")));
+
assertTrue(validateProxyConnectivityOutputProperties.containsKey("status"));
+
assertTrue(validateProxyConnectivityOutputProperties.containsKey("checks"));
+
assertTrue(validateProxyConnectivityOutputProperties.containsKey("category"));
+
assertTrue(validateProxyConnectivityOutputProperties.containsKey("recovery"));
Map<?, ?> executeUpdateTool = findTool(capabilities,
"database_gateway_execute_update");
Map<?, ?> executeUpdateOutputProperties = (Map<?, ?>) ((Map<?, ?>)
executeUpdateTool.get("outputSchema")).get("properties");
assertTrue(executeUpdateOutputProperties.containsKey("response_mode"));
diff --git
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/ToolDefinitionRegistryTest.java
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/ToolDefinitionRegistryTest.java
index b68526ec8a4..18d6e8c45ed 100644
---
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/ToolDefinitionRegistryTest.java
+++
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/ToolDefinitionRegistryTest.java
@@ -55,7 +55,7 @@ class ToolDefinitionRegistryTest {
@Test
void assertGetSupportedTools() {
- assertThat(ToolDefinitionRegistry.getSupportedTools(),
contains("database_gateway_search_metadata", "database_gateway_execute_query",
+ assertThat(ToolDefinitionRegistry.getSupportedTools(),
contains("database_gateway_search_metadata",
"database_gateway_validate_proxy_connectivity",
"database_gateway_execute_query",
"database_gateway_execute_update",
"database_gateway_apply_workflow", "database_gateway_validate_workflow"));
}
@@ -63,20 +63,22 @@ class ToolDefinitionRegistryTest {
void assertGetSupportedToolDescriptors() {
List<MCPToolDescriptor> actual =
ToolDefinitionRegistry.getSupportedToolDescriptors();
assertThat(actual.stream().map(MCPToolDescriptor::getName).toList(),
- is(List.of("database_gateway_search_metadata",
"database_gateway_execute_query", "database_gateway_execute_update",
+ is(List.of("database_gateway_search_metadata",
"database_gateway_validate_proxy_connectivity",
"database_gateway_execute_query", "database_gateway_execute_update",
"database_gateway_apply_workflow",
"database_gateway_validate_workflow")));
assertToolFields(actual.get(0), List.of("database", "schema", "query",
"object_types"));
- assertToolFields(actual.get(1), List.of("database", "schema", "sql",
"max_rows", "timeout_ms"));
- assertRequiredFields(actual.get(1), List.of("database", "sql"));
- assertToolFields(actual.get(2), List.of("database", "schema", "sql",
"execution_mode", "max_rows", "timeout_ms"));
- assertRequiredFields(actual.get(2), List.of("database", "sql",
"execution_mode"));
- assertField(actual.get(2), "execution_mode", "string",
List.of("execute", "preview"), true);
- assertField(actual.get(2), "max_rows", "integer", List.of(), false);
- assertField(actual.get(2), "timeout_ms", "integer", List.of(), false);
- assertToolFields(actual.get(3), List.of("plan_id", "execution_mode",
"approved_steps"));
- assertRequiredFields(actual.get(3), List.of("plan_id",
"execution_mode"));
- assertField(actual.get(3), "execution_mode", "string",
List.of("preview", "review-then-execute", "manual-only"), true);
- assertField(actual.get(3), "approved_steps", "array", List.of(),
false);
+ assertToolFields(actual.get(1), List.of("database"));
+ assertRequiredFields(actual.get(1), List.of("database"));
+ assertToolFields(actual.get(2), List.of("database", "schema", "sql",
"max_rows", "timeout_ms"));
+ assertRequiredFields(actual.get(2), List.of("database", "sql"));
+ assertToolFields(actual.get(3), List.of("database", "schema", "sql",
"execution_mode", "max_rows", "timeout_ms"));
+ assertRequiredFields(actual.get(3), List.of("database", "sql",
"execution_mode"));
+ assertField(actual.get(3), "execution_mode", "string",
List.of("execute", "preview"), true);
+ assertField(actual.get(3), "max_rows", "integer", List.of(), false);
+ assertField(actual.get(3), "timeout_ms", "integer", List.of(), false);
+ assertToolFields(actual.get(4), List.of("plan_id", "execution_mode",
"approved_steps"));
+ assertRequiredFields(actual.get(4), List.of("plan_id",
"execution_mode"));
+ assertField(actual.get(4), "execution_mode", "string",
List.of("preview", "review-then-execute", "manual-only"), true);
+ assertField(actual.get(4), "approved_steps", "array", List.of(),
false);
}
@Test
diff --git
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/metadata/ValidateProxyConnectivityToolHandlerTest.java
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/metadata/ValidateProxyConnectivityToolHandlerTest.java
new file mode 100644
index 00000000000..13fe5faba2d
--- /dev/null
+++
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/tool/handler/metadata/ValidateProxyConnectivityToolHandlerTest.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.mcp.core.tool.handler.metadata;
+
+import org.apache.shardingsphere.mcp.api.protocol.response.MCPResponse;
+import org.apache.shardingsphere.mcp.api.tool.MCPToolCall;
+import
org.apache.shardingsphere.mcp.support.database.MCPDatabaseHandlerContext;
+import
org.apache.shardingsphere.mcp.support.database.metadata.jdbc.RuntimeDatabaseConfiguration;
+import
org.apache.shardingsphere.mcp.support.database.tool.request.ProxyPreflightValidationRequest;
+import
org.apache.shardingsphere.mcp.support.database.tool.response.ProxyPreflightCheckResult;
+import
org.apache.shardingsphere.mcp.support.database.tool.response.ProxyPreflightValidationResult;
+import
org.apache.shardingsphere.mcp.support.database.tool.service.ProxyPreflightValidationService;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class ValidateProxyConnectivityToolHandlerTest {
+
+ @Test
+ void assertGetToolName() {
+ assertThat(new ValidateProxyConnectivityToolHandler().getToolName(),
is("database_gateway_validate_proxy_connectivity"));
+ }
+
+ @Test
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ void assertHandle() {
+ ProxyPreflightValidationService validationService =
mock(ProxyPreflightValidationService.class);
+ when(validationService.validate(any(), any(), any()))
+ .thenReturn(ProxyPreflightValidationResult.ready("logic_db",
List.of(ProxyPreflightCheckResult.passed("configuration", "Validated the
request."))));
+ MCPDatabaseHandlerContext databaseContext =
mock(MCPDatabaseHandlerContext.class);
+ RuntimeDatabaseConfiguration runtimeDatabaseConfig =
mock(RuntimeDatabaseConfiguration.class);
+
when(databaseContext.findRuntimeDatabaseConfiguration("logic_db")).thenReturn(Optional.of(runtimeDatabaseConfig));
+ MCPResponse actual = new
ValidateProxyConnectivityToolHandler(validationService).handle(databaseContext,
new MCPToolCall("session-1", Map.of(
+ "jdbcUrl", "jdbc:mysql://127.0.0.1:3307/logic_db",
+ "database", "logic_db")));
+ assertThat(actual.toPayload().get("response_mode"), is("validation"));
+ ArgumentCaptor<ProxyPreflightValidationRequest> requestCaptor =
ArgumentCaptor.forClass(ProxyPreflightValidationRequest.class);
+ ArgumentCaptor<Function<String,
Optional<RuntimeDatabaseConfiguration>>> resolverCaptor =
ArgumentCaptor.forClass(Function.class);
+ verify(validationService).validate(requestCaptor.capture(),
resolverCaptor.capture(), any());
+ assertThat(requestCaptor.getValue().getDatabase(), is("logic_db"));
+ assertThat(resolverCaptor.getValue().apply("logic_db"),
is(Optional.of(runtimeDatabaseConfig)));
+ }
+}
diff --git
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/MCPDatabaseHandlerContext.java
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/MCPDatabaseHandlerContext.java
index beb2f8993dc..cfd9d7c8496 100644
---
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/MCPDatabaseHandlerContext.java
+++
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/MCPDatabaseHandlerContext.java
@@ -18,11 +18,14 @@
package org.apache.shardingsphere.mcp.support.database;
import org.apache.shardingsphere.mcp.api.MCPHandlerContext;
+import
org.apache.shardingsphere.mcp.support.database.metadata.jdbc.RuntimeDatabaseConfiguration;
import
org.apache.shardingsphere.mcp.support.database.spi.MCPFeatureCapabilityFacade;
import
org.apache.shardingsphere.mcp.support.database.spi.MCPFeatureExecutionFacade;
import
org.apache.shardingsphere.mcp.support.database.spi.MCPFeatureQueryFacade;
import
org.apache.shardingsphere.mcp.support.database.spi.MCPMetadataQueryFacade;
+import java.util.Optional;
+
/**
* Database-aware MCP handler context.
*/
@@ -62,4 +65,12 @@ public interface MCPDatabaseHandlerContext extends
MCPHandlerContext {
* @return capability facade
*/
MCPFeatureCapabilityFacade getCapabilityFacade();
+
+ /**
+ * Find runtime database configuration.
+ *
+ * @param databaseName database name
+ * @return runtime database configuration
+ */
+ Optional<RuntimeDatabaseConfiguration>
findRuntimeDatabaseConfiguration(String databaseName);
}
diff --git
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/RuntimeDatabaseConnectionException.java
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/RuntimeDatabaseConnectionException.java
index 7d1d209cb60..ee892dc7a77 100644
---
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/RuntimeDatabaseConnectionException.java
+++
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/RuntimeDatabaseConnectionException.java
@@ -44,6 +44,8 @@ public final class RuntimeDatabaseConnectionException extends
RuntimeException {
public static final String CATEGORY_CONNECTION_FAILED =
"connection_failed";
+ public static final String CATEGORY_DATABASE_NOT_VISIBLE =
"database_not_visible";
+
private static final long serialVersionUID = -757957427736251437L;
private final String databaseName;
@@ -89,6 +91,17 @@ public final class RuntimeDatabaseConnectionException
extends RuntimeException {
return new RuntimeDatabaseConnectionException(databaseName,
resolveCategory(cause), cause);
}
+ /**
+ * Create database not visible exception.
+ *
+ * @param databaseName database name
+ * @param cause cause
+ * @return runtime database connection exception
+ */
+ public static RuntimeDatabaseConnectionException databaseNotVisible(final
String databaseName, final Throwable cause) {
+ return new RuntimeDatabaseConnectionException(databaseName,
CATEGORY_DATABASE_NOT_VISIBLE, cause);
+ }
+
private static String resolveCategory(final SQLException cause) {
String sqlState = Objects.toString(cause.getSQLState(), "");
String message = Objects.toString(cause.getMessage(),
"").toLowerCase(Locale.ENGLISH);
diff --git
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/request/ProxyPreflightValidationRequest.java
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/request/ProxyPreflightValidationRequest.java
new file mode 100644
index 00000000000..ef77760014c
--- /dev/null
+++
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/request/ProxyPreflightValidationRequest.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.mcp.support.database.tool.request;
+
+import lombok.Getter;
+
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Proxy preflight validation request.
+ */
+@Getter
+public final class ProxyPreflightValidationRequest {
+
+ private final String database;
+
+ public ProxyPreflightValidationRequest(final String database) {
+ this.database = Objects.toString(database, "");
+ }
+
+ /**
+ * Create request from MCP tool arguments.
+ *
+ * @param arguments tool arguments
+ * @return request
+ */
+ public static ProxyPreflightValidationRequest from(final Map<String,
Object> arguments) {
+ return new ProxyPreflightValidationRequest(getRawString(arguments,
"database"));
+ }
+
+ private static String getRawString(final Map<String, Object> arguments,
final String fieldName) {
+ return Objects.toString(arguments.get(fieldName), "");
+ }
+}
diff --git
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/response/ProxyPreflightCheckResult.java
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/response/ProxyPreflightCheckResult.java
new file mode 100644
index 00000000000..23711ae5d14
--- /dev/null
+++
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/response/ProxyPreflightCheckResult.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.mcp.support.database.tool.response;
+
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Proxy preflight check result.
+ */
+@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
+@Getter
+public final class ProxyPreflightCheckResult {
+
+ private static final String STATUS_PASSED = "passed";
+
+ private static final String STATUS_FAILED = "failed";
+
+ private static final String STATUS_SKIPPED = "skipped";
+
+ private final String name;
+
+ private final String status;
+
+ private final String category;
+
+ private final String message;
+
+ /**
+ * Create a passed check result.
+ *
+ * @param name check name
+ * @param message check message
+ * @return check result
+ */
+ public static ProxyPreflightCheckResult passed(final String name, final
String message) {
+ return new ProxyPreflightCheckResult(name, STATUS_PASSED, "ready",
message);
+ }
+
+ /**
+ * Create a failed check result.
+ *
+ * @param name check name
+ * @param category failure category
+ * @param message check message
+ * @return check result
+ */
+ public static ProxyPreflightCheckResult failed(final String name, final
String category, final String message) {
+ return new ProxyPreflightCheckResult(name, STATUS_FAILED, category,
message);
+ }
+
+ /**
+ * Create a skipped check result.
+ *
+ * @param name check name
+ * @param message check message
+ * @return check result
+ */
+ public static ProxyPreflightCheckResult skipped(final String name, final
String message) {
+ return new ProxyPreflightCheckResult(name, STATUS_SKIPPED, "skipped",
message);
+ }
+
+ /**
+ * Convert to payload.
+ *
+ * @return payload
+ */
+ public Map<String, Object> toPayload() {
+ Map<String, Object> result = new LinkedHashMap<>(4, 1F);
+ result.put("name", name);
+ result.put("status", status);
+ result.put("category", category);
+ result.put("message", message);
+ return result;
+ }
+}
diff --git
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/response/ProxyPreflightValidationResult.java
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/response/ProxyPreflightValidationResult.java
new file mode 100644
index 00000000000..4af5fcfc809
--- /dev/null
+++
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/response/ProxyPreflightValidationResult.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.mcp.support.database.tool.response;
+
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.apache.shardingsphere.mcp.api.protocol.response.MCPResponse;
+import org.apache.shardingsphere.mcp.support.protocol.MCPPayloadFieldNames;
+import org.apache.shardingsphere.mcp.support.protocol.MCPResponseMode;
+
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Proxy preflight validation result.
+ */
+@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
+@Getter
+public final class ProxyPreflightValidationResult implements MCPResponse {
+
+ private final String status;
+
+ private final String database;
+
+ private final List<ProxyPreflightCheckResult> checks;
+
+ private final String category;
+
+ private final Map<String, Object> recovery;
+
+ /**
+ * Create a success result.
+ *
+ * @param database database name
+ * @param checks check results
+ * @return validation result
+ */
+ public static ProxyPreflightValidationResult ready(final String database,
final List<ProxyPreflightCheckResult> checks) {
+ return new ProxyPreflightValidationResult("ready",
Objects.toString(database, ""), checks, "ready", Map.of());
+ }
+
+ /**
+ * Create a failure result.
+ *
+ * @param database database name
+ * @param checks check results
+ * @param category failure category
+ * @param recovery recovery payload
+ * @return validation result
+ */
+ public static ProxyPreflightValidationResult failed(final String database,
final List<ProxyPreflightCheckResult> checks, final String category, final
Map<String, Object> recovery) {
+ return new ProxyPreflightValidationResult("failed",
Objects.toString(database, ""), checks, category, null == recovery ? Map.of() :
recovery);
+ }
+
+ @Override
+ public Map<String, Object> toPayload() {
+ Map<String, Object> result = new LinkedHashMap<>(6, 1F);
+ result.put("response_mode", MCPResponseMode.VALIDATION);
+ result.put("status", status);
+ result.put("database", database);
+ result.put("checks", createChecksPayload());
+ result.put("category", category);
+ result.put(MCPPayloadFieldNames.RECOVERY, recovery);
+ return result;
+ }
+
+ private List<Map<String, Object>> createChecksPayload() {
+ List<Map<String, Object>> result = new LinkedList<>();
+ for (ProxyPreflightCheckResult each : checks) {
+ result.add(each.toPayload());
+ }
+ return result;
+ }
+}
diff --git
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/service/ProxyPreflightValidationService.java
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/service/ProxyPreflightValidationService.java
new file mode 100644
index 00000000000..9d25609c988
--- /dev/null
+++
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/tool/service/ProxyPreflightValidationService.java
@@ -0,0 +1,206 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.mcp.support.database.tool.service;
+
+import lombok.RequiredArgsConstructor;
+import
org.apache.shardingsphere.mcp.support.database.metadata.jdbc.MCPJdbcDatabaseProfileLoader;
+import
org.apache.shardingsphere.mcp.support.database.metadata.jdbc.MCPJdbcMetadataLoader;
+import
org.apache.shardingsphere.mcp.support.database.metadata.jdbc.RuntimeDatabaseConfiguration;
+import
org.apache.shardingsphere.mcp.support.database.metadata.jdbc.RuntimeDatabaseConnectionException;
+import
org.apache.shardingsphere.mcp.support.database.metadata.jdbc.RuntimeDatabaseProfile;
+import
org.apache.shardingsphere.mcp.support.database.metadata.model.MCPDatabaseMetadata;
+import
org.apache.shardingsphere.mcp.support.database.metadata.model.MCPSchemaMetadata;
+import
org.apache.shardingsphere.mcp.support.database.tool.request.ProxyPreflightValidationRequest;
+import
org.apache.shardingsphere.mcp.support.database.tool.response.ProxyPreflightCheckResult;
+import
org.apache.shardingsphere.mcp.support.database.tool.response.ProxyPreflightValidationResult;
+
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Function;
+
+/**
+ * Proxy preflight validation service.
+ */
+@RequiredArgsConstructor
+public final class ProxyPreflightValidationService {
+
+ private static final String PRE_FLIGHT_BINDING_DATABASE =
"__preflight_validation__";
+
+ private final MCPJdbcDatabaseProfileLoader profileLoader;
+
+ private final MCPJdbcMetadataLoader metadataLoader;
+
+ public ProxyPreflightValidationService() {
+ this(new MCPJdbcDatabaseProfileLoader(), new MCPJdbcMetadataLoader());
+ }
+
+ /**
+ * Validate proxy preflight request.
+ *
+ * @param request validation request
+ * @param runtimeDatabaseResolver runtime database resolver
+ * @param recoveryFactory runtime recovery factory
+ * @return validation result
+ */
+ public ProxyPreflightValidationResult validate(final
ProxyPreflightValidationRequest request,
+ final Function<String,
Optional<RuntimeDatabaseConfiguration>> runtimeDatabaseResolver,
+ final
Function<RuntimeDatabaseConnectionException, Map<String, Object>>
recoveryFactory) {
+ List<ProxyPreflightCheckResult> checks = new LinkedList<>();
+ String database = normalize(request.getDatabase());
+ Optional<RuntimeDatabaseConfiguration> runtimeDatabaseConfig =
findRuntimeDatabaseConfiguration(database, runtimeDatabaseResolver);
+ if (runtimeDatabaseConfig.isEmpty()) {
+ RuntimeDatabaseConnectionException ex =
createMissingRuntimeDatabaseException(database);
+ checks.add(ProxyPreflightCheckResult.failed("configuration",
ex.getCategory(), "The requested database is not configured for this MCP
runtime."));
+ appendSkippedChecks(checks, "jdbc_driver", "configuration
validation did not finish");
+ appendSkippedChecks(checks, "jdbc_connectivity", "configuration
validation did not finish");
+ appendSkippedChecks(checks, "metadata_read", "configuration
validation did not finish");
+ appendSkippedChecks(checks, "database_visibility", "configuration
validation did not finish");
+ return createFailureResult(database, checks, ex, recoveryFactory);
+ }
+ checks.add(ProxyPreflightCheckResult.passed("configuration", "Resolved
the configured runtime database."));
+ RuntimeDatabaseProfile databaseProfile;
+ try {
+ databaseProfile = profileLoader.load(database,
runtimeDatabaseConfig.get());
+ checks.add(ProxyPreflightCheckResult.passed("jdbc_driver", "Loaded
the configured JDBC driver."));
+ checks.add(ProxyPreflightCheckResult.passed("jdbc_connectivity",
"Opened a JDBC connection and validated the configured database type."));
+ } catch (final RuntimeDatabaseConnectionException ex) {
+ appendProfileFailureChecks(checks, ex);
+ appendSkippedChecks(checks, "metadata_read", "driver or
connectivity validation failed");
+ appendSkippedChecks(checks, "database_visibility", "driver or
connectivity validation failed");
+ return createFailureResult(database, checks, ex, recoveryFactory);
+ }
+ MCPDatabaseMetadata databaseMetadata;
+ try {
+ databaseMetadata = metadataLoader.load(database,
runtimeDatabaseConfig.get(), databaseProfile);
+ checks.add(ProxyPreflightCheckResult.passed("metadata_read", "Read
metadata through the configured JDBC connection."));
+ } catch (final RuntimeDatabaseConnectionException ex) {
+ checks.add(ProxyPreflightCheckResult.failed("metadata_read",
ex.getCategory(), "Failed to read metadata through the configured JDBC
connection."));
+ appendSkippedChecks(checks, "database_visibility", "metadata
validation failed");
+ return createFailureResult(database, checks, ex, recoveryFactory);
+ }
+ try {
+ validateDatabaseVisibility(database, runtimeDatabaseConfig.get(),
databaseMetadata);
+ checks.add(ProxyPreflightCheckResult.passed("database_visibility",
"Validated the requested database name against visible JDBC metadata and
connection context."));
+ } catch (final RuntimeDatabaseConnectionException ex) {
+ checks.add(ProxyPreflightCheckResult.failed("database_visibility",
ex.getCategory(), "The requested database name is not visible to the configured
JDBC connection."));
+ return createFailureResult(database, checks, ex, recoveryFactory);
+ }
+ return ProxyPreflightValidationResult.ready(database, checks);
+ }
+
+ private Optional<RuntimeDatabaseConfiguration>
findRuntimeDatabaseConfiguration(final String database,
+
final Function<String, Optional<RuntimeDatabaseConfiguration>>
runtimeDatabaseResolver) {
+ return database.isEmpty() ? Optional.empty() :
runtimeDatabaseResolver.apply(database);
+ }
+
+ private RuntimeDatabaseConnectionException
createMissingRuntimeDatabaseException(final String database) {
+ return
RuntimeDatabaseConnectionException.invalidConfiguration(resolveExceptionDatabaseName(database),
+ new IllegalStateException("Proxy preflight validation requires
one configured runtime database."));
+ }
+
+ private void appendProfileFailureChecks(final
List<ProxyPreflightCheckResult> checks, final
RuntimeDatabaseConnectionException ex) {
+ if
(RuntimeDatabaseConnectionException.CATEGORY_MISSING_JDBC_DRIVER.equals(ex.getCategory()))
{
+ checks.add(ProxyPreflightCheckResult.failed("jdbc_driver",
ex.getCategory(), "Failed to load the configured JDBC driver."));
+ appendSkippedChecks(checks, "jdbc_connectivity", "driver loading
failed");
+ return;
+ }
+ checks.add(ProxyPreflightCheckResult.passed("jdbc_driver", "Loaded the
configured JDBC driver."));
+ checks.add(ProxyPreflightCheckResult.failed("jdbc_connectivity",
ex.getCategory(), "Failed to open a JDBC connection or validate the configured
database type."));
+ }
+
+ private void appendSkippedChecks(final List<ProxyPreflightCheckResult>
checks, final String name, final String reason) {
+ checks.add(ProxyPreflightCheckResult.skipped(name,
String.format("Skipped because %s.", reason)));
+ }
+
+ private ProxyPreflightValidationResult createFailureResult(final String
database, final List<ProxyPreflightCheckResult> checks, final
RuntimeDatabaseConnectionException cause,
+ final
Function<RuntimeDatabaseConnectionException, Map<String, Object>>
recoveryFactory) {
+ return ProxyPreflightValidationResult.failed(database, checks,
cause.getCategory(), recoveryFactory.apply(cause));
+ }
+
+ private void validateDatabaseVisibility(final String database, final
RuntimeDatabaseConfiguration runtimeDatabaseConfig, final MCPDatabaseMetadata
databaseMetadata) {
+ if (containsVisibleSchema(databaseMetadata, database)) {
+ return;
+ }
+ try (Connection connection =
runtimeDatabaseConfig.openConnection(resolveExceptionDatabaseName(database))) {
+ if (isVisibleDatabase(connection, database)) {
+ return;
+ }
+ } catch (final SQLException ex) {
+ throw
RuntimeDatabaseConnectionException.connectionFailed(resolveExceptionDatabaseName(database),
ex);
+ }
+ throw
RuntimeDatabaseConnectionException.databaseNotVisible(resolveExceptionDatabaseName(database),
+ new IllegalStateException(String.format("Requested database
`%s` is not visible to the configured JDBC connection.", database)));
+ }
+
+ private boolean containsVisibleSchema(final MCPDatabaseMetadata
databaseMetadata, final String database) {
+ for (MCPSchemaMetadata each : databaseMetadata.getSchemas()) {
+ if (database.equalsIgnoreCase(each.getSchema())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean isVisibleDatabase(final Connection connection, final
String database) throws SQLException {
+ return matches(connection.getCatalog(), database)
+ || matches(connection.getSchema(), database)
+ || containsCatalog(connection.getMetaData(), database)
+ || containsSchema(connection.getMetaData(), database);
+ }
+
+ private boolean containsCatalog(final DatabaseMetaData databaseMetaData,
final String database) throws SQLException {
+ try (ResultSet resultSet = databaseMetaData.getCatalogs()) {
+ while (resultSet.next()) {
+ if (matches(resultSet.getString(1), database)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private boolean containsSchema(final DatabaseMetaData databaseMetaData,
final String database) throws SQLException {
+ try (ResultSet resultSet = databaseMetaData.getSchemas()) {
+ while (resultSet.next()) {
+ if (matches(resultSet.getString("TABLE_SCHEM"), database)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private boolean matches(final String actualValue, final String
expectedValue) {
+ return !Objects.toString(actualValue, "").trim().isEmpty() &&
actualValue.trim().equalsIgnoreCase(expectedValue);
+ }
+
+ private String resolveExceptionDatabaseName(final String database) {
+ return database.isEmpty() ? PRE_FLIGHT_BINDING_DATABASE : database;
+ }
+
+ private String normalize(final String value) {
+ return Objects.toString(value, "").trim();
+ }
+}
diff --git
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPModelFirstContractPayloadBuilder.java
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPModelFirstContractPayloadBuilder.java
index 1434d25b520..c6ac58476d5 100644
---
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPModelFirstContractPayloadBuilder.java
+++
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/descriptor/MCPModelFirstContractPayloadBuilder.java
@@ -35,6 +35,8 @@ final class MCPModelFirstContractPayloadBuilder {
private static final String PLANNING_TOOL_NAME_PREFIX =
"database_gateway_plan_";
+ private static final String PREFLIGHT_TOOL_NAME =
"database_gateway_validate_proxy_connectivity";
+
private static final String CATALOG_RESOURCE_URI =
"shardingsphere://capabilities";
private static final String ARGUMENT_COMPLETION_METHOD =
"completion/complete";
@@ -44,13 +46,14 @@ final class MCPModelFirstContractPayloadBuilder {
private final MCPDescriptorCatalog catalog;
Map<String, Object> createModelFirstSummary() {
- Map<String, Object> result = new LinkedHashMap<>(10, 1F);
+ Map<String, Object> result = new LinkedHashMap<>(11, 1F);
result.put("official_discovery_methods",
createOfficialDiscoveryMethods());
result.put("argument_completion_method", ARGUMENT_COMPLETION_METHOD);
result.put("catalog_resource_role", CATALOG_RESOURCE_URI
+ " complements MCP list methods with ShardingSphere domain
capability guidance, workflow guidance, and side-effect notes.");
result.put("optional_catalog_resource", CATALOG_RESOURCE_URI);
result.put("metadata_rule", createMetadataRule());
+ result.put("preflight_rule", createPreflightRule());
result.put("sql_tool_selection", createSqlToolSelection());
result.put("side_effect_rule", "Preview side effects first; execute
only when the requested side effect is still intended.");
result.put("workflow_rule", createWorkflowRule());
@@ -60,12 +63,13 @@ final class MCPModelFirstContractPayloadBuilder {
}
Map<String, Object> createModelContract() {
- Map<String, Object> result = new LinkedHashMap<>(11, 1F);
+ Map<String, Object> result = new LinkedHashMap<>(12, 1F);
result.put("public_surface_source", MCP_LIST_METHODS_SOURCE);
result.put("official_discovery_methods",
createOfficialDiscoveryMethods());
result.put("argument_completion_method", ARGUMENT_COMPLETION_METHOD);
result.put("optional_catalog_resource", CATALOG_RESOURCE_URI);
result.put("metadata_first_resource", "shardingsphere://databases");
+ result.put("preflight_rule", "Use
database_gateway_validate_proxy_connectivity with a configured database name
before onboarding or troubleshooting runtime connectivity.");
result.put("sql_tool_selection", Map.of(
"read_only", "Use database_gateway_execute_query for one
classifier-approved SELECT or EXPLAIN ANALYZE statement.",
"side_effecting", "Use database_gateway_execute_update with
execution_mode=preview before execution."));
@@ -78,12 +82,13 @@ final class MCPModelFirstContractPayloadBuilder {
}
Map<String, Object> createSurfaceSummary() {
- Map<String, Object> result = new LinkedHashMap<>(10, 1F);
+ Map<String, Object> result = new LinkedHashMap<>(11, 1F);
result.put("official_discovery_methods",
createOfficialDiscoveryMethods());
result.put("argument_completion_method", ARGUMENT_COMPLETION_METHOD);
result.put("optional_catalog_resource", CATALOG_RESOURCE_URI);
result.put("metadata_resource", "shardingsphere://databases");
result.put("metadata_search_tool", "database_gateway_search_metadata");
+ result.put("preflight_validation_tool", PREFLIGHT_TOOL_NAME);
result.put("read_only_sql_tool", "database_gateway_execute_query");
result.put("side_effect_sql_tool", "database_gateway_execute_update");
result.put("workflow_tools",
List.of("database_gateway_apply_workflow",
"database_gateway_validate_workflow"));
@@ -126,6 +131,9 @@ final class MCPModelFirstContractPayloadBuilder {
"call_tool database_gateway_search_metadata",
"read_resource returned resource.uri"),
"Stop when the requested metadata detail resource is
read.",
List.of("database_gateway_search_metadata"),
List.of("shardingsphere://databases")),
+ createCommonFlow("validate_runtime_database",
List.of("read_resource shardingsphere://databases", "call_tool
database_gateway_validate_proxy_connectivity"),
+ "Stop after the configured runtime database reports
ready or returns structured recovery guidance.",
+ List.of(PREFLIGHT_TOOL_NAME),
List.of("shardingsphere://databases")),
createCommonFlow("read_only_sql", List.of("read_resource
shardingsphere://databases/{database}/capabilities", "call_tool
database_gateway_execute_query"),
"Use one SELECT or EXPLAIN ANALYZE statement and stop
after the result is reported.",
List.of("database_gateway_execute_query"),
List.of("shardingsphere://databases/{database}/capabilities")),
@@ -176,6 +184,14 @@ final class MCPModelFirstContractPayloadBuilder {
return result;
}
+ private Map<String, Object> createPreflightRule() {
+ Map<String, Object> result = new LinkedHashMap<>(3, 1F);
+ result.put("tool", PREFLIGHT_TOOL_NAME);
+ result.put("input_rule", "Pass only a configured runtime database
name.");
+ result.put("secret_rule", "Connection details stay in administrator
runtime configuration and are not tool arguments.");
+ return result;
+ }
+
private Map<String, Object> createSqlToolSelection() {
Map<String, Object> result = new LinkedHashMap<>(2, 1F);
Map<String, Object> readOnly = new LinkedHashMap<>(2, 1F);
diff --git
a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/RuntimeDatabaseConnectionExceptionTest.java
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/RuntimeDatabaseConnectionExceptionTest.java
index 2f6a7a1aaca..3dbc4d787e1 100644
---
a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/RuntimeDatabaseConnectionExceptionTest.java
+++
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/RuntimeDatabaseConnectionExceptionTest.java
@@ -72,4 +72,11 @@ class RuntimeDatabaseConnectionExceptionTest {
RuntimeDatabaseConnectionException actual =
RuntimeDatabaseConnectionException.connectionFailed("logic_db", new
SQLException("Broken connection"));
assertThat(actual.getCategory(),
is(RuntimeDatabaseConnectionException.CATEGORY_CONNECTION_FAILED));
}
+
+ @Test
+ void assertDatabaseNotVisible() {
+ RuntimeDatabaseConnectionException actual =
RuntimeDatabaseConnectionException.databaseNotVisible("logic_db", new
IllegalStateException("not visible"));
+ assertThat(actual.getMessage(), is("Runtime database `logic_db`
connection failed: database_not_visible."));
+ assertThat(actual.getCategory(),
is(RuntimeDatabaseConnectionException.CATEGORY_DATABASE_NOT_VISIBLE));
+ }
}
diff --git
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/OfficialMCPToolNames.java
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/request/ProxyPreflightValidationRequestTest.java
similarity index 51%
copy from
test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/OfficialMCPToolNames.java
copy to
mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/request/ProxyPreflightValidationRequestTest.java
index 5bef92123fc..7b995801339 100644
---
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/OfficialMCPToolNames.java
+++
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/request/ProxyPreflightValidationRequestTest.java
@@ -15,33 +15,28 @@
* limitations under the License.
*/
-package org.apache.shardingsphere.test.e2e.mcp.support;
+package org.apache.shardingsphere.mcp.support.database.tool.request;
-import java.util.List;
+import org.junit.jupiter.api.Test;
-/**
- * Official MCP tool names packaged by default.
- */
-public final class OfficialMCPToolNames {
-
- private static final List<String> ALL = List.of(
- "database_gateway_search_metadata",
- "database_gateway_execute_query",
- "database_gateway_execute_update",
- "database_gateway_apply_workflow",
- "database_gateway_validate_workflow",
- "database_gateway_plan_encrypt_rule",
- "database_gateway_plan_mask_rule");
+import java.util.Map;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+class ProxyPreflightValidationRequestTest {
- private OfficialMCPToolNames() {
+ @Test
+ void assertFromPreservesDatabaseArgument() {
+ ProxyPreflightValidationRequest actual =
ProxyPreflightValidationRequest.from(Map.of(
+ "jdbcUrl", " jdbc:mysql://127.0.0.1:3307/logic_db ",
+ "database", " logic_db "));
+ assertThat(actual.getDatabase(), is(" logic_db "));
}
- /**
- * Get official MCP tool names.
- *
- * @return official MCP tool names
- */
- public static List<String> getAll() {
- return ALL;
+ @Test
+ void assertFromDefaultsMissingDatabase() {
+ ProxyPreflightValidationRequest actual =
ProxyPreflightValidationRequest.from(Map.of());
+ assertThat(actual.getDatabase(), is(""));
}
}
diff --git
a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/response/ProxyPreflightCheckResultTest.java
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/response/ProxyPreflightCheckResultTest.java
new file mode 100644
index 00000000000..10f219ffd92
--- /dev/null
+++
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/response/ProxyPreflightCheckResultTest.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.mcp.support.database.tool.response;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Map;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+class ProxyPreflightCheckResultTest {
+
+ @Test
+ void assertPassed() {
+ ProxyPreflightCheckResult actual =
ProxyPreflightCheckResult.passed("jdbc_connectivity", "Opened a JDBC
connection.");
+ assertThat(actual.getName(), is("jdbc_connectivity"));
+ assertThat(actual.getStatus(), is("passed"));
+ assertThat(actual.getCategory(), is("ready"));
+ assertThat(actual.getMessage(), is("Opened a JDBC connection."));
+ }
+
+ @Test
+ void assertFailed() {
+ ProxyPreflightCheckResult actual =
ProxyPreflightCheckResult.failed("jdbc_driver", "missing_jdbc_driver", "Failed
to load the configured JDBC driver.");
+ assertThat(actual.getName(), is("jdbc_driver"));
+ assertThat(actual.getStatus(), is("failed"));
+ assertThat(actual.getCategory(), is("missing_jdbc_driver"));
+ assertThat(actual.getMessage(), is("Failed to load the configured JDBC
driver."));
+ }
+
+ @Test
+ void assertSkipped() {
+ ProxyPreflightCheckResult actual =
ProxyPreflightCheckResult.skipped("database_visibility", "Skipped because no
database was provided.");
+ assertThat(actual.getName(), is("database_visibility"));
+ assertThat(actual.getStatus(), is("skipped"));
+ assertThat(actual.getCategory(), is("skipped"));
+ assertThat(actual.getMessage(), is("Skipped because no database was
provided."));
+ }
+
+ @Test
+ void assertToPayload() {
+ Map<String, Object> actual =
ProxyPreflightCheckResult.failed("metadata_read", "connection_failed", "Failed
to read metadata.").toPayload();
+ assertThat(actual, is(Map.of(
+ "name", "metadata_read",
+ "status", "failed",
+ "category", "connection_failed",
+ "message", "Failed to read metadata.")));
+ }
+}
diff --git
a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/response/ProxyPreflightValidationResultTest.java
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/response/ProxyPreflightValidationResultTest.java
new file mode 100644
index 00000000000..5c273be299e
--- /dev/null
+++
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/response/ProxyPreflightValidationResultTest.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.mcp.support.database.tool.response;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+class ProxyPreflightValidationResultTest {
+
+ @Test
+ void assertReady() {
+ ProxyPreflightValidationResult actual =
ProxyPreflightValidationResult.ready("logic_db",
List.of(ProxyPreflightCheckResult.passed("configuration", "Validated the
request.")));
+ assertThat(actual.getStatus(), is("ready"));
+ assertThat(actual.getDatabase(), is("logic_db"));
+ assertThat(actual.getCategory(), is("ready"));
+ assertThat(actual.getRecovery(), is(Map.of()));
+ }
+
+ @Test
+ void assertFailed() {
+ Map<String, Object> recovery = Map.of("category", "connection_failed");
+ ProxyPreflightValidationResult actual =
ProxyPreflightValidationResult.failed("logic_db",
+ List.of(ProxyPreflightCheckResult.failed("jdbc_connectivity",
"connection_failed", "Failed to open a JDBC connection.")),
"connection_failed", recovery);
+ assertThat(actual.getStatus(), is("failed"));
+ assertThat(actual.getDatabase(), is("logic_db"));
+ assertThat(actual.getCategory(), is("connection_failed"));
+ assertThat(actual.getRecovery(), is(recovery));
+ }
+
+ @Test
+ void assertToPayload() {
+ Map<String, Object> actual =
ProxyPreflightValidationResult.failed("logic_db",
+ List.of(ProxyPreflightCheckResult.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("status"), is("failed"));
+ assertThat(actual.get("database"), is("logic_db"));
+ assertThat(actual.get("category"), is("connection_failed"));
+ assertThat(actual.get("recovery"), is(Map.of("category",
"connection_failed")));
+ assertThat(actual.get("checks"), is(List.of(Map.of(
+ "name", "metadata_read",
+ "status", "failed",
+ "category", "connection_failed",
+ "message", "Failed to read metadata."))));
+ }
+}
diff --git
a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/service/ProxyPreflightValidationServiceTest.java
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/service/ProxyPreflightValidationServiceTest.java
new file mode 100644
index 00000000000..7538b24650d
--- /dev/null
+++
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/tool/service/ProxyPreflightValidationServiceTest.java
@@ -0,0 +1,280 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shardingsphere.mcp.support.database.tool.service;
+
+import
org.apache.shardingsphere.mcp.support.database.metadata.jdbc.MCPJdbcDatabaseProfileLoader;
+import
org.apache.shardingsphere.mcp.support.database.metadata.jdbc.MCPJdbcMetadataLoader;
+import
org.apache.shardingsphere.mcp.support.database.metadata.jdbc.RuntimeDatabaseConfiguration;
+import
org.apache.shardingsphere.mcp.support.database.metadata.jdbc.RuntimeDatabaseConnectionException;
+import
org.apache.shardingsphere.mcp.support.database.metadata.jdbc.RuntimeDatabaseProfile;
+import
org.apache.shardingsphere.mcp.support.database.metadata.model.MCPDatabaseMetadata;
+import
org.apache.shardingsphere.mcp.support.database.metadata.model.MCPSchemaMetadata;
+import
org.apache.shardingsphere.mcp.support.database.tool.request.ProxyPreflightValidationRequest;
+import
org.apache.shardingsphere.mcp.support.database.tool.response.ProxyPreflightValidationResult;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.mockito.ArgumentCaptor;
+
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.Driver;
+import java.sql.DriverManager;
+import java.sql.DriverPropertyInfo;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.SQLFeatureNotSupportedException;
+import java.sql.SQLTimeoutException;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Properties;
+import java.util.logging.Logger;
+import java.util.stream.Stream;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+class ProxyPreflightValidationServiceTest {
+
+ @Test
+ void assertValidateWithMissingDatabase() {
+ MCPJdbcDatabaseProfileLoader profileLoader =
mock(MCPJdbcDatabaseProfileLoader.class);
+ MCPJdbcMetadataLoader metadataLoader =
mock(MCPJdbcMetadataLoader.class);
+ ProxyPreflightValidationResult actual = new
ProxyPreflightValidationService(profileLoader, metadataLoader)
+ .validate(new ProxyPreflightValidationRequest(""),
+ ignored -> Optional.empty(),
+
ProxyPreflightValidationServiceTest::createRecoveryPayload);
+ Map<String, Object> actualPayload = actual.toPayload();
+ assertThat(actualPayload.get("status"), is("failed"));
+ assertThat(actualPayload.get("category"), is("invalid_configuration"));
+ assertThat(actualPayload.get("recovery"), is(Map.of("category",
"invalid_configuration")));
+ assertThat(((Map<?, ?>) ((List<?>)
actualPayload.get("checks")).get(0)).get("name"), is("configuration"));
+ assertThat(((Map<?, ?>) ((List<?>)
actualPayload.get("checks")).get(1)).get("status"), is("skipped"));
+ verifyNoInteractions(profileLoader, metadataLoader);
+ }
+
+ @Test
+ void assertValidateWithUnknownDatabase() {
+ MCPJdbcDatabaseProfileLoader profileLoader =
mock(MCPJdbcDatabaseProfileLoader.class);
+ MCPJdbcMetadataLoader metadataLoader =
mock(MCPJdbcMetadataLoader.class);
+ ProxyPreflightValidationResult actual = new
ProxyPreflightValidationService(profileLoader, metadataLoader)
+ .validate(new ProxyPreflightValidationRequest("logic_db"),
+ ignored -> Optional.empty(),
+
ProxyPreflightValidationServiceTest::createRecoveryPayload);
+ Map<String, Object> actualPayload = actual.toPayload();
+ assertThat(actualPayload.get("status"), is("failed"));
+ assertThat(actualPayload.get("category"), is("invalid_configuration"));
+ assertThat(((Map<?, ?>) ((List<?>)
actualPayload.get("checks")).get(0)).get("status"), is("failed"));
+ verifyNoInteractions(profileLoader, metadataLoader);
+ }
+
+ @Test
+ void assertValidateWithConfiguredDatabase() {
+ MCPJdbcDatabaseProfileLoader profileLoader =
mock(MCPJdbcDatabaseProfileLoader.class);
+ MCPJdbcMetadataLoader metadataLoader =
mock(MCPJdbcMetadataLoader.class);
+ RuntimeDatabaseConfiguration runtimeDatabaseConfig =
createRuntimeDatabaseConfiguration();
+ when(profileLoader.load(any(),
any(RuntimeDatabaseConfiguration.class))).thenReturn(createProfile());
+ when(metadataLoader.load(any(),
any(RuntimeDatabaseConfiguration.class),
any(RuntimeDatabaseProfile.class))).thenReturn(createMetadata("logic_db"));
+ ProxyPreflightValidationService service = new
ProxyPreflightValidationService(profileLoader, metadataLoader);
+ ProxyPreflightValidationResult actual = service.validate(new
ProxyPreflightValidationRequest("logic_db"),
+ ignored -> Optional.of(runtimeDatabaseConfig),
+ ProxyPreflightValidationServiceTest::createRecoveryPayload);
+ Map<String, Object> actualPayload = actual.toPayload();
+ assertThat(actualPayload.get("status"), is("ready"));
+ ArgumentCaptor<RuntimeDatabaseConfiguration> configurationCaptor =
ArgumentCaptor.forClass(RuntimeDatabaseConfiguration.class);
+ verify(profileLoader).load(any(), configurationCaptor.capture());
+ assertThat(configurationCaptor.getValue(), is(runtimeDatabaseConfig));
+ }
+
+ @Test
+ void assertValidateWithVisibleDatabase() {
+ MCPJdbcDatabaseProfileLoader profileLoader =
mock(MCPJdbcDatabaseProfileLoader.class);
+ MCPJdbcMetadataLoader metadataLoader =
mock(MCPJdbcMetadataLoader.class);
+ RuntimeDatabaseConfiguration runtimeDatabaseConfig =
createRuntimeDatabaseConfiguration();
+ when(profileLoader.load(any(),
any(RuntimeDatabaseConfiguration.class))).thenReturn(createProfile());
+ when(metadataLoader.load(any(),
any(RuntimeDatabaseConfiguration.class),
any(RuntimeDatabaseProfile.class))).thenReturn(createMetadata("logic_db"));
+ ProxyPreflightValidationResult actual = new
ProxyPreflightValidationService(profileLoader, metadataLoader)
+ .validate(new ProxyPreflightValidationRequest("logic_db"),
+ ignored -> Optional.of(runtimeDatabaseConfig),
+
ProxyPreflightValidationServiceTest::createRecoveryPayload);
+ Map<String, Object> actualPayload = actual.toPayload();
+ assertThat(actualPayload.get("status"), is("ready"));
+ assertThat(((Map<?, ?>) ((List<?>)
actualPayload.get("checks")).get(4)).get("status"), is("passed"));
+ }
+
+ @Test
+ void assertValidateWithInvisibleDatabase() {
+ MCPJdbcDatabaseProfileLoader profileLoader =
mock(MCPJdbcDatabaseProfileLoader.class);
+ MCPJdbcMetadataLoader metadataLoader =
mock(MCPJdbcMetadataLoader.class);
+ RuntimeDatabaseConfiguration runtimeDatabaseConfig = new
RuntimeDatabaseConfiguration("MySQL", InvisibleDatabaseDriver.JDBC_URL, "demo",
"", InvisibleDatabaseDriver.class.getName());
+ when(profileLoader.load(any(),
any(RuntimeDatabaseConfiguration.class))).thenReturn(createProfile());
+ when(metadataLoader.load(any(),
any(RuntimeDatabaseConfiguration.class),
any(RuntimeDatabaseProfile.class))).thenReturn(createMetadata("public"));
+ ProxyPreflightValidationResult actual = new
ProxyPreflightValidationService(profileLoader, metadataLoader)
+ .validate(new ProxyPreflightValidationRequest("logic_db"),
+ ignored -> Optional.of(runtimeDatabaseConfig),
+
ProxyPreflightValidationServiceTest::createRecoveryPayload);
+ Map<String, Object> actualPayload = actual.toPayload();
+ assertThat(actualPayload.get("status"), is("failed"));
+ assertThat(actualPayload.get("category"),
is(RuntimeDatabaseConnectionException.CATEGORY_DATABASE_NOT_VISIBLE));
+ assertThat(actualPayload.get("recovery"), is(Map.of("category",
RuntimeDatabaseConnectionException.CATEGORY_DATABASE_NOT_VISIBLE)));
+ assertThat(((Map<?, ?>) ((List<?>)
actualPayload.get("checks")).get(4)).get("status"), is("failed"));
+ }
+
+ @Test
+ void assertValidateWithMetadataReadFailure() {
+ MCPJdbcDatabaseProfileLoader profileLoader =
mock(MCPJdbcDatabaseProfileLoader.class);
+ MCPJdbcMetadataLoader metadataLoader =
mock(MCPJdbcMetadataLoader.class);
+ RuntimeDatabaseConfiguration runtimeDatabaseConfig =
createRuntimeDatabaseConfiguration();
+ when(profileLoader.load(any(),
any(RuntimeDatabaseConfiguration.class))).thenReturn(createProfile());
+ when(metadataLoader.load(any(),
any(RuntimeDatabaseConfiguration.class), any(RuntimeDatabaseProfile.class)))
+
.thenThrow(RuntimeDatabaseConnectionException.connectionFailed("logic_db", new
SQLException("Broken connection")));
+ ProxyPreflightValidationResult actual = new
ProxyPreflightValidationService(profileLoader, metadataLoader)
+ .validate(new ProxyPreflightValidationRequest("logic_db"),
+ ignored -> Optional.of(runtimeDatabaseConfig),
+
ProxyPreflightValidationServiceTest::createRecoveryPayload);
+ Map<String, Object> actualPayload = actual.toPayload();
+ assertThat(actualPayload.get("status"), is("failed"));
+ assertThat(actualPayload.get("category"),
is(RuntimeDatabaseConnectionException.CATEGORY_CONNECTION_FAILED));
+ assertThat(((Map<?, ?>) ((List<?>)
actualPayload.get("checks")).get(3)).get("status"), is("failed"));
+ assertThat(((Map<?, ?>) ((List<?>)
actualPayload.get("checks")).get(4)).get("status"), is("skipped"));
+ }
+
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("assertValidateFailsDuringProfileLoadCases")
+ void assertValidateFailsDuringProfileLoad(final String name, final
RuntimeDatabaseConnectionException cause, final String expectedDriverStatus,
+ final String
expectedConnectivityStatus, final String expectedCategory) {
+ MCPJdbcDatabaseProfileLoader profileLoader =
mock(MCPJdbcDatabaseProfileLoader.class);
+ MCPJdbcMetadataLoader metadataLoader =
mock(MCPJdbcMetadataLoader.class);
+ RuntimeDatabaseConfiguration runtimeDatabaseConfig =
createRuntimeDatabaseConfiguration();
+ when(profileLoader.load(any(),
any(RuntimeDatabaseConfiguration.class))).thenThrow(cause);
+ ProxyPreflightValidationResult actual = new
ProxyPreflightValidationService(profileLoader, metadataLoader)
+ .validate(new ProxyPreflightValidationRequest("logic_db"),
+ ignored -> Optional.of(runtimeDatabaseConfig),
+
ProxyPreflightValidationServiceTest::createRecoveryPayload);
+ Map<String, Object> actualPayload = actual.toPayload();
+ List<?> checks = (List<?>) actualPayload.get("checks");
+ assertThat(actualPayload.get("status"), is("failed"));
+ assertThat(actualPayload.get("category"), is(expectedCategory));
+ assertThat(((Map<?, ?>) checks.get(1)).get("status"),
is(expectedDriverStatus));
+ assertThat(((Map<?, ?>) checks.get(2)).get("status"),
is(expectedConnectivityStatus));
+ assertThat(((Map<?, ?>) checks.get(3)).get("status"), is("skipped"));
+ assertThat(((Map<?, ?>) checks.get(4)).get("status"), is("skipped"));
+ verifyNoInteractions(metadataLoader);
+ }
+
+ private static Stream<Arguments>
assertValidateFailsDuringProfileLoadCases() {
+ return Stream.of(
+ Arguments.of("missing driver",
RuntimeDatabaseConnectionException.missingJdbcDriver("logic_db", new
ClassNotFoundException("missing")), "failed", "skipped",
+
RuntimeDatabaseConnectionException.CATEGORY_MISSING_JDBC_DRIVER),
+ Arguments.of("authentication failed",
RuntimeDatabaseConnectionException.connectionFailed("logic_db", new
SQLException("Access denied", "28000")), "passed", "failed",
+
RuntimeDatabaseConnectionException.CATEGORY_AUTHENTICATION_FAILED),
+ Arguments.of("authorization failed",
RuntimeDatabaseConnectionException.connectionFailed("logic_db", new
SQLException("permission denied", "42501")), "passed", "failed",
+
RuntimeDatabaseConnectionException.CATEGORY_AUTHORIZATION_FAILED),
+ Arguments.of("connection timeout",
RuntimeDatabaseConnectionException.connectionFailed("logic_db", new
SQLTimeoutException("timed out")), "passed", "failed",
+
RuntimeDatabaseConnectionException.CATEGORY_CONNECTION_TIMEOUT),
+ Arguments.of("database type mismatch",
RuntimeDatabaseConnectionException.invalidConfiguration("logic_db", new
IllegalStateException("mismatch")), "passed", "failed",
+
RuntimeDatabaseConnectionException.CATEGORY_INVALID_CONFIGURATION));
+ }
+
+ private static RuntimeDatabaseProfile createProfile() {
+ return new RuntimeDatabaseProfile("logic_db", "MySQL", "8.0.36");
+ }
+
+ private static RuntimeDatabaseConfiguration
createRuntimeDatabaseConfiguration() {
+ return new RuntimeDatabaseConfiguration("MySQL", "jdbc:test:profile",
"demo", "", "com.mysql.cj.jdbc.Driver");
+ }
+
+ private static MCPDatabaseMetadata createMetadata(final String schemaName)
{
+ return new MCPDatabaseMetadata("logic_db", "MySQL", "8.0.36",
List.of(new MCPSchemaMetadata("logic_db", schemaName, List.of(), List.of(),
List.of())));
+ }
+
+ private static Map<String, Object> createRecoveryPayload(final
RuntimeDatabaseConnectionException cause) {
+ return Map.of("category", cause.getCategory());
+ }
+
+ private static final class InvisibleDatabaseDriver implements Driver {
+
+ private static final String JDBC_URL = "jdbc:preflight:invisible";
+
+ private static final InvisibleDatabaseDriver INSTANCE = new
InvisibleDatabaseDriver();
+
+ static {
+ try {
+ DriverManager.registerDriver(INSTANCE);
+ } catch (final SQLException ex) {
+ throw new ExceptionInInitializerError(ex);
+ }
+ }
+
+ @Override
+ public Connection connect(final String url, final Properties info)
throws SQLException {
+ if (!acceptsURL(url)) {
+ return null;
+ }
+ Connection result = mock(Connection.class);
+ DatabaseMetaData databaseMetaData = mock(DatabaseMetaData.class);
+ ResultSet catalogs = mock(ResultSet.class);
+ ResultSet schemas = mock(ResultSet.class);
+ when(result.getCatalog()).thenReturn("");
+ when(result.getSchema()).thenReturn("");
+ when(result.getMetaData()).thenReturn(databaseMetaData);
+ when(databaseMetaData.getCatalogs()).thenReturn(catalogs);
+ when(databaseMetaData.getSchemas()).thenReturn(schemas);
+ when(catalogs.next()).thenReturn(false);
+ when(schemas.next()).thenReturn(false);
+ return result;
+ }
+
+ @Override
+ public boolean acceptsURL(final String url) {
+ return JDBC_URL.equals(url);
+ }
+
+ @Override
+ public DriverPropertyInfo[] getPropertyInfo(final String url, final
Properties info) {
+ return new DriverPropertyInfo[0];
+ }
+
+ @Override
+ public int getMajorVersion() {
+ return 1;
+ }
+
+ @Override
+ public int getMinorVersion() {
+ return 0;
+ }
+
+ @Override
+ public boolean jdbcCompliant() {
+ return false;
+ }
+
+ @Override
+ public Logger getParentLogger() throws SQLFeatureNotSupportedException
{
+ throw new SQLFeatureNotSupportedException("Invisible database
driver does not expose a parent logger.");
+ }
+ }
+}
diff --git
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/AbstractProductionMySQLRuntimeE2ETest.java
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/AbstractProductionMySQLRuntimeE2ETest.java
index 18269c66f46..498e927094d 100644
---
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/AbstractProductionMySQLRuntimeE2ETest.java
+++
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/AbstractProductionMySQLRuntimeE2ETest.java
@@ -246,10 +246,12 @@ abstract class AbstractProductionMySQLRuntimeE2ETest
extends AbstractTransportPa
Map<String, Object> modelFirstSummary =
getMap(capabilities.get("model_first_summary"));
assertThat(getMap(modelFirstSummary.get("official_discovery_methods")).get("tools"),
is("tools/list"));
assertThat(modelFirstSummary.get("optional_catalog_resource"),
is("shardingsphere://capabilities"));
+
assertThat(getMap(modelFirstSummary.get("preflight_rule")).get("tool"),
is("database_gateway_validate_proxy_connectivity"));
assertThat(getMap(getMap(modelFirstSummary.get("sql_tool_selection")).get("read_only")).get("tool"),
is("database_gateway_execute_query"));
assertThat(getMap(getMap(modelFirstSummary.get("workflow_rule")).get("preview_tool")).get("tool"),
is("database_gateway_apply_workflow"));
Map<String, Object> surfaceSummary =
getMap(capabilities.get("surface_summary"));
assertThat(getMap(surfaceSummary.get("official_discovery_methods")).get("resources"),
is("resources/list"));
+ assertThat(surfaceSummary.get("preflight_validation_tool"),
is("database_gateway_validate_proxy_connectivity"));
assertThat(surfaceSummary.get("read_only_sql_tool"),
is("database_gateway_execute_query"));
assertThat(surfaceSummary.get("side_effect_sql_tool"),
is("database_gateway_execute_update"));
}
diff --git
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/PackagedDistributionE2ETest.java
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/PackagedDistributionE2ETest.java
index 1464ccba44e..a0dae6017a6 100644
---
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/PackagedDistributionE2ETest.java
+++
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/PackagedDistributionE2ETest.java
@@ -68,8 +68,8 @@ class PackagedDistributionE2ETest {
private static final List<String> EXPECTED_RUNTIME_ARTIFACT_IDS = List.of(
"shardingsphere-mcp-bootstrap",
"shardingsphere-mcp-feature-encrypt", "shardingsphere-mcp-feature-mask");
- private static final List<String> CORE_TOOL_NAMES =
List.of("database_gateway_search_metadata", "database_gateway_execute_query",
"database_gateway_execute_update",
- "database_gateway_apply_workflow",
"database_gateway_validate_workflow");
+ private static final List<String> CORE_TOOL_NAMES =
List.of("database_gateway_search_metadata",
"database_gateway_validate_proxy_connectivity",
+ "database_gateway_execute_query",
"database_gateway_execute_update", "database_gateway_apply_workflow",
"database_gateway_validate_workflow");
private static final List<String> REMOVED_FEATURE_TOOL_NAMES =
OfficialMCPToolNames.getAll().stream().filter(each ->
!CORE_TOOL_NAMES.contains(each)).toList();
@@ -274,8 +274,8 @@ class PackagedDistributionE2ETest {
private void assertDiscoveredTools(final List<Map<String, Object>> tools) {
List<String> actualToolNames = tools.stream().map(each ->
String.valueOf(each.get("name"))).toList();
- assertThat(actualToolNames,
hasItems("database_gateway_search_metadata", "database_gateway_execute_query",
"database_gateway_execute_update",
- "database_gateway_apply_workflow",
"database_gateway_validate_workflow", "fixture_ping"));
+ assertThat(actualToolNames,
hasItems("database_gateway_search_metadata",
"database_gateway_validate_proxy_connectivity",
"database_gateway_execute_query",
+ "database_gateway_execute_update",
"database_gateway_apply_workflow", "database_gateway_validate_workflow",
"fixture_ping"));
for (String each : REMOVED_FEATURE_TOOL_NAMES) {
assertFalse(actualToolNames.contains(each));
}
@@ -300,8 +300,8 @@ class PackagedDistributionE2ETest {
private void assertCapabilities(final Map<String, Object> payload) {
List<String> actualSupportedTools = ((List<?>)
payload.get("supportedTools")).stream().map(String::valueOf).toList();
- assertThat(actualSupportedTools,
hasItems("database_gateway_search_metadata", "database_gateway_execute_query",
"database_gateway_execute_update",
- "database_gateway_apply_workflow",
"database_gateway_validate_workflow", "fixture_ping"));
+ assertThat(actualSupportedTools,
hasItems("database_gateway_search_metadata",
"database_gateway_validate_proxy_connectivity",
"database_gateway_execute_query",
+ "database_gateway_execute_update",
"database_gateway_apply_workflow", "database_gateway_validate_workflow",
"fixture_ping"));
for (String each : REMOVED_FEATURE_TOOL_NAMES) {
assertFalse(actualSupportedTools.contains(each));
}
diff --git
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/ProductionMySQLRuntimeE2ETest.java
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/ProductionMySQLRuntimeE2ETest.java
index 9b5d75911af..233b57f8a2d 100644
---
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/ProductionMySQLRuntimeE2ETest.java
+++
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/production/ProductionMySQLRuntimeE2ETest.java
@@ -78,6 +78,7 @@ class ProductionMySQLRuntimeE2ETest extends
AbstractProductionMySQLRuntimeE2ETes
List<Map<String, Object>> actual = interactionClient.listTools();
assertOfficialToolNames(actual.stream().map(each ->
String.valueOf(each.get("name"))).toList());
assertToolDefinition(actual, "database_gateway_search_metadata",
"Search Metadata", "", "object_types", "array");
+ assertToolDefinition(actual,
"database_gateway_validate_proxy_connectivity", "Validate Proxy Connectivity",
"database", "database", "string");
assertToolDefinition(actual, "database_gateway_execute_query",
"Execute Query SQL", "sql", "timeout_ms", "integer");
assertToolDefinition(actual, "database_gateway_execute_update",
"Execute Update SQL", "sql", "timeout_ms", "integer");
}
diff --git
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/programmatic/HttpTransportContractE2ETest.java
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/programmatic/HttpTransportContractE2ETest.java
index 0f1e93de2c1..341197afbb2 100644
---
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/programmatic/HttpTransportContractE2ETest.java
+++
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/runtime/programmatic/HttpTransportContractE2ETest.java
@@ -44,7 +44,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
class HttpTransportContractE2ETest extends
AbstractHttpProgrammaticRuntimeE2ETest {
private static final List<String> OFFICIAL_TOOL_NAMES = List.of(
- "database_gateway_search_metadata",
"database_gateway_execute_query", "database_gateway_execute_update",
"database_gateway_apply_workflow",
+ "database_gateway_search_metadata",
"database_gateway_validate_proxy_connectivity",
"database_gateway_execute_query", "database_gateway_execute_update",
"database_gateway_apply_workflow",
"database_gateway_validate_workflow",
"database_gateway_plan_encrypt_rule", "database_gateway_plan_mask_rule");
private static final String PLAN_MASK_TOOL_NAME =
"database_gateway_plan_mask_rule";
diff --git
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/OfficialMCPToolNames.java
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/OfficialMCPToolNames.java
index 5bef92123fc..66c91ffb1da 100644
---
a/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/OfficialMCPToolNames.java
+++
b/test/e2e/mcp/src/test/java/org/apache/shardingsphere/test/e2e/mcp/support/OfficialMCPToolNames.java
@@ -26,6 +26,7 @@ public final class OfficialMCPToolNames {
private static final List<String> ALL = List.of(
"database_gateway_search_metadata",
+ "database_gateway_validate_proxy_connectivity",
"database_gateway_execute_query",
"database_gateway_execute_update",
"database_gateway_apply_workflow",
diff --git
a/test/e2e/mcp/src/test/resources/baseline-contract/model-contract/capabilities.yaml
b/test/e2e/mcp/src/test/resources/baseline-contract/model-contract/capabilities.yaml
index cfac7e92c06..86c42f20d43 100644
---
a/test/e2e/mcp/src/test/resources/baseline-contract/model-contract/capabilities.yaml
+++
b/test/e2e/mcp/src/test/resources/baseline-contract/model-contract/capabilities.yaml
@@ -27,6 +27,9 @@ model_first_summary:
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.}
+ preflight_rule: {tool: database_gateway_validate_proxy_connectivity,
input_rule: Pass
+ only a configured runtime database name., secret_rule: Connection
details stay
+ in administrator runtime configuration and are not tool arguments.}
sql_tool_selection:
read_only: {tool: database_gateway_execute_query, statement_rule: Use for
one
SELECT or EXPLAIN ANALYZE statement.}
@@ -51,6 +54,8 @@ model_contract:
argument_completion_method: completion/complete
optional_catalog_resource: shardingsphere://capabilities
metadata_first_resource: shardingsphere://databases
+ preflight_rule: Use database_gateway_validate_proxy_connectivity with a
configured
+ database name before onboarding or troubleshooting runtime connectivity.
sql_tool_selection: {side_effecting: Use database_gateway_execute_update
with execution_mode=preview
before execution., read_only: Use database_gateway_execute_query for one
classifier-approved
SELECT or EXPLAIN ANALYZE statement.}
@@ -71,6 +76,7 @@ surface_summary:
optional_catalog_resource: shardingsphere://capabilities
metadata_resource: shardingsphere://databases
metadata_search_tool: database_gateway_search_metadata
+ preflight_validation_tool: database_gateway_validate_proxy_connectivity
read_only_sql_tool: database_gateway_execute_query
side_effect_sql_tool: database_gateway_execute_update
workflow_tools: [database_gateway_apply_workflow,
database_gateway_validate_workflow]
@@ -121,6 +127,12 @@ common_flows:
stop_condition: Stop when the requested metadata detail resource is read.
referenced_tools: [database_gateway_search_metadata]
referenced_resources: ['shardingsphere://databases']
+- flow_id: validate_runtime_database
+ steps: [read_resource shardingsphere://databases, call_tool
database_gateway_validate_proxy_connectivity]
+ stop_condition: Stop after the configured runtime database reports ready or
returns
+ structured recovery guidance.
+ referenced_tools: [database_gateway_validate_proxy_connectivity]
+ referenced_resources: ['shardingsphere://databases']
- flow_id: read_only_sql
steps: ['read_resource shardingsphere://databases/{database}/capabilities',
call_tool
database_gateway_execute_query]
@@ -152,9 +164,9 @@ common_flows:
referenced_resources: ['shardingsphere://capabilities']
protocolAvailability: {resources: true, resourceTemplates: true, tools: true,
toolAnnotations: true,
toolOutputSchemas: true, prompts: true, completions: true,
resourceNavigation: true}
-fingerprints: {algorithm: sha256, descriptorCatalog:
f4ad81145c1af6bb9223e71ee783865c29edbf9b25d9ecc6999a1934fbc929e8,
+fingerprints: {algorithm: sha256, descriptorCatalog:
48d9d27f8c40295dcd4f44fbaeaca59d963e90848bbc1f20f66fc150a358a096,
promptSet: 3ce46d5c41e261470f1846680fab5274b38b35457b7d0a92d0dd0659ec8b5c17,
resourceNavigation:
64abf531e9c72549a99ab5f23ee2e0f6d769e45e19b56c73f86144e8f2e4799f,
- modelFacingSchemas:
9acdd7ed62c0ad8f3f184debf0b4f33868a8af3b677ea4212cf785a74c1ded8b}
+ modelFacingSchemas:
a782fa53deb816c51ce3e2a4a3b27c1a04b0f651454953594767d606ec34060f}
resources:
- uri: shardingsphere://capabilities
name: server-capability-catalog
@@ -353,6 +365,9 @@ tools:
- name: database_gateway_search_metadata
annotations: {title: Search Metadata, readOnlyHint: true, destructiveHint:
false,
idempotentHint: true, openWorldHint: true}
+- name: database_gateway_validate_proxy_connectivity
+ annotations: {title: Validate Proxy Connectivity, readOnlyHint: true,
destructiveHint: false,
+ idempotentHint: true, openWorldHint: true}
- name: database_gateway_execute_query
annotations: {title: Execute Query SQL, readOnlyHint: true, destructiveHint:
false,
idempotentHint: true, openWorldHint: true}