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 0de7fa09452 Harden runtime configuration and diagnostics (#38752)
0de7fa09452 is described below
commit 0de7fa09452db46caf605c1f6dc74c435efa2ed4
Author: Liang Zhang <[email protected]>
AuthorDate: Fri May 29 18:04:06 2026 +0800
Harden runtime configuration and diagnostics (#38752)
- require username and driverClassName for runtime database entries
- allow omitted password for no-password ShardingSphere-Proxy accounts
- remove demo-looking runtime config and empty-driver guidance from docs
and distribution config
- keep HTTP origin rejection responses generic and log safe server-side
categories
- add safe metadata empty-state and runtime connection diagnostic categories
- route packaged console logs to stderr for stdio protocol safety
---
.../mcp/src/main/resources/conf/logback.xml | 6 +--
.../mcp/src/main/resources/conf/mcp-http.yaml | 13 ++---
.../mcp/src/main/resources/conf/mcp-stdio.yaml | 13 ++---
.../shardingsphere-mcp/configuration.cn.md | 6 +--
.../shardingsphere-mcp/configuration.en.md | 6 +--
.../shardingsphere-mcp/quick-start.cn.md | 1 +
.../shardingsphere-mcp/quick-start.en.md | 1 +
.../shardingsphere-mcp/troubleshooting.cn.md | 12 ++---
.../shardingsphere-mcp/troubleshooting.en.md | 12 ++---
.../config/YamlRuntimeDatabaseConfiguration.java | 6 +--
.../YamlRuntimeDatabaseConfigurationSwapper.java | 3 +-
...YamlRuntimeDatabaseConfigurationsValidator.java | 13 +----
.../constraint/OriginHeaderConstraint.java | 15 ++++--
.../config/MCPLaunchConfigurationTest.java | 4 +-
.../config/loader/MCPConfigurationLoaderTest.java | 4 +-
.../YamlMCPLaunchConfigurationSwapperTest.java | 44 +++++++++++++---
.../YamlMCPTransportConfigurationSwapperTest.java | 2 +-
...amlRuntimeDatabaseConfigurationSwapperTest.java | 28 +++++++---
...mlRuntimeDatabaseConfigurationsSwapperTest.java | 17 ++++--
.../constraint/OriginHeaderConstraintTest.java | 6 +--
.../error/MCPBasicRecoveryPayloadFactory.java | 3 ++
.../protocol/error/MCPRecoveryPayloadSupport.java | 1 +
.../handler/capability/RuntimeStatusHandler.java | 2 +
.../handler/metadata/MetadataResourceHandler.java | 60 +++++++++++++++++-----
.../mcp/core/protocol/MCPErrorConverterTest.java | 11 ++++
.../capability/RuntimeStatusHandlerTest.java | 7 +--
.../metadata/MetadataResourceHandlerTest.java | 44 ++++++++++++++++
.../jdbc/MCPJdbcDatabaseProfileLoader.java | 4 +-
.../metadata/jdbc/MCPJdbcMetadataLoader.java | 4 +-
.../jdbc/RuntimeDatabaseConfiguration.java | 10 ++--
.../jdbc/RuntimeDatabaseConnectionException.java | 5 ++
.../metadata/jdbc/MCPJdbcMetadataLoaderTest.java | 14 ++---
.../jdbc/RuntimeDatabaseConfigurationTest.java | 13 ++++-
.../RuntimeDatabaseConnectionExceptionTest.java | 7 +++
34 files changed, 284 insertions(+), 113 deletions(-)
diff --git a/distribution/mcp/src/main/resources/conf/logback.xml
b/distribution/mcp/src/main/resources/conf/logback.xml
index d1ecc7aa00f..a222c15c3b0 100644
--- a/distribution/mcp/src/main/resources/conf/logback.xml
+++ b/distribution/mcp/src/main/resources/conf/logback.xml
@@ -20,7 +20,7 @@
<property name="APP_HOME" value="${APP_HOME:-.}" />
<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread]
%-5level %logger - %msg%n" />
- <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+ <appender name="STDERR" class="ch.qos.logback.core.ConsoleAppender">
<target>System.err</target>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
@@ -41,12 +41,12 @@
</appender>
<logger name="org.apache.shardingsphere" level="INFO" additivity="false">
- <appender-ref ref="STDOUT" />
+ <appender-ref ref="STDERR" />
<appender-ref ref="FILE" />
</logger>
<root level="WARN">
- <appender-ref ref="STDOUT" />
+ <appender-ref ref="STDERR" />
<appender-ref ref="FILE" />
</root>
</configuration>
diff --git a/distribution/mcp/src/main/resources/conf/mcp-http.yaml
b/distribution/mcp/src/main/resources/conf/mcp-http.yaml
index 20d2ccee47e..9eb520c1914 100644
--- a/distribution/mcp/src/main/resources/conf/mcp-http.yaml
+++ b/distribution/mcp/src/main/resources/conf/mcp-http.yaml
@@ -18,10 +18,11 @@ transport:
runtimeDatabases:
# Configure this entry before startup.
- # Replace logic_db, JDBC URL, username, and password with your
ShardingSphere-Proxy logical database.
- logic_db:
+ # Replace the placeholders with your ShardingSphere-Proxy logical database.
+ # Omit password or set it to an empty string when the Proxy account has no
password.
+ "<logic-database>":
databaseType: MySQL
- jdbcUrl:
"jdbc:mysql://127.0.0.1:3307/logic_db?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC"
- username: "proxy_user"
- password: "proxy_password"
- driverClassName: com.mysql.cj.jdbc.Driver
+ jdbcUrl: "jdbc:mysql://<proxy-host>:<proxy-port>/<logic-database>"
+ username: "<proxy-username>"
+ password: "<proxy-password>"
+ driverClassName: "com.mysql.cj.jdbc.Driver"
diff --git a/distribution/mcp/src/main/resources/conf/mcp-stdio.yaml
b/distribution/mcp/src/main/resources/conf/mcp-stdio.yaml
index ccdc19c72c0..c4cae404daf 100644
--- a/distribution/mcp/src/main/resources/conf/mcp-stdio.yaml
+++ b/distribution/mcp/src/main/resources/conf/mcp-stdio.yaml
@@ -18,10 +18,11 @@ transport:
runtimeDatabases:
# Configure this entry before startup.
- # Replace logic_db, JDBC URL, username, and password with your
ShardingSphere-Proxy logical database.
- logic_db:
+ # Replace the placeholders with your ShardingSphere-Proxy logical database.
+ # Omit password or set it to an empty string when the Proxy account has no
password.
+ "<logic-database>":
databaseType: MySQL
- jdbcUrl:
"jdbc:mysql://127.0.0.1:3307/logic_db?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC"
- username: "proxy_user"
- password: "proxy_password"
- driverClassName: com.mysql.cj.jdbc.Driver
+ jdbcUrl: "jdbc:mysql://<proxy-host>:<proxy-port>/<logic-database>"
+ username: "<proxy-username>"
+ password: "<proxy-password>"
+ driverClassName: "com.mysql.cj.jdbc.Driver"
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 7c7d80d57ae..567d8d946d2 100644
--- a/docs/document/content/user-manual/shardingsphere-mcp/configuration.cn.md
+++ b/docs/document/content/user-manual/shardingsphere-mcp/configuration.cn.md
@@ -60,9 +60,9 @@ runtimeDatabases:
| --- | --- | --- |
| `databaseType` | 是 | 数据库类型,例如 `MySQL` 或 `PostgreSQL`。 |
| `jdbcUrl` | 是 | MCP Server 连接逻辑库的 JDBC URL。 |
-| `username` | 是 | 连接逻辑库的用户名;无用户名时写空字符串 `""`。 |
-| `password` | 是 | 连接逻辑库的密码;无密码时写空字符串 `""`。 |
-| `driverClassName` | 是 | JDBC 驱动类名;如果 JDBC 4 驱动可自动注册且不需要显式覆盖,写空字符串 `""`。 |
+| `username` | 是 | 连接 ShardingSphere-Proxy 逻辑库的用户名。 |
+| `password` | 否 | 连接 ShardingSphere-Proxy 逻辑库的密码;无密码账号可以省略或写空字符串 `""`。 |
+| `driverClassName` | 是 | JDBC 驱动类名,例如 MySQL 驱动使用 `com.mysql.cj.jdbc.Driver`。 |
注意事项:
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 c74f20597c1..15f9340ae65 100644
--- a/docs/document/content/user-manual/shardingsphere-mcp/configuration.en.md
+++ b/docs/document/content/user-manual/shardingsphere-mcp/configuration.en.md
@@ -60,9 +60,9 @@ runtimeDatabases:
| --- | --- | --- |
| `databaseType` | Yes | Database type, such as `MySQL` or `PostgreSQL`. |
| `jdbcUrl` | Yes | JDBC URL used by the MCP Server to connect to the logical
database. |
-| `username` | Yes | Username for the logical database; use an empty string
`""` when no username is needed. |
-| `password` | Yes | Password for the logical database; use an empty string
`""` when no password is needed. |
-| `driverClassName` | Yes | JDBC driver class name; use an empty string `""`
when a JDBC 4 driver auto-registers and no explicit override is needed. |
+| `username` | Yes | Username for the ShardingSphere-Proxy logical database. |
+| `password` | No | Password for the ShardingSphere-Proxy logical database.
Omit it or use an empty string `""` for a no-password account. |
+| `driverClassName` | Yes | JDBC driver class name, such as
`com.mysql.cj.jdbc.Driver` for the MySQL driver. |
Notes:
diff --git
a/docs/document/content/user-manual/shardingsphere-mcp/quick-start.cn.md
b/docs/document/content/user-manual/shardingsphere-mcp/quick-start.cn.md
index 1a8eec2d36a..cfc68dd0046 100644
--- a/docs/document/content/user-manual/shardingsphere-mcp/quick-start.cn.md
+++ b/docs/document/content/user-manual/shardingsphere-mcp/quick-start.cn.md
@@ -44,6 +44,7 @@ runtimeDatabases:
```
将 `<logic-database>`、`<proxy-host>`、`<proxy-port>`、`<proxy-username>` 和
`<proxy-password>` 替换为 ShardingSphere-Proxy 的实际连接信息。
+如果 Proxy 账号无密码,可以省略 `password`,或把它写成空字符串 `""`。
如果目标数据库驱动没有随发行包提供,请在启动前把对应 JDBC 驱动 jar 放入 `plugins/`。
## 启动 HTTP MCP Server
diff --git
a/docs/document/content/user-manual/shardingsphere-mcp/quick-start.en.md
b/docs/document/content/user-manual/shardingsphere-mcp/quick-start.en.md
index 0b1c60dd48c..e162f6d7c28 100644
--- a/docs/document/content/user-manual/shardingsphere-mcp/quick-start.en.md
+++ b/docs/document/content/user-manual/shardingsphere-mcp/quick-start.en.md
@@ -44,6 +44,7 @@ runtimeDatabases:
```
Replace `<logic-database>`, `<proxy-host>`, `<proxy-port>`,
`<proxy-username>`, and `<proxy-password>` with the actual ShardingSphere-Proxy
connection information.
+For a no-password Proxy account, omit `password` or set it to an empty string
`""`.
If the target database driver is not packaged, copy the corresponding JDBC
driver jar to `plugins/` before startup.
## Start the HTTP MCP Server
diff --git
a/docs/document/content/user-manual/shardingsphere-mcp/troubleshooting.cn.md
b/docs/document/content/user-manual/shardingsphere-mcp/troubleshooting.cn.md
index 7c7c0e614f4..001f79e00a4 100644
--- a/docs/document/content/user-manual/shardingsphere-mcp/troubleshooting.cn.md
+++ b/docs/document/content/user-manual/shardingsphere-mcp/troubleshooting.cn.md
@@ -10,20 +10,20 @@ weight = 7
| 现象 | 可能原因 | 处理方式 | 是否需要代码改进 |
| --- | --- | --- | --- |
-| 启动失败 | JDK、配置路径、YAML 字段或必填字段不正确。 | 查看终端错误和 `logs/mcp.log`。 |
部分需要:字段可省略能力待优化。 |
+| 启动失败 | JDK、配置路径、YAML 字段或必填字段不正确。 | 查看终端错误和 `logs/mcp.log`。 | 通常不需要。 |
| HTTP 无法连接 | 端口、端点路径、传输方式或绑定地址不正确。 | 检查 `port`、`endpointPath`、`bindHost` 和客户端
URL。 | 通常不需要。 |
-| HTTP 返回 403 | 请求 `Origin` 与绑定地址安全策略不匹配。 | 本机调试用回环地址;远程访问走受控网关。 |
可以改进:错误响应可给出更明确提示。 |
+| HTTP 返回 403 | 请求 `Origin` 与绑定地址安全策略不匹配。 | 本机调试用回环地址;远程访问走受控网关;详细原因看服务端日志。 |
通常不需要。 |
| 会话请求失败 | 未初始化、缺少会话头,或复用已关闭会话。 | 先调用 `initialize`,后续请求持续携带响应头。 | 通常不需要。 |
-| STDIO 没有响应 | 被当成人工交互 Shell,或 stdout 被日志污染。 | 由 MCP 客户端拉起进程;诊断信息看 stderr 或日志。
| 可以改进:继续保护 stdout。 |
-| 逻辑库或元数据为空 | 配置、驱动或权限不正确。 | 确认连接 Proxy 逻辑库,并检查驱动和权限。 | 可以改进:空结果可给出诊断。 |
-| JDBC 驱动错误 | 驱动不在类路径,或 `driverClassName` 不正确。 | 把驱动 jar 放入
`plugins/`,或加入嵌入式运行时类路径。 | 部分需要:字段可省略能力待优化。 |
+| STDIO 没有响应 | 被当成人工交互 Shell,或客户端未按 MCP stdio 协议发送 JSON-RPC。 | 由 MCP
客户端拉起进程;诊断信息看 stderr 或日志。 | 通常不需要。 |
+| 逻辑库或元数据为空 | 未配置逻辑库、逻辑库名称不正确、连接失败、权限不足,或目标范围确实为空。 | 先读
`shardingsphere://runtime`,再看资源返回的 `empty_state` 和 `recovery`。 | 通常不需要。 |
+| JDBC 驱动错误 | 驱动不在类路径,或 `driverClassName` 不正确。 | 把驱动 jar 放入 `plugins/`,并确认
`driverClassName` 非空且类名正确。 | 通常不需要。 |
| SQL 工具调用失败 | 工具选错、多语句被拒绝或参数超限。 | 查询用 `execute_query`;有副作用 SQL 用
`execute_update` 并先预览。 | 通常不需要;错误消息可增强。 |
| 工作流失败 | `plan_id`、会话、执行模式或人工执行步骤不正确。 | 同一会话内复用 `plan_id`;先预览;人工执行后再校验。 |
通常不需要。 |
| 敏感输入无法传递 | 补问要求密钥或凭证。 | 由客户端或运维侧取值,再通过受保护 MCP 调用传入。 | 如需服务端解析密钥引用,需要改代码。 |
补充说明:
-- `username`、`password` 和 `driverClassName` 目前必须显式写出;不需要值时写 `""`。
+- `username` 和 `driverClassName` 必须显式写出且不能为空;无密码账号可以省略 `password` 或写 `""`。
- `MCP-Session-Id` 和 `MCP-Protocol-Version` 来自 `initialize` 响应头,关闭会话后不能复用。
- 使用 `manual-only` 后,应先人工执行返回的 SQL 或 DistSQL,再调用校验工具。
- 人工执行包中的密钥占位符应由执行人员在受控环境替换。
diff --git
a/docs/document/content/user-manual/shardingsphere-mcp/troubleshooting.en.md
b/docs/document/content/user-manual/shardingsphere-mcp/troubleshooting.en.md
index 33163f1c957..fa767b9a9b2 100644
--- a/docs/document/content/user-manual/shardingsphere-mcp/troubleshooting.en.md
+++ b/docs/document/content/user-manual/shardingsphere-mcp/troubleshooting.en.md
@@ -10,20 +10,20 @@ For feature-specific business rule issues, see the
corresponding feature plugin
| Symptom | Possible cause | Action | Needs code improvement |
| --- | --- | --- | --- |
-| Startup failure | JDK, config path, YAML field, or required field is wrong.
| Inspect terminal output and `logs/mcp.log`. | Partially: optional field
support can be improved. |
+| Startup failure | JDK, config path, YAML field, or required field is wrong.
| Inspect terminal output and `logs/mcp.log`. | Usually no. |
| HTTP connection failure | Port, endpoint path, transport type, or bind
address is wrong. | Check `port`, `endpointPath`, `bindHost`, and client URL. |
Usually no. |
-| HTTP 403 response | Request `Origin` does not match the bind-address policy.
| Use loopback locally; use a controlled gateway for remote access. | Yes:
error response can give clearer hints. |
+| HTTP 403 response | Request `Origin` does not match the bind-address policy.
| Use loopback locally; use a controlled gateway for remote access; inspect
server logs for the safe reason category. | Usually no. |
| Session request failure | Session was not initialized, headers are missing,
or a closed session is reused. | Call `initialize` first and keep sending the
response headers. | Usually no. |
-| No response in STDIO mode | STDIO is used as a shell, or stdout is polluted
by logs. | Let an MCP client launch the process; read stderr or logs for
diagnostics. | Yes: keep protecting stdout. |
-| Logical database or metadata is empty | Config, driver, or permission is
wrong. | Confirm Proxy logical database, driver, and permissions. | Yes: empty
results can include diagnostics. |
-| JDBC driver error | Driver is not on classpath, or `driverClassName` is
wrong. | Put the driver jar under `plugins/`, or add it to the embedded runtime
classpath. | Partially: optional field support can be improved. |
+| No response in STDIO mode | STDIO is used as a shell, or the client does not
send JSON-RPC over MCP stdio. | Let an MCP client launch the process; read
stderr or logs for diagnostics. | Usually no. |
+| Logical database or metadata is empty | No logical database is configured,
the logical database name is wrong, connection failed, permission is
insufficient, or the target scope is empty. | Read `shardingsphere://runtime`,
then inspect `empty_state` and `recovery` in resource responses. | Usually no. |
+| JDBC driver error | Driver is not on classpath, or `driverClassName` is
wrong. | Put the driver jar under `plugins/`, and keep `driverClassName`
non-empty and correct. | Usually no. |
| SQL tool call failure | Wrong tool, multiple statements, or argument out of
range. | Use `execute_query` for queries; use `execute_update` with preview for
side effects. | Usually no; messages can improve. |
| Workflow failure | `plan_id`, session, execution mode, or manual step is
wrong. | Reuse `plan_id` in one session; preview first; validate after manual
execution. | Usually no. |
| Secret input cannot be passed safely | A clarification asks for a key or
credential. | Resolve it outside the server, then pass it through a protected
MCP call. | Server-side secret references require code changes. |
Additional notes:
-- `username`, `password`, and `driverClassName` must currently be declared
explicitly; use `""` when no value is needed.
+- `username` and `driverClassName` must be declared explicitly and cannot be
empty; a no-password account can omit `password` or use `""`.
- `MCP-Session-Id` and `MCP-Protocol-Version` come from the `initialize`
response headers and cannot be reused after close.
- After `manual-only`, execute the returned SQL or DistSQL manually before
calling validation.
- Secret placeholders in manual packages should be replaced by operators in a
controlled environment.
diff --git
a/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/config/yaml/config/YamlRuntimeDatabaseConfiguration.java
b/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/config/yaml/config/YamlRuntimeDatabaseConfiguration.java
index 27430c7e0d2..edf11ffbd41 100644
---
a/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/config/yaml/config/YamlRuntimeDatabaseConfiguration.java
+++
b/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/config/yaml/config/YamlRuntimeDatabaseConfiguration.java
@@ -22,7 +22,6 @@ import lombok.Setter;
import org.apache.shardingsphere.infra.util.yaml.YamlConfiguration;
import javax.validation.constraints.NotBlank;
-import javax.validation.constraints.NotNull;
/**
* YAML runtime database configuration.
@@ -37,12 +36,11 @@ public final class YamlRuntimeDatabaseConfiguration
implements YamlConfiguration
@NotBlank(message = "is required")
private String jdbcUrl;
- @NotNull(message = "is required. Use an empty string when no value is
needed")
+ @NotBlank(message = "is required")
private String username;
- @NotNull(message = "is required. Use an empty string when no value is
needed")
private String password;
- @NotNull(message = "is required. Use an empty string when no value is
needed")
+ @NotBlank(message = "is required")
private String driverClassName;
}
diff --git
a/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/config/yaml/swapper/YamlRuntimeDatabaseConfigurationSwapper.java
b/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/config/yaml/swapper/YamlRuntimeDatabaseConfigurationSwapper.java
index 195a24c0a66..e0f5bb24a7f 100644
---
a/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/config/yaml/swapper/YamlRuntimeDatabaseConfigurationSwapper.java
+++
b/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/config/yaml/swapper/YamlRuntimeDatabaseConfigurationSwapper.java
@@ -41,6 +41,7 @@ public final class YamlRuntimeDatabaseConfigurationSwapper
implements YamlConfig
@Override
public RuntimeDatabaseConfiguration swapToObject(final
YamlRuntimeDatabaseConfiguration yamlConfig) {
MCPYamlConfigurationValidator.validate(yamlConfig, "MCP runtime
database configuration");
- return new RuntimeDatabaseConfiguration(yamlConfig.getDatabaseType(),
yamlConfig.getJdbcUrl(), yamlConfig.getUsername(), yamlConfig.getPassword(),
yamlConfig.getDriverClassName());
+ return new RuntimeDatabaseConfiguration(yamlConfig.getDatabaseType(),
yamlConfig.getJdbcUrl(), yamlConfig.getUsername(),
+ null == yamlConfig.getPassword() ? "" :
yamlConfig.getPassword(), yamlConfig.getDriverClassName());
}
}
diff --git
a/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/config/yaml/validator/YamlRuntimeDatabaseConfigurationsValidator.java
b/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/config/yaml/validator/YamlRuntimeDatabaseConfigurationsValidator.java
index 16c96059fb2..30fcca59621 100644
---
a/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/config/yaml/validator/YamlRuntimeDatabaseConfigurationsValidator.java
+++
b/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/config/yaml/validator/YamlRuntimeDatabaseConfigurationsValidator.java
@@ -63,9 +63,8 @@ public final class YamlRuntimeDatabaseConfigurationsValidator
implements Constra
private boolean validateRequiredProperties(final Entry<String, Map<String,
Object>> databaseEntry, final ConstraintValidatorContext context) {
boolean result = validateRequiredText(databaseEntry,
YamlRuntimeDatabaseConfigurationProperties.DATABASE_TYPE, context);
result = validateRequiredText(databaseEntry,
YamlRuntimeDatabaseConfigurationProperties.JDBC_URL, context) && result;
- result = validateExplicitText(databaseEntry,
YamlRuntimeDatabaseConfigurationProperties.USERNAME, context) && result;
- result = validateExplicitText(databaseEntry,
YamlRuntimeDatabaseConfigurationProperties.PASSWORD, context) && result;
- return validateExplicitText(databaseEntry,
YamlRuntimeDatabaseConfigurationProperties.DRIVER_CLASS_NAME, context) &&
result;
+ result = validateRequiredText(databaseEntry,
YamlRuntimeDatabaseConfigurationProperties.USERNAME, context) && result;
+ return validateRequiredText(databaseEntry,
YamlRuntimeDatabaseConfigurationProperties.DRIVER_CLASS_NAME, context) &&
result;
}
private boolean validateRequiredText(final Entry<String, Map<String,
Object>> databaseEntry, final String key, final ConstraintValidatorContext
context) {
@@ -77,14 +76,6 @@ public final class
YamlRuntimeDatabaseConfigurationsValidator implements Constra
return false;
}
- private boolean validateExplicitText(final Entry<String, Map<String,
Object>> databaseEntry, final String key, final ConstraintValidatorContext
context) {
- if (null != databaseEntry.getValue().get(key)) {
- return true;
- }
- addViolation(context, String.format("contains database `%s` property
`%s` is required. Use an empty string when no value is needed",
databaseEntry.getKey(), key));
- return false;
- }
-
private void addViolation(final ConstraintValidatorContext context, final
String message) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(message).addConstraintViolation();
diff --git
a/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/transport/server/http/validator/constraint/OriginHeaderConstraint.java
b/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/transport/server/http/validator/constraint/OriginHeaderConstraint.java
index 4180b8ea903..cfb38cf5eaf 100644
---
a/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/transport/server/http/validator/constraint/OriginHeaderConstraint.java
+++
b/mcp/bootstrap/src/main/java/org/apache/shardingsphere/mcp/bootstrap/transport/server/http/validator/constraint/OriginHeaderConstraint.java
@@ -19,6 +19,7 @@ package
org.apache.shardingsphere.mcp.bootstrap.transport.server.http.validator.
import
io.modelcontextprotocol.server.transport.ServerTransportSecurityException;
import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
import org.apache.shardingsphere.infra.exception.ShardingSpherePreconditions;
import
org.apache.shardingsphere.mcp.bootstrap.transport.HttpTransportHostUtils;
import
org.apache.shardingsphere.mcp.bootstrap.transport.HttpTransportOriginUtils;
@@ -29,8 +30,11 @@ import java.net.URI;
* Origin header constraint.
*/
@RequiredArgsConstructor
+@Slf4j
public final class OriginHeaderConstraint implements TransportHeaderConstraint
{
+ private static final String FORBIDDEN_MESSAGE = "Origin is not allowed by
MCP HTTP transport policy.";
+
private final boolean loopbackBinding;
@Override
@@ -44,11 +48,14 @@ public final class OriginHeaderConstraint implements
TransportHeaderConstraint {
return;
}
String actualOrigin = HttpTransportOriginUtils.normalizeOrigin(value);
- ShardingSpherePreconditions.checkNotEmpty(actualOrigin,
this::createForbiddenException);
- ShardingSpherePreconditions.checkState(loopbackBinding &&
HttpTransportHostUtils.isLoopbackHost(URI.create(actualOrigin).getHost()),
this::createForbiddenException);
+ ShardingSpherePreconditions.checkNotEmpty(actualOrigin, () ->
createForbiddenException("invalid_origin"));
+ ShardingSpherePreconditions.checkState(loopbackBinding, () ->
createForbiddenException("origin_header_on_non_loopback_binding"));
+
ShardingSpherePreconditions.checkState(HttpTransportHostUtils.isLoopbackHost(URI.create(actualOrigin).getHost()),
+ () ->
createForbiddenException("non_loopback_origin_on_loopback_binding"));
}
- private ServerTransportSecurityException createForbiddenException() {
- return new ServerTransportSecurityException(403, "Origin is not
allowed for the current binding.");
+ private ServerTransportSecurityException createForbiddenException(final
String reason) {
+ log.warn("Rejected MCP HTTP request origin: reason={},
loopbackBinding={}.", reason, loopbackBinding);
+ return new ServerTransportSecurityException(403, FORBIDDEN_MESSAGE);
}
}
diff --git
a/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/config/MCPLaunchConfigurationTest.java
b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/config/MCPLaunchConfigurationTest.java
index a9c695960e0..92a45e52998 100644
---
a/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/config/MCPLaunchConfigurationTest.java
+++
b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/config/MCPLaunchConfigurationTest.java
@@ -136,8 +136,8 @@ class MCPLaunchConfigurationTest {
return Collections.singletonMap("logic_db", Map.of(
"databaseType", "MySQL",
"jdbcUrl", "jdbc:mysql://localhost:3306/logic_db",
- "username", "",
+ "username", "demo",
"password", "",
- "driverClassName", ""));
+ "driverClassName", "com.mysql.cj.jdbc.Driver"));
}
}
diff --git
a/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/config/loader/MCPConfigurationLoaderTest.java
b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/config/loader/MCPConfigurationLoaderTest.java
index c2217a874b3..2b37b7c76fc 100644
---
a/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/config/loader/MCPConfigurationLoaderTest.java
+++
b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/config/loader/MCPConfigurationLoaderTest.java
@@ -49,7 +49,7 @@ class MCPConfigurationLoaderTest {
logic_db:
databaseType: MySQL
jdbcUrl: jdbc:mysql://localhost:3306/logic_db
- username: ''
+ username: demo
password: ''
driverClassName: com.mysql.cj.jdbc.Driver
""";
@@ -61,7 +61,7 @@ class MCPConfigurationLoaderTest {
logic_db:
databaseType: MySQL
jdbcUrl: jdbc:mysql://localhost:3306/logic_db
- username: ''
+ username: demo
password: ''
driverClassName: com.mysql.cj.jdbc.Driver
""";
diff --git
a/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/config/yaml/swapper/YamlMCPLaunchConfigurationSwapperTest.java
b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/config/yaml/swapper/YamlMCPLaunchConfigurationSwapperTest.java
index 50627e1ce41..0f5809af16f 100644
---
a/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/config/yaml/swapper/YamlMCPLaunchConfigurationSwapperTest.java
+++
b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/config/yaml/swapper/YamlMCPLaunchConfigurationSwapperTest.java
@@ -64,6 +64,34 @@ class YamlMCPLaunchConfigurationSwapperTest {
assertThat(actual.getDatabases().get("logic_db").getUsername(),
is("demo"));
}
+ @Test
+ void assertSwapToObjectWithPasswordMissing() {
+ String yamlContent = "transport:\n"
+ + " type: STDIO\n"
+ + "runtimeDatabases:\n"
+ + " logic_db:\n"
+ + " databaseType: MySQL\n"
+ + " jdbcUrl: jdbc:mysql://localhost:3306/logic_db\n"
+ + " username: demo\n"
+ + " driverClassName: com.mysql.cj.jdbc.Driver\n";
+ MCPLaunchConfiguration actual =
swapper.swapToObject(YamlEngine.unmarshal(yamlContent,
YamlMCPLaunchConfiguration.class));
+ assertThat(actual.getDatabases().get("logic_db").getPassword(),
is(""));
+ }
+
+ @Test
+ void assertSwapToObjectWithBlankDriverClassName() {
+ String yamlContent = "transport:\n"
+ + " type: STDIO\n"
+ + "runtimeDatabases:\n"
+ + " logic_db:\n"
+ + " databaseType: MySQL\n"
+ + " jdbcUrl: jdbc:mysql://localhost:3306/logic_db\n"
+ + " username: demo\n"
+ + " driverClassName: ''\n";
+ IllegalArgumentException actual =
assertThrows(IllegalArgumentException.class, () ->
swapper.swapToObject(YamlEngine.unmarshal(yamlContent,
YamlMCPLaunchConfiguration.class)));
+ assertThat(actual.getMessage(), is("MCP launch configuration property
`runtimeDatabases` contains database `logic_db` property `driverClassName` is
required."));
+ }
+
@Test
void assertSwapToObjectWithNullConfiguration() {
IllegalArgumentException actual =
assertThrows(IllegalArgumentException.class, () -> swapper.swapToObject(null));
@@ -82,7 +110,7 @@ class YamlMCPLaunchConfigurationSwapperTest {
+ " logic_db:\n"
+ " databaseType: MySQL\n"
+ " jdbcUrl: jdbc:mysql://localhost:3306/logic_db\n"
- + " username: ''\n"
+ + " username: demo\n"
+ " password: ''\n"
+ " driverClassName: com.mysql.cj.jdbc.Driver\n",
YamlMCPLaunchConfiguration.class)));
assertThat(actual.getMessage(), is("MCP launch configuration property
`transport` is required."));
@@ -141,7 +169,7 @@ class YamlMCPLaunchConfigurationSwapperTest {
+ " 1:\n"
+ " databaseType: MySQL\n"
+ " jdbcUrl: jdbc:mysql://localhost:3306/logic_db\n"
- + " username: ''\n"
+ + " username: demo\n"
+ " password: ''\n"
+ " driverClassName: com.mysql.cj.jdbc.Driver\n";
MCPLaunchConfiguration actual =
swapper.swapToObject(YamlEngine.unmarshal(yamlContent,
YamlMCPLaunchConfiguration.class));
@@ -156,7 +184,7 @@ class YamlMCPLaunchConfigurationSwapperTest {
+ " '':\n"
+ " databaseType: MySQL\n"
+ " jdbcUrl: jdbc:mysql://localhost:3306/logic_db\n"
- + " username: ''\n"
+ + " username: demo\n"
+ " password: ''\n"
+ " driverClassName: com.mysql.cj.jdbc.Driver\n";
ConstructorException actual = assertThrows(ConstructorException.class,
() -> swapper.swapToObject(YamlEngine.unmarshal(yamlContent,
YamlMCPLaunchConfiguration.class)));
@@ -171,7 +199,7 @@ class YamlMCPLaunchConfigurationSwapperTest {
+ " null:\n"
+ " databaseType: MySQL\n"
+ " jdbcUrl: jdbc:mysql://localhost:3306/logic_db\n"
- + " username: ''\n"
+ + " username: demo\n"
+ " password: ''\n"
+ " driverClassName: com.mysql.cj.jdbc.Driver\n";
ConstructorException actual = assertThrows(ConstructorException.class,
() -> swapper.swapToObject(YamlEngine.unmarshal(yamlContent,
YamlMCPLaunchConfiguration.class)));
@@ -196,7 +224,7 @@ class YamlMCPLaunchConfigurationSwapperTest {
+ " logic_db:\n"
+ " databaseType: MySQL\n"
+ " jdbcUrl: jdbc:mysql://localhost:3306/logic_db\n"
- + " username: ''\n"
+ + " username: demo\n"
+ " password: ''\n"
+ " driverClassName: com.mysql.cj.jdbc.Driver\n"
+ " unsupported: true\n";
@@ -207,11 +235,11 @@ class YamlMCPLaunchConfigurationSwapperTest {
@Test
void assertSwapToYamlConfigurationWithRuntimeDatabases() {
Map<String, RuntimeDatabaseConfiguration> databases = new
LinkedHashMap<>(1, 1F);
- databases.put("logic_db", new RuntimeDatabaseConfiguration("MySQL",
"jdbc:mysql://localhost:3306/logic_db", "", "", "com.mysql.cj.jdbc.Driver"));
+ databases.put("logic_db", new RuntimeDatabaseConfiguration("MySQL",
"jdbc:mysql://localhost:3306/logic_db", "demo", "",
"com.mysql.cj.jdbc.Driver"));
MCPLaunchConfiguration launchConfig = new
MCPLaunchConfiguration(MCPTransportType.STREAMABLE_HTTP, new
HttpTransportConfiguration("127.0.0.1", 18088, "/mcp"), databases);
YamlMCPLaunchConfiguration actual =
swapper.swapToYamlConfiguration(launchConfig);
assertThat(String.valueOf(actual.getRuntimeDatabases().get("logic_db").get("databaseType")),
is("MySQL"));
-
assertThat(String.valueOf(actual.getRuntimeDatabases().get("logic_db").get("username")),
is(""));
+
assertThat(String.valueOf(actual.getRuntimeDatabases().get("logic_db").get("username")),
is("demo"));
assertThat(actual.getTransport().getType(),
is(MCPTransportType.STREAMABLE_HTTP));
assertThat(actual.getTransport().getHttp().getBindHost(),
is("127.0.0.1"));
}
@@ -229,7 +257,7 @@ class YamlMCPLaunchConfigurationSwapperTest {
+ " logic_db:\n"
+ " databaseType: MySQL\n"
+ " jdbcUrl: jdbc:mysql://localhost:3306/logic_db\n"
- + " username: ''\n"
+ + " username: demo\n"
+ " password: ''\n"
+ " driverClassName: com.mysql.cj.jdbc.Driver\n";
}
diff --git
a/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/config/yaml/swapper/YamlMCPTransportConfigurationSwapperTest.java
b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/config/yaml/swapper/YamlMCPTransportConfigurationSwapperTest.java
index d35eab71bb5..b8176989356 100644
---
a/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/config/yaml/swapper/YamlMCPTransportConfigurationSwapperTest.java
+++
b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/config/yaml/swapper/YamlMCPTransportConfigurationSwapperTest.java
@@ -90,7 +90,7 @@ class YamlMCPTransportConfigurationSwapperTest {
result.setRuntimeDatabases(Map.of("logic_db", Map.of(
"databaseType", "MySQL",
"jdbcUrl", "jdbc:mysql://localhost:3306/logic_db",
- "username", "",
+ "username", "demo",
"password", "",
"driverClassName", "com.mysql.cj.jdbc.Driver")));
return result;
diff --git
a/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/config/yaml/swapper/YamlRuntimeDatabaseConfigurationSwapperTest.java
b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/config/yaml/swapper/YamlRuntimeDatabaseConfigurationSwapperTest.java
index 53b97a4d71f..2790197166e 100644
---
a/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/config/yaml/swapper/YamlRuntimeDatabaseConfigurationSwapperTest.java
+++
b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/config/yaml/swapper/YamlRuntimeDatabaseConfigurationSwapperTest.java
@@ -76,15 +76,23 @@ class YamlRuntimeDatabaseConfigurationSwapperTest {
YamlRuntimeDatabaseConfiguration yamlConfig = createYamlConfig();
yamlConfig.setUsername(null);
IllegalArgumentException actual =
assertThrows(IllegalArgumentException.class, () ->
swapper.swapToObject(yamlConfig));
- assertThat(actual.getMessage(), is("MCP runtime database configuration
property `username` is required. Use an empty string when no value is
needed."));
+ assertThat(actual.getMessage(), is("MCP runtime database configuration
property `username` is required."));
+ }
+
+ @Test
+ void assertSwapToObjectWithBlankUsername() {
+ YamlRuntimeDatabaseConfiguration yamlConfig = createYamlConfig();
+ yamlConfig.setUsername(" ");
+ IllegalArgumentException actual =
assertThrows(IllegalArgumentException.class, () ->
swapper.swapToObject(yamlConfig));
+ assertThat(actual.getMessage(), is("MCP runtime database configuration
property `username` is required."));
}
@Test
void assertSwapToObjectWithPasswordMissing() {
YamlRuntimeDatabaseConfiguration yamlConfig = createYamlConfig();
yamlConfig.setPassword(null);
- IllegalArgumentException actual =
assertThrows(IllegalArgumentException.class, () ->
swapper.swapToObject(yamlConfig));
- assertThat(actual.getMessage(), is("MCP runtime database configuration
property `password` is required. Use an empty string when no value is
needed."));
+ RuntimeDatabaseConfiguration actual = swapper.swapToObject(yamlConfig);
+ assertThat(actual.getPassword(), is(""));
}
@Test
@@ -92,7 +100,15 @@ class YamlRuntimeDatabaseConfigurationSwapperTest {
YamlRuntimeDatabaseConfiguration yamlConfig = createYamlConfig();
yamlConfig.setDriverClassName(null);
IllegalArgumentException actual =
assertThrows(IllegalArgumentException.class, () ->
swapper.swapToObject(yamlConfig));
- assertThat(actual.getMessage(), is("MCP runtime database configuration
property `driverClassName` is required. Use an empty string when no value is
needed."));
+ assertThat(actual.getMessage(), is("MCP runtime database configuration
property `driverClassName` is required."));
+ }
+
+ @Test
+ void assertSwapToObjectWithBlankDriverClassName() {
+ YamlRuntimeDatabaseConfiguration yamlConfig = createYamlConfig();
+ yamlConfig.setDriverClassName(" ");
+ IllegalArgumentException actual =
assertThrows(IllegalArgumentException.class, () ->
swapper.swapToObject(yamlConfig));
+ assertThat(actual.getMessage(), is("MCP runtime database configuration
property `driverClassName` is required."));
}
@Test
@@ -104,10 +120,10 @@ class YamlRuntimeDatabaseConfigurationSwapperTest {
@Test
void assertSwapToYamlConfiguration() {
YamlRuntimeDatabaseConfiguration actual =
swapper.swapToYamlConfiguration(
- new RuntimeDatabaseConfiguration("MySQL",
"jdbc:mysql://localhost:3306/logic_db", "", "", "com.mysql.cj.jdbc.Driver"));
+ new RuntimeDatabaseConfiguration("MySQL",
"jdbc:mysql://localhost:3306/logic_db", "demo", "",
"com.mysql.cj.jdbc.Driver"));
assertThat(actual.getDatabaseType(), is("MySQL"));
assertThat(actual.getJdbcUrl(),
is("jdbc:mysql://localhost:3306/logic_db"));
- assertThat(actual.getUsername(), is(""));
+ assertThat(actual.getUsername(), is("demo"));
assertThat(actual.getPassword(), is(""));
assertThat(actual.getDriverClassName(),
is("com.mysql.cj.jdbc.Driver"));
}
diff --git
a/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/config/yaml/swapper/YamlRuntimeDatabaseConfigurationsSwapperTest.java
b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/config/yaml/swapper/YamlRuntimeDatabaseConfigurationsSwapperTest.java
index 14ab73da099..8d3c1a4c4d2 100644
---
a/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/config/yaml/swapper/YamlRuntimeDatabaseConfigurationsSwapperTest.java
+++
b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/config/yaml/swapper/YamlRuntimeDatabaseConfigurationsSwapperTest.java
@@ -37,7 +37,7 @@ class YamlRuntimeDatabaseConfigurationsSwapperTest {
Map<String, RuntimeDatabaseConfiguration> actual =
swapper.swapToObject(Map.of("logic_db", Map.of(
"databaseType", "MySQL",
"jdbcUrl", "jdbc:mysql://localhost:3306/logic_db",
- "username", "",
+ "username", "demo",
"password", "",
"driverClassName", "com.mysql.cj.jdbc.Driver")));
@@ -45,6 +45,17 @@ class YamlRuntimeDatabaseConfigurationsSwapperTest {
assertThat(actual.get("logic_db").getJdbcUrl(),
is("jdbc:mysql://localhost:3306/logic_db"));
}
+ @Test
+ void assertSwapToObjectWithPasswordMissing() {
+ Map<String, RuntimeDatabaseConfiguration> actual =
swapper.swapToObject(Map.of("logic_db", Map.of(
+ "databaseType", "MySQL",
+ "jdbcUrl", "jdbc:mysql://localhost:3306/logic_db",
+ "username", "demo",
+ "driverClassName", "com.mysql.cj.jdbc.Driver")));
+
+ assertThat(actual.get("logic_db").getPassword(), is(""));
+ }
+
@Test
void assertSwapToObjectWithNullRuntimeConfiguration() {
Map<String, RuntimeDatabaseConfiguration> actual =
swapper.swapToObject(null);
@@ -67,7 +78,7 @@ class YamlRuntimeDatabaseConfigurationsSwapperTest {
IllegalArgumentException actual =
assertThrows(IllegalArgumentException.class, () ->
swapper.swapToObject(Map.of("logic_db", Map.of(
"databaseType", "MySQL",
"jdbcUrl", "jdbc:mysql://localhost:3306/logic_db",
- "username", "",
+ "username", "demo",
"password", "",
"driverClassName", "com.mysql.cj.jdbc.Driver",
"unsupported", true))));
@@ -78,7 +89,7 @@ class YamlRuntimeDatabaseConfigurationsSwapperTest {
@Test
void assertSwapToYamlConfiguration() {
Map<String, Map<String, Object>> actual =
swapper.swapToYamlConfiguration(Map.of(
- "logic_db", new RuntimeDatabaseConfiguration("MySQL",
"jdbc:mysql://localhost:3306/logic_db", "", "", "com.mysql.cj.jdbc.Driver")));
+ "logic_db", new RuntimeDatabaseConfiguration("MySQL",
"jdbc:mysql://localhost:3306/logic_db", "demo", "",
"com.mysql.cj.jdbc.Driver")));
assertThat(String.valueOf(actual.get("logic_db").get("databaseType")),
is("MySQL"));
assertThat(String.valueOf(actual.get("logic_db").get("driverClassName")),
is("com.mysql.cj.jdbc.Driver"));
diff --git
a/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/server/http/validator/constraint/OriginHeaderConstraintTest.java
b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/server/http/validator/constraint/OriginHeaderConstraintTest.java
index d872daf2270..42167653f6c 100644
---
a/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/server/http/validator/constraint/OriginHeaderConstraintTest.java
+++
b/mcp/bootstrap/src/test/java/org/apache/shardingsphere/mcp/bootstrap/transport/server/http/validator/constraint/OriginHeaderConstraintTest.java
@@ -51,20 +51,20 @@ class OriginHeaderConstraintTest {
void assertValidateWithRemoteOriginOnLoopbackBinding() {
ServerTransportSecurityException actual =
assertThrows(ServerTransportSecurityException.class, () -> new
OriginHeaderConstraint(true).validate("http://example.com:8080"));
assertThat(actual.getStatusCode(), is(403));
- assertThat(actual.getMessage(), is("Origin is not allowed for the
current binding."));
+ assertThat(actual.getMessage(), is("Origin is not allowed by MCP HTTP
transport policy."));
}
@Test
void assertValidateWithLoopbackOriginOnNonLoopbackBinding() {
ServerTransportSecurityException actual =
assertThrows(ServerTransportSecurityException.class, () -> new
OriginHeaderConstraint(false).validate("http://127.0.0.1:8080"));
assertThat(actual.getStatusCode(), is(403));
- assertThat(actual.getMessage(), is("Origin is not allowed for the
current binding."));
+ assertThat(actual.getMessage(), is("Origin is not allowed by MCP HTTP
transport policy."));
}
@Test
void assertValidateWithInvalidOrigin() {
ServerTransportSecurityException actual =
assertThrows(ServerTransportSecurityException.class, () -> new
OriginHeaderConstraint(true).validate("://bad-origin"));
assertThat(actual.getStatusCode(), is(403));
- assertThat(actual.getMessage(), is("Origin is not allowed for the
current binding."));
+ assertThat(actual.getMessage(), is("Origin is not allowed by MCP HTTP
transport policy."));
}
}
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 e7dc2c4cae0..7a5ed3eb673 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
@@ -216,6 +216,9 @@ final class MCPBasicRecoveryPayloadFactory {
if
(RuntimeDatabaseConnectionException.CATEGORY_AUTHENTICATION_FAILED.equals(cause.getCategory()))
{
return "Check the runtime database credentials outside MCP, then
retry.";
}
+ if
(RuntimeDatabaseConnectionException.CATEGORY_AUTHORIZATION_FAILED.equals(cause.getCategory()))
{
+ return "Check runtime database account privileges outside MCP,
then retry.";
+ }
if
(RuntimeDatabaseConnectionException.CATEGORY_CONNECTION_TIMEOUT.equals(cause.getCategory()))
{
return "Check database reachability and timeout settings, 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 2a8800df641..67760f1324c 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
@@ -85,6 +85,7 @@ final class MCPRecoveryPayloadSupport {
private static boolean isRuntimeRecoveryCategory(final String category) {
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);
}
diff --git
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/resource/handler/capability/RuntimeStatusHandler.java
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/resource/handler/capability/RuntimeStatusHandler.java
index 8e892f84ad3..7aec6b9a9af 100644
---
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/resource/handler/capability/RuntimeStatusHandler.java
+++
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/resource/handler/capability/RuntimeStatusHandler.java
@@ -102,6 +102,7 @@ public final class RuntimeStatusHandler implements
MCPResourceHandler<MCPDatabas
return List.of(
RuntimeDatabaseConnectionException.CATEGORY_MISSING_JDBC_DRIVER,
RuntimeDatabaseConnectionException.CATEGORY_AUTHENTICATION_FAILED,
+
RuntimeDatabaseConnectionException.CATEGORY_AUTHORIZATION_FAILED,
RuntimeDatabaseConnectionException.CATEGORY_CONNECTION_TIMEOUT,
RuntimeDatabaseConnectionException.CATEGORY_INVALID_CONFIGURATION,
RuntimeDatabaseConnectionException.CATEGORY_DATABASE_UNAVAILABLE,
@@ -112,6 +113,7 @@ public final class RuntimeStatusHandler implements
MCPResourceHandler<MCPDatabas
return List.of(
createDiagnosticOperatorAction(RuntimeDatabaseConnectionException.CATEGORY_MISSING_JDBC_DRIVER,
"Install the configured runtime database JDBC driver."),
createDiagnosticOperatorAction(RuntimeDatabaseConnectionException.CATEGORY_AUTHENTICATION_FAILED,
"Check runtime database credentials outside MCP."),
+
createDiagnosticOperatorAction(RuntimeDatabaseConnectionException.CATEGORY_AUTHORIZATION_FAILED,
"Check metadata and SQL privileges for the configured runtime database
account."),
createDiagnosticOperatorAction(RuntimeDatabaseConnectionException.CATEGORY_CONNECTION_TIMEOUT,
"Check database reachability and timeout settings."),
createDiagnosticOperatorAction(RuntimeDatabaseConnectionException.CATEGORY_INVALID_CONFIGURATION,
"Fix runtimeDatabases databaseType, driver, or binding configuration."),
createDiagnosticOperatorAction(RuntimeDatabaseConnectionException.CATEGORY_DATABASE_UNAVAILABLE,
"Check database service availability and network access."),
diff --git
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/resource/handler/metadata/MetadataResourceHandler.java
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/resource/handler/metadata/MetadataResourceHandler.java
index 607dbb787bd..7c4c407c5c3 100644
---
a/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/resource/handler/metadata/MetadataResourceHandler.java
+++
b/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/resource/handler/metadata/MetadataResourceHandler.java
@@ -49,6 +49,14 @@ public final class MetadataResourceHandler implements
MCPResourceHandler<MCPData
private static final int LARGE_RESULT_THRESHOLD = 100;
+ private static final String CATEGORY_NO_RUNTIME_DATABASE =
"no_runtime_database";
+
+ private static final String CATEGORY_UNKNOWN_DATABASE = "unknown_database";
+
+ private static final String CATEGORY_NOT_FOUND = "not_found";
+
+ private static final String CATEGORY_EMPTY_SCOPE = "empty_scope";
+
private final String uriTemplate;
private final BiFunction<MCPDatabaseHandlerContext, MCPUriVariables,
List<?>> metadataLoader;
@@ -71,14 +79,14 @@ public final class MetadataResourceHandler implements
MCPResourceHandler<MCPData
Map<String, Object> navigationPayload =
createNavigationPayload(descriptor, uriVariables);
if (isDetailResource(metadata)) {
if (items.isEmpty()) {
- appendEmptyStateGuidance(navigationPayload, metadata,
uriVariables);
+ appendEmptyStateGuidance(navigationPayload, metadata,
databaseContext, uriVariables);
}
return new MCPMapResponse(createDetailPayload(metadata, items,
navigationPayload));
}
List<?> returnedItems = capListItems(items);
appendListSizeMetadata(navigationPayload, items.size(),
returnedItems.size());
if (items.isEmpty()) {
- appendEmptyStateGuidance(navigationPayload, metadata,
uriVariables);
+ appendEmptyStateGuidance(navigationPayload, metadata,
databaseContext, uriVariables);
} else if (isTruncated(items, returnedItems)) {
appendLargeResultGuidance(navigationPayload, metadata,
uriVariables, items.size());
}
@@ -120,30 +128,56 @@ public final class MetadataResourceHandler implements
MCPResourceHandler<MCPData
return result;
}
- private void appendEmptyStateGuidance(final Map<String, Object> payload,
final ShardingSphereMCPResourceMetadata descriptor, final MCPUriVariables
uriVariables) {
- Map<String, Object> emptyState = new LinkedHashMap<>(3, 1F);
+ private void appendEmptyStateGuidance(final Map<String, Object> payload,
final ShardingSphereMCPResourceMetadata descriptor,
+ final MCPDatabaseHandlerContext
databaseContext, final MCPUriVariables uriVariables) {
+ Map<String, Object> emptyState = new LinkedHashMap<>(4, 1F);
String resourceKind = null == descriptor.getObjectScope() ? "metadata"
: descriptor.getObjectScope();
- String recoveryCategory;
- if (isDetailResource(descriptor)) {
+ String recoveryCategory = resolveEmptyStateCategory(descriptor,
databaseContext, uriVariables);
+ if (CATEGORY_NOT_FOUND.equals(recoveryCategory)) {
emptyState.put("state", "not_found");
- emptyState.put("category", "not_found");
- emptyState.put(MCPPayloadFieldNames.REASON, String.format("%s
detail resource was not found for this URI.", resourceKind));
- recoveryCategory = "not_found";
+ emptyState.put("category", recoveryCategory);
} else {
emptyState.put("state", "no_items");
- emptyState.put("category", "empty_scope");
- emptyState.put(MCPPayloadFieldNames.REASON, "No metadata items are
available in this scope.");
- recoveryCategory = "empty_scope";
+ emptyState.put("category", recoveryCategory);
}
+ String reason = createEmptyStateReason(recoveryCategory, resourceKind);
+ emptyState.put(MCPPayloadFieldNames.REASON, reason);
emptyState.put(MCPPayloadFieldNames.RESOURCE_KIND, resourceKind);
payload.put("empty_state", emptyState);
String parentUri =
getResourceHintUri(payload.get(MCPPayloadFieldNames.PARENT_RESOURCE));
payload.put(MCPPayloadFieldNames.RECOVERY,
createRecovery(recoveryCategory, resourceKind, parentUri, uriVariables));
payload.put(MCPPayloadFieldNames.NEXT_ACTIONS, parentUri.isEmpty()
- ? List.of(MCPNextActionUtils.stop("No metadata items are
available in this scope."))
+ ? List.of(MCPNextActionUtils.stop(reason))
: List.of(MCPNextActionUtils.readResource(parentUri, "Read the
parent metadata resource before broadening or correcting the request.")));
}
+ private String resolveEmptyStateCategory(final
ShardingSphereMCPResourceMetadata descriptor, final MCPDatabaseHandlerContext
databaseContext, final MCPUriVariables uriVariables) {
+ if ("shardingsphere://databases".equals(uriTemplate)) {
+ return CATEGORY_NO_RUNTIME_DATABASE;
+ }
+ if (uriVariables.containsVariable("database") &&
!isKnownDatabase(databaseContext, uriVariables.getValue("database"))) {
+ return CATEGORY_UNKNOWN_DATABASE;
+ }
+ return isDetailResource(descriptor) ? CATEGORY_NOT_FOUND :
CATEGORY_EMPTY_SCOPE;
+ }
+
+ private boolean isKnownDatabase(final MCPDatabaseHandlerContext
databaseContext, final String databaseName) {
+ return
Optional.ofNullable(databaseContext.getCapabilityFacade()).flatMap(capabilityFacade
-> capabilityFacade.findDatabaseProfile(databaseName)).isPresent();
+ }
+
+ private String createEmptyStateReason(final String category, final String
resourceKind) {
+ switch (category) {
+ case CATEGORY_NO_RUNTIME_DATABASE:
+ return "No ShardingSphere-Proxy logical database is available
to MCP. Configure runtimeDatabases before reading metadata.";
+ case CATEGORY_UNKNOWN_DATABASE:
+ return "The requested logical database is not visible to MCP.
Check runtimeDatabases and ShardingSphere-Proxy connectivity.";
+ case CATEGORY_NOT_FOUND:
+ return String.format("%s detail resource was not found for
this URI.", resourceKind);
+ default:
+ return "No metadata items are visible in this scope. Check
metadata permissions if objects are expected.";
+ }
+ }
+
private Map<String, Object> createRecovery(final String category, final
String resourceKind, final String parentUri, final MCPUriVariables
uriVariables) {
Map<String, Object> result = new LinkedHashMap<>(6, 1F);
result.put("response_mode", MCPResponseMode.RECOVERY);
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 ff299226ecb..4bf465ac7f6 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
@@ -363,6 +363,17 @@ class MCPErrorConverterTest {
assertThat(((Map<?, ?>) ((List<?>)
actualRecovery.get("next_actions")).get(1)).get("depends_on"), is(List.of(1)));
}
+ @Test
+ void assertConvertRuntimeDatabaseAuthorizationWithRecovery() {
+ Map<String, Object> actual = MCPErrorConverter.convert(new
RuntimeDatabaseConnectionException("logic_db",
+
RuntimeDatabaseConnectionException.CATEGORY_AUTHORIZATION_FAILED, new
SQLException("permission denied"))).toPayload();
+ Map<?, ?> actualRecovery = (Map<?, ?>) actual.get("recovery");
+ assertThat(actualRecovery.get("category"), is("authorization_failed"));
+ assertThat(actualRecovery.get("recovery_category"),
is("unavailable_runtime"));
+ assertThat(actualRecovery.get("database"), is("logic_db"));
+ assertThat(actualRecovery.get("model_action"), is("Check runtime
database account privileges outside MCP, then retry."));
+ }
+
@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/RuntimeStatusHandlerTest.java
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/capability/RuntimeStatusHandlerTest.java
index 38d66935792..6479362dc8b 100644
---
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/capability/RuntimeStatusHandlerTest.java
+++
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/capability/RuntimeStatusHandlerTest.java
@@ -90,14 +90,15 @@ class RuntimeStatusHandlerTest {
List<?> actualSafeCategories = (List<?>)
actualDiagnostics.get("safe_categories");
assertTrue(actualSafeCategories.contains("missing_jdbc_driver"));
assertTrue(actualSafeCategories.contains("authentication_failed"));
+ assertTrue(actualSafeCategories.contains("authorization_failed"));
assertTrue(actualSafeCategories.contains("connection_timeout"));
assertTrue(actualSafeCategories.contains("invalid_configuration"));
assertTrue(actualSafeCategories.contains("database_unavailable"));
assertTrue(actualSafeCategories.contains("connection_failed"));
List<?> actualOperatorNextActions = (List<?>)
actualDiagnostics.get("operator_next_actions");
- assertThat(actualOperatorNextActions.size(), is(6));
- assertThat(((Map<?, ?>)
actualOperatorNextActions.get(3)).get("category"), is("invalid_configuration"));
- assertTrue((Boolean) ((Map<?, ?>)
actualOperatorNextActions.get(3)).get("secret_safe"));
+ assertThat(actualOperatorNextActions.size(), is(7));
+ assertThat(((Map<?, ?>)
actualOperatorNextActions.get(4)).get("category"), is("invalid_configuration"));
+ assertTrue((Boolean) ((Map<?, ?>)
actualOperatorNextActions.get(4)).get("secret_safe"));
}
private void assertRuntimeProtection(final Map<String, Object> payload) {
diff --git
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/metadata/MetadataResourceHandlerTest.java
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/metadata/MetadataResourceHandlerTest.java
index 2312c4553a3..7818ad472d6 100644
---
a/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/metadata/MetadataResourceHandlerTest.java
+++
b/mcp/core/src/test/java/org/apache/shardingsphere/mcp/core/resource/handler/metadata/MetadataResourceHandlerTest.java
@@ -21,18 +21,22 @@ import
org.apache.shardingsphere.mcp.api.protocol.response.MCPResponse;
import org.apache.shardingsphere.mcp.api.resource.MCPUriVariables;
import
org.apache.shardingsphere.mcp.api.resource.descriptor.MCPResourceDescriptor;
import
org.apache.shardingsphere.mcp.support.database.MCPDatabaseHandlerContext;
+import
org.apache.shardingsphere.mcp.support.database.metadata.jdbc.RuntimeDatabaseProfile;
+import
org.apache.shardingsphere.mcp.support.database.spi.MCPFeatureCapabilityFacade;
import
org.apache.shardingsphere.mcp.support.descriptor.MCPDescriptorCatalogIndex;
import org.junit.jupiter.api.Test;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
class MetadataResourceHandlerTest {
@@ -73,6 +77,37 @@ class MetadataResourceHandlerTest {
assertThat(((Map<?, ?>)
actualNextAction.get("arguments")).get("object_types"),
is(List.of("database")));
}
+ @Test
+ void assertHandleRootListResourceWithoutRuntimeDatabase() {
+ MetadataResourceHandler handler = new
MetadataResourceHandler("shardingsphere://databases", (requestContext,
uriVariables) -> List.of());
+ MCPResponse actual =
handler.handle(mock(MCPDatabaseHandlerContext.class), new
MCPUriVariables(Map.of()));
+ Map<?, ?> actualEmptyState = (Map<?, ?>)
actual.toPayload().get("empty_state");
+ assertThat(actualEmptyState.get("category"),
is("no_runtime_database"));
+ assertThat(actualEmptyState.get("reason"), is("No ShardingSphere-Proxy
logical database is available to MCP. Configure runtimeDatabases before reading
metadata."));
+ assertThat(((Map<?, ?>)
actual.toPayload().get("recovery")).get("recovery_category"),
is("no_runtime_database"));
+ }
+
+ @Test
+ void assertHandleListResourceWithUnknownDatabase() {
+ MetadataResourceHandler handler = new
MetadataResourceHandler("shardingsphere://databases/{database}/schemas",
(requestContext, uriVariables) -> List.of());
+ MCPResponse actual =
handler.handle(createDatabaseContext(Optional.empty()), new
MCPUriVariables(Map.of("database", "missing_db")));
+ Map<?, ?> actualEmptyState = (Map<?, ?>)
actual.toPayload().get("empty_state");
+ assertThat(actualEmptyState.get("category"), is("unknown_database"));
+ assertThat(actualEmptyState.get("reason"), is("The requested logical
database is not visible to MCP. Check runtimeDatabases and ShardingSphere-Proxy
connectivity."));
+ assertThat(((Map<?, ?>)
actual.toPayload().get("recovery")).get("recovery_category"),
is("unknown_database"));
+ }
+
+ @Test
+ void assertHandleListResourceWithEmptyScope() {
+ MetadataResourceHandler handler = new
MetadataResourceHandler("shardingsphere://databases/{database}/schemas",
(requestContext, uriVariables) -> List.of());
+ MCPResponse actual =
handler.handle(createDatabaseContext(Optional.of(new
RuntimeDatabaseProfile("logic_db", "MySQL", "8.0"))),
+ new MCPUriVariables(Map.of("database", "logic_db")));
+ Map<?, ?> actualEmptyState = (Map<?, ?>)
actual.toPayload().get("empty_state");
+ assertThat(actualEmptyState.get("category"), is("empty_scope"));
+ assertThat(actualEmptyState.get("reason"), is("No metadata items are
visible in this scope. Check metadata permissions if objects are expected."));
+ assertThat(((Map<?, ?>)
actual.toPayload().get("recovery")).get("recovery_category"),
is("empty_scope"));
+ }
+
@Test
void assertHandleDetailResource() {
MetadataResourceHandler handler = new
MetadataResourceHandler("shardingsphere://databases/{database}",
@@ -99,6 +134,15 @@ class MetadataResourceHandlerTest {
assertThat(((Map<?, ?>) ((List<?>)
actual.toPayload().get("next_actions")).get(0)).get("type"), is("terminal"));
}
+ private MCPDatabaseHandlerContext createDatabaseContext(final
Optional<RuntimeDatabaseProfile> databaseProfile) {
+ MCPFeatureCapabilityFacade capabilityFacade =
mock(MCPFeatureCapabilityFacade.class);
+
when(capabilityFacade.findDatabaseProfile("logic_db")).thenReturn(databaseProfile);
+
when(capabilityFacade.findDatabaseProfile("missing_db")).thenReturn(Optional.empty());
+ MCPDatabaseHandlerContext result =
mock(MCPDatabaseHandlerContext.class);
+ when(result.getCapabilityFacade()).thenReturn(capabilityFacade);
+ return result;
+ }
+
private List<Map<String, String>> createDatabases(final int count) {
List<Map<String, String>> result = new LinkedList<>();
for (int i = 0; i < count; i++) {
diff --git
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcDatabaseProfileLoader.java
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcDatabaseProfileLoader.java
index 0bb33387c71..31b8c58aea6 100644
---
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcDatabaseProfileLoader.java
+++
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcDatabaseProfileLoader.java
@@ -62,7 +62,7 @@ public final class MCPJdbcDatabaseProfileLoader {
* @param runtimeDatabaseConfig runtime database configuration
* @return runtime database profile
* @throws RuntimeDatabaseConnectionException when runtime database
connection or configuration fails
- * @throws IllegalStateException when profile metadata loading fails
+ * @throws RuntimeDatabaseConnectionException when profile metadata
loading fails
*/
public RuntimeDatabaseProfile load(final String databaseName, final
RuntimeDatabaseConfiguration runtimeDatabaseConfig) {
try (Connection connection =
runtimeDatabaseConfig.openConnection(databaseName)) {
@@ -71,7 +71,7 @@ public final class MCPJdbcDatabaseProfileLoader {
String databaseVersion =
Objects.toString(databaseMetaData.getDatabaseProductVersion(), "").trim();
return new RuntimeDatabaseProfile(databaseName, databaseType,
databaseVersion);
} catch (final SQLException ex) {
- throw new IllegalStateException(String.format("Failed to load
database profile for database `%s`.", databaseName), ex);
+ throw
RuntimeDatabaseConnectionException.connectionFailed(databaseName, ex);
}
}
diff --git
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcMetadataLoader.java
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcMetadataLoader.java
index 9761adc8163..41a5f5f5a8e 100644
---
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcMetadataLoader.java
+++
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcMetadataLoader.java
@@ -73,13 +73,13 @@ public final class MCPJdbcMetadataLoader {
* @param runtimeDatabaseConfig runtime database configuration
* @param databaseProfile runtime database profile
* @return database metadata
- * @throws IllegalStateException when metadata loading fails
+ * @throws RuntimeDatabaseConnectionException when metadata loading fails
*/
public MCPDatabaseMetadata load(final String databaseName, final
RuntimeDatabaseConfiguration runtimeDatabaseConfig, final
RuntimeDatabaseProfile databaseProfile) {
try (Connection connection =
runtimeDatabaseConfig.openConnection(databaseName)) {
return loadDatabaseMetadata(databaseName, databaseProfile,
connection, connection.getMetaData());
} catch (final SQLException ex) {
- throw new IllegalStateException(String.format("Failed to load
metadata for database `%s`.", databaseName), ex);
+ throw
RuntimeDatabaseConnectionException.connectionFailed(databaseName, ex);
}
}
diff --git
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/RuntimeDatabaseConfiguration.java
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/RuntimeDatabaseConfiguration.java
index 831c24a5708..b2ce63ba8f9 100644
---
a/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/RuntimeDatabaseConfiguration.java
+++
b/mcp/support/src/main/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/RuntimeDatabaseConfiguration.java
@@ -23,6 +23,7 @@ import lombok.Getter;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
+import java.util.Objects;
import java.util.Properties;
/**
@@ -52,10 +53,10 @@ public final class RuntimeDatabaseConfiguration {
public Connection openConnection(final String databaseName) throws
SQLException {
loadDriver(databaseName);
Properties props = new Properties();
- if (!username.isEmpty()) {
+ if (!Objects.toString(username, "").isEmpty()) {
props.setProperty("user", username);
}
- if (!password.isEmpty()) {
+ if (!Objects.toString(password, "").isEmpty()) {
props.setProperty("password", password);
}
try {
@@ -66,11 +67,8 @@ public final class RuntimeDatabaseConfiguration {
}
private void loadDriver(final String databaseName) {
- if (driverClassName.isEmpty()) {
- return;
- }
try {
- Class.forName(driverClassName);
+ Class.forName(Objects.toString(driverClassName, ""));
} catch (final ClassNotFoundException ex) {
throw
RuntimeDatabaseConnectionException.missingJdbcDriver(databaseName, ex);
}
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 04d16222b07..7d1d209cb60 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
@@ -34,6 +34,8 @@ public final class RuntimeDatabaseConnectionException extends
RuntimeException {
public static final String CATEGORY_AUTHENTICATION_FAILED =
"authentication_failed";
+ public static final String CATEGORY_AUTHORIZATION_FAILED =
"authorization_failed";
+
public static final String CATEGORY_CONNECTION_TIMEOUT =
"connection_timeout";
public static final String CATEGORY_INVALID_CONFIGURATION =
"invalid_configuration";
@@ -93,6 +95,9 @@ public final class RuntimeDatabaseConnectionException extends
RuntimeException {
if (cause instanceof SQLTimeoutException ||
message.contains("timeout") || message.contains("timed out")) {
return CATEGORY_CONNECTION_TIMEOUT;
}
+ if (sqlState.startsWith("42501") || message.contains("permission
denied") || message.contains("insufficient privilege") || message.contains("not
authorized")) {
+ return CATEGORY_AUTHORIZATION_FAILED;
+ }
if (sqlState.startsWith("28") || message.contains("authentication") ||
message.contains("access denied") || message.contains("password")) {
return CATEGORY_AUTHENTICATION_FAILED;
}
diff --git
a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcMetadataLoaderTest.java
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcMetadataLoaderTest.java
index e48730a71f5..dcc1c557d65 100644
---
a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcMetadataLoaderTest.java
+++
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/MCPJdbcMetadataLoaderTest.java
@@ -96,7 +96,7 @@ class MCPJdbcMetadataLoaderTest {
Driver mockDriver = new MockDriver("jdbc:mock:no-schema",
createConnectionWithoutSchema("MySQL"));
DriverManager.registerDriver(mockDriver);
try {
- LoadedMetadataCatalog actual = load(Map.of("logic_db", new
RuntimeDatabaseConfiguration("MySQL", "jdbc:mock:no-schema", "", "", "")));
+ LoadedMetadataCatalog actual = load(Map.of("logic_db", new
RuntimeDatabaseConfiguration("MySQL", "jdbc:mock:no-schema", "", "",
MockDriver.class.getName())));
MCPDatabaseMetadata databaseMetadata =
actual.findMetadata("logic_db").orElseThrow();
assertThat(databaseMetadata.getSchemas().size(), is(1));
assertThat(databaseMetadata.getSchemas().get(0).getSchema(),
is("logic_db"));
@@ -186,9 +186,9 @@ class MCPJdbcMetadataLoaderTest {
SQLException expected = new SQLException("connection failed");
when(runtimeDatabaseConfiguration.openConnection("logic_db")).thenThrow(expected);
MCPJdbcMetadataLoader metadataLoader = new MCPJdbcMetadataLoader();
- IllegalStateException actual =
assertThrows(IllegalStateException.class,
+ RuntimeDatabaseConnectionException actual =
assertThrows(RuntimeDatabaseConnectionException.class,
() -> metadataLoader.load("logic_db",
runtimeDatabaseConfiguration, new RuntimeDatabaseProfile("logic_db",
"PostgreSQL", "")));
- assertThat(actual.getMessage(), is("Failed to load metadata for
database `logic_db`."));
+ assertThat(actual.getMessage(), is("Runtime database `logic_db`
connection failed: connection_failed."));
assertThat(actual.getCause(), is(expected));
}
@@ -305,9 +305,9 @@ class MCPJdbcMetadataLoaderTest {
Driver mockDriver = new MockDriver("jdbc:mock:failed-sequence-query",
createConnectionWithFailedSequenceMetadataQuery());
DriverManager.registerDriver(mockDriver);
try {
- IllegalStateException actual =
assertThrows(IllegalStateException.class,
- () -> load(Map.of("logic_db", new
RuntimeDatabaseConfiguration("PostgreSQL", "jdbc:mock:failed-sequence-query",
"", "", ""))));
- assertThat(actual.getMessage(), is("Failed to load metadata for
database `logic_db`."));
+ RuntimeDatabaseConnectionException actual =
assertThrows(RuntimeDatabaseConnectionException.class,
+ () -> load(Map.of("logic_db", new
RuntimeDatabaseConfiguration("PostgreSQL", "jdbc:mock:failed-sequence-query",
"", "", MockDriver.class.getName()))));
+ assertThat(actual.getMessage(), is("Runtime database `logic_db`
connection failed: connection_failed."));
assertThat(actual.getCause().getMessage(), is("sequence metadata
query failed"));
} finally {
DriverManager.deregisterDriver(mockDriver);
@@ -353,7 +353,7 @@ class MCPJdbcMetadataLoaderTest {
Driver mockDriver = new MockDriver(jdbcUrl,
createConnectionWithSequenceMetadata(databaseType, sequenceSchema,
sequenceName, sequenceQuery));
DriverManager.registerDriver(mockDriver);
try {
- LoadedMetadataCatalog actual = load(Map.of("logic_db", new
RuntimeDatabaseConfiguration(databaseType, jdbcUrl, "", "", "")));
+ LoadedMetadataCatalog actual = load(Map.of("logic_db", new
RuntimeDatabaseConfiguration(databaseType, jdbcUrl, "", "",
MockDriver.class.getName())));
assertTrue(containsMetadata(actual.findMetadata("logic_db").orElseThrow(),
SupportedMCPMetadataObjectType.SEQUENCE, sequenceName));
} finally {
DriverManager.deregisterDriver(mockDriver);
diff --git
a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/RuntimeDatabaseConfigurationTest.java
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/RuntimeDatabaseConfigurationTest.java
index e17c9fe3bb1..c1d7f8a12bd 100644
---
a/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/RuntimeDatabaseConfigurationTest.java
+++
b/mcp/support/src/test/java/org/apache/shardingsphere/mcp/support/database/metadata/jdbc/RuntimeDatabaseConfigurationTest.java
@@ -37,9 +37,9 @@ import static org.mockito.Mockito.mock;
class RuntimeDatabaseConfigurationTest {
@Test
- void assertOpenConnectionWithoutDriverClassName() throws SQLException {
+ void assertOpenConnectionWithoutCredentials() throws SQLException {
RecordingDriver.reset();
- try (Connection actual = new RuntimeDatabaseConfiguration("MySQL",
RecordingDriver.JDBC_URL, "", "", "").openConnection("logic_db")) {
+ try (Connection actual = new RuntimeDatabaseConfiguration("MySQL",
RecordingDriver.JDBC_URL, "", "",
RecordingDriver.class.getName()).openConnection("logic_db")) {
assertThat(actual, is(RecordingDriver.CONNECTION));
assertThat(RecordingDriver.lastUrl, is(RecordingDriver.JDBC_URL));
assertTrue(RecordingDriver.lastProperties.isEmpty());
@@ -56,6 +56,15 @@ class RuntimeDatabaseConfigurationTest {
}
}
+ @SuppressWarnings("resource")
+ @Test
+ void assertOpenConnectionWithBlankDriverClassName() {
+ RuntimeDatabaseConnectionException actual =
assertThrows(RuntimeDatabaseConnectionException.class,
+ () -> new RuntimeDatabaseConfiguration("MySQL",
"jdbc:test:missing-driver", "", "", "").openConnection("logic_db"));
+ assertThat(actual.getMessage(), is("Runtime database `logic_db`
connection failed: missing_jdbc_driver."));
+ assertThat(actual.getCategory(), is("missing_jdbc_driver"));
+ }
+
@SuppressWarnings("resource")
@Test
void assertOpenConnectionWithUnavailableDriverClassName() {
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 acd04b8caf1..2f6a7a1aaca 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
@@ -54,6 +54,13 @@ class RuntimeDatabaseConnectionExceptionTest {
assertThat(actual.getCategory(),
is(RuntimeDatabaseConnectionException.CATEGORY_AUTHENTICATION_FAILED));
}
+ @Test
+ void assertConnectionFailedAsAuthorization() {
+ RuntimeDatabaseConnectionException actual =
RuntimeDatabaseConnectionException.connectionFailed("logic_db", new
SQLException("permission denied", "42501"));
+ assertThat(actual.getMessage(), is("Runtime database `logic_db`
connection failed: authorization_failed."));
+ assertThat(actual.getCategory(),
is(RuntimeDatabaseConnectionException.CATEGORY_AUTHORIZATION_FAILED));
+ }
+
@Test
void assertConnectionFailedAsDatabaseUnavailable() {
RuntimeDatabaseConnectionException actual =
RuntimeDatabaseConnectionException.connectionFailed("logic_db", new
SQLException("Connection refused", "08001"));