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

Reply via email to