This is an automated email from the ASF dual-hosted git repository.

FreeOnePlus pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris-mcp-server.git


The following commit(s) were added to refs/heads/master by this push:
     new fae518e  fix: handle unknown data freshness safely (#98)
fae518e is described below

commit fae518e247d177a49f2d3c2607f4c1d386f88222
Author: Yijia Su <[email protected]>
AuthorDate: Wed Jul 29 17:47:43 2026 +0800

    fix: handle unknown data freshness safely (#98)
    
    Co-authored-by: FreeOnePlus <[email protected]>
---
 MCP-2026-07-28-DEVELOPMENT-LEDGER.md            | 37 ++++++++++++-
 doris_mcp_server/tools/tools_manager.py         |  2 +
 doris_mcp_server/utils/data_governance_tools.py | 18 ++++---
 test/protocol/stdio_capability_server.py        | 43 ++++++++++++++-
 test/protocol/test_mcp_v2_protocol.py           | 70 +++++++++++++++++++++++++
 test/tools/test_tools_operation_guard.py        | 40 ++++++++++++++
 6 files changed, 201 insertions(+), 9 deletions(-)

diff --git a/MCP-2026-07-28-DEVELOPMENT-LEDGER.md 
b/MCP-2026-07-28-DEVELOPMENT-LEDGER.md
index 005bcb3..19229d3 100644
--- a/MCP-2026-07-28-DEVELOPMENT-LEDGER.md
+++ b/MCP-2026-07-28-DEVELOPMENT-LEDGER.md
@@ -139,7 +139,7 @@
 | `CORE-008` | P2 | 大结果边界 | 无 | 行数、字节数、超时和取消可配置且有硬上限 | `BACKLOG` |
 | `CORE-009` | P2 | manager 模块职责拆分 | CORE-005 | 不改变行为前提下缩小超大文件,模块边界有测试 | 
`BACKLOG` |
 | `CORE-010` | P1 | 修复 SQL profile 分析未绑定 `auth_context` | 无 | 真实 Doris 
调用不再触发局部变量未赋值;鉴权上下文覆盖测试通过 | `DONE` |
-| `CORE-011` | P1 | 修复数据新鲜度空阈值比较 | 无 | 阈值缺失或为 `None` 时返回类型化错误/默认值,不抛 
`TypeError` | `READY` |
+| `CORE-011` | P1 | 修复数据新鲜度空阈值比较 | 无 | 阈值缺失或为 `None` 时返回类型化错误/默认值,不抛 
`TypeError` | `DONE` |
 | `COMPAT-001` | P1 | Doris 4.0 元数据字段兼容 | 无 | Doris 4.0.5 的角色/权限查询不再依赖不存在的 
`Default_role` 字段 | `READY` |
 | `COMPAT-002` | P2 | FE/BE HTTP 端点独立配置 | SEC-018 | SQL、FE HTTP、BE HTTP 
可分别配置主机/端口并通过代理/隧道环境测试 | `BACKLOG` |
 
@@ -477,13 +477,46 @@ recovery result: org_tenant=47040
 
 连接通过既有 SSH key 与临时 SQL/FE HTTP 隧道完成;凭据未写入仓库、测试或台账,探针结束后 MCP 服务与隧道均已关闭。
 
+提交与评审回执:
+
+- commit:`e2e90e0 fix: bind auth context for SQL profiles`
+- Draft 
PR:[apache/doris-mcp-server#97](https://github.com/apache/doris-mcp-server/pull/97)
+
+### CORE-011
+
+数据新鲜度工具现在把缺省或显式为 `None` 的 `freshness_threshold_hours` 统一归一为 24 
小时,并在业务实现入口再次兜底。无法从 Doris 推断更新时间时,`staleness_hours=None` 保持为明确的 `unknown` 
状态,不再进入数值比较;只有真实数字且超过 72 小时时才生成“严重陈旧”数据流问题。
+
+自动化验证:
+
+- 路由层:显式 `None` 向业务层传递默认值 24;
+- 分析层:`unknown + staleness_hours=None` 返回空问题列表,不抛 `TypeError`;
+- Streamable HTTP:缺省阈值、未知更新时间返回结构化结果;
+- 真实子进程 STDIO modern/legacy:同一未知状态和默认阈值契约;
+- 完整 pytest:`351 passed / 57 skipped / 0 failed / 249 warnings`;
+- `uv lock --check`、新增测试完整 Ruff、运行时严重错误规则 Ruff、`compileall`、`uv build` 全部通过。
+
+真实 Doris 验证:
+
+```text
+environment: 192.168.31.63 / hhm_dt_sim
+table: org_tenant
+default threshold: 24
+explicit null threshold: 24
+freshness status: unknown
+recovery result: org_tenant=47040
+```
+
+- HTTP modern/legacy:缺省与显式空阈值均返回 24,未知更新时间不抛异常,随后真实查询成功;
+- STDIO modern/legacy:同样覆盖缺省与显式空阈值,随后真实查询成功;
+- 连接通过既有 SSH key 和临时本地隧道完成;凭据未写入仓库、测试或台账,探针完成后服务与隧道均已关闭。
+
 ## 11. 下一开发批次
 
 批次:`BATCH-02-CONFORMANCE-AND-ERROR-SEMANTICS`
 
 按以下顺序推进:
 
-1. `CORE-011` / `COMPAT-001`:继续修复真实 Doris 已复现缺陷;
+1. `COMPAT-001`:继续修复真实 Doris 已复现缺陷;
 2. `TEST-003`:运行官方 `server-stateless` Conformance;
 3. `TEST-005` / `TEST-012`:补权限不足、超时、故障恢复和工具错误路径;
 4. `PROTO-018` / `DOC-001` / `DOC-002`:版本单一来源和迁移文档;
diff --git a/doris_mcp_server/tools/tools_manager.py 
b/doris_mcp_server/tools/tools_manager.py
index 8d12c31..89e5e30 100644
--- a/doris_mcp_server/tools/tools_manager.py
+++ b/doris_mcp_server/tools/tools_manager.py
@@ -1844,6 +1844,8 @@ No parameters required. Returns connection status, 
configuration, and diagnostic
         """Data freshness monitoring tool routing"""
         table_names = arguments.get("table_names")
         freshness_threshold_hours = arguments.get("freshness_threshold_hours", 
24)
+        if freshness_threshold_hours is None:
+            freshness_threshold_hours = 24
         include_update_patterns = arguments.get("include_update_patterns", 
True)
         catalog_name = arguments.get("catalog_name")
         db_name = arguments.get("db_name")
diff --git a/doris_mcp_server/utils/data_governance_tools.py 
b/doris_mcp_server/utils/data_governance_tools.py
index ebdc11c..4e3238d 100644
--- a/doris_mcp_server/utils/data_governance_tools.py
+++ b/doris_mcp_server/utils/data_governance_tools.py
@@ -162,6 +162,8 @@ class DataGovernanceTools:
         """
         try:
             start_time = time.time()
+            if time_threshold_hours is None:
+                time_threshold_hours = 24
             connection = await self.connection_manager.get_connection("query")
             
             # 1. Get list of tables to monitor
@@ -967,11 +969,15 @@ class DataGovernanceTools:
             })
         
         # Identify particularly stale tables
-        very_stale_tables = [
-            (table_name, info.get("staleness_hours", 0)) 
-            for table_name, info in table_freshness.items() 
-            if info.get("staleness_hours", 0) > 72  # More than 3 days
-        ]
+        very_stale_tables = []
+        for table_name, info in table_freshness.items():
+            staleness_hours = info.get("staleness_hours")
+            if (
+                isinstance(staleness_hours, (int, float))
+                and not isinstance(staleness_hours, bool)
+                and staleness_hours > 72
+            ):
+                very_stale_tables.append((table_name, staleness_hours))
         
         if very_stale_tables:
             issues.append({
@@ -1019,4 +1025,4 @@ class DataGovernanceTools:
                     "timestamp": datetime.now().isoformat()
                 })
         
-        return alerts 
\ No newline at end of file
+        return alerts
diff --git a/test/protocol/stdio_capability_server.py 
b/test/protocol/stdio_capability_server.py
index ce15db8..0991a46 100644
--- a/test/protocol/stdio_capability_server.py
+++ b/test/protocol/stdio_capability_server.py
@@ -34,7 +34,9 @@ from mcp.types import (
 )
 
 from doris_mcp_server.protocol import create_doris_mcp_server
+from doris_mcp_server.tools.tools_manager import DorisToolsManager
 from doris_mcp_server.utils.analysis_tools import SQLAnalyzer
+from doris_mcp_server.utils.data_governance_tools import DataGovernanceTools
 from doris_mcp_server.utils.db import QueryResult
 
 REQUIRED_EXTENSION = "io.apache.doris/read"
@@ -88,9 +90,30 @@ class ProfileAnalyzer(SQLAnalyzer):
         return {"profile": "ok", "query_id": query_id}
 
 
+class FreshnessGovernanceTools(DataGovernanceTools):
+    async def _analyze_table_freshness(
+        self,
+        connection,
+        table_name: str,
+        threshold_hours: int,
+    ) -> dict:
+        return {
+            "last_update": None,
+            "staleness_hours": None,
+            "freshness_score": 0.0,
+            "status": "unknown",
+            "method_used": "none",
+        }
+
+
 class OneToolManager:
     def __init__(self) -> None:
-        self.profile_analyzer = ProfileAnalyzer(ProfileConnectionManager())
+        connection_manager = ProfileConnectionManager()
+        self.profile_analyzer = ProfileAnalyzer(connection_manager)
+        self.freshness_router = object.__new__(DorisToolsManager)
+        self.freshness_router.data_governance_tools = FreshnessGovernanceTools(
+            connection_manager
+        )
 
     async def list_tools(self) -> list[Tool]:
         return [
@@ -111,6 +134,20 @@ class OneToolManager:
                     "required": ["sql"],
                 },
             ),
+            Tool(
+                name="monitor_data_freshness",
+                description="Exercise unknown freshness values with the 
default threshold.",
+                input_schema={
+                    "type": "object",
+                    "properties": {
+                        "table_names": {
+                            "type": "array",
+                            "items": {"type": "string"},
+                        },
+                        "freshness_threshold_hours": {"type": "integer"},
+                    },
+                },
+            ),
         ]
 
     async def call_tool(self, name: str, arguments: dict) -> str:
@@ -121,6 +158,10 @@ class OneToolManager:
                     db_name=arguments.get("db_name"),
                 )
             )
+        if name == "monitor_data_freshness":
+            return json.dumps(
+                await 
self.freshness_router._monitor_data_freshness_tool(arguments)
+            )
         return "{}"
 
 
diff --git a/test/protocol/test_mcp_v2_protocol.py 
b/test/protocol/test_mcp_v2_protocol.py
index 7741877..624a419 100644
--- a/test/protocol/test_mcp_v2_protocol.py
+++ b/test/protocol/test_mcp_v2_protocol.py
@@ -664,6 +664,43 @@ async def 
test_http_sql_profile_without_catalog_uses_production_analyzer_path():
         assert recovered.json()["result"]["isError"] is False
 
 
[email protected]
+async def test_http_unknown_freshness_uses_default_threshold():
+    app = create_test_server(
+        tools_manager=ProfileToolManager(),
+    ).streamable_http_app(
+        json_response=True,
+        stateless_http=True,
+        host="127.0.0.1",
+        transport_security=create_transport_security("127.0.0.1"),
+    )
+
+    async with (
+        app.router.lifespan_context(app),
+        httpx2.ASGITransport(app) as transport,
+        httpx2.AsyncClient(
+            transport=transport,
+            base_url="http://127.0.0.1:3000";,
+        ) as client,
+    ):
+        result = await client.post(
+            "/mcp",
+            json=modern_tool_request(
+                1,
+                "monitor_data_freshness",
+                {"table_names": ["org_tenant"]},
+            ),
+            headers=modern_tool_headers("monitor_data_freshness"),
+        )
+
+        assert result.status_code == 200
+        payload = result.json()["result"]["structuredContent"]
+        assert result.json()["result"]["isError"] is False
+        assert "error" not in payload
+        assert payload["monitoring_scope"]["time_threshold_hours"] == 24
+        assert payload["table_freshness"]["org_tenant"]["status"] == "unknown"
+
+
 @pytest.mark.asyncio
 async def test_stdio_validates_capabilities_versions_and_process_survival():
     server_script = Path(__file__).with_name("stdio_capability_server.py")
@@ -713,12 +750,14 @@ async def 
test_stdio_validates_capabilities_versions_and_process_survival():
         assert [tool.name for tool in (await capable.list_tools()).tools] == [
             "echo",
             "get_sql_profile",
+            "monitor_data_freshness",
         ]
 
     async with Client(stdio_client(server_params), mode="legacy") as legacy:
         assert [tool.name for tool in (await legacy.list_tools()).tools] == [
             "echo",
             "get_sql_profile",
+            "monitor_data_freshness",
         ]
         legacy_error = await legacy.read_resource("doris://table/missing")
         assert json.loads(legacy_error.contents[0].text)["error_code"] == 
"RESOURCE_NOT_FOUND"
@@ -755,6 +794,37 @@ async def 
test_stdio_sql_profile_without_catalog_uses_production_analyzer_path()
         assert json.loads(result.content[0].text)["success"] is True
 
 
[email protected]
+async def test_stdio_unknown_freshness_uses_default_threshold():
+    server_script = Path(__file__).with_name("stdio_capability_server.py")
+    server_params = StdioServerParameters(
+        command=sys.executable,
+        args=[str(server_script)],
+    )
+
+    async with Client(
+        stdio_client(server_params),
+        extensions=[advertise(REQUIRED_EXTENSION)],
+    ) as modern:
+        result = await modern.call_tool(
+            "monitor_data_freshness",
+            {"table_names": ["org_tenant"]},
+        )
+        payload = result.structured_content
+        assert result.is_error is False
+        assert "error" not in payload
+        assert payload["monitoring_scope"]["time_threshold_hours"] == 24
+
+    async with Client(stdio_client(server_params), mode="legacy") as legacy:
+        result = await legacy.call_tool(
+            "monitor_data_freshness",
+            {"table_names": ["org_tenant"]},
+        )
+        payload = json.loads(result.content[0].text)
+        assert "error" not in payload
+        assert payload["monitoring_scope"]["time_threshold_hours"] == 24
+
+
 @pytest.mark.asyncio
 async def test_stdio_prompt_errors_are_typed_and_process_survives():
     server_script = Path(__file__).with_name("stdio_capability_server.py")
diff --git a/test/tools/test_tools_operation_guard.py 
b/test/tools/test_tools_operation_guard.py
index 8876a6f..b7dd71d 100644
--- a/test/tools/test_tools_operation_guard.py
+++ b/test/tools/test_tools_operation_guard.py
@@ -12,6 +12,7 @@ from doris_mcp_server.auth.operation_policy import (
 from doris_mcp_server.protocol import create_doris_mcp_server
 from doris_mcp_server.tools.tools_manager import DorisToolsManager
 from doris_mcp_server.utils.analysis_tools import SQLAnalyzer
+from doris_mcp_server.utils.data_governance_tools import DataGovernanceTools
 from doris_mcp_server.utils.db import QueryResult
 from doris_mcp_server.utils.query_executor import DorisQueryExecutor
 from doris_mcp_server.utils.schema_extractor import MetadataExtractor
@@ -410,6 +411,45 @@ async def 
test_sql_profile_binds_auth_context_without_catalog(tmp_path, db_name)
         assert call["doris_user"] == "alice"
 
 
[email protected]
+async def test_data_freshness_none_threshold_uses_default():
+    manager = object.__new__(DorisToolsManager)
+    manager.data_governance_tools = SimpleNamespace(
+        monitor_data_freshness=AsyncMock(return_value={"ok": True})
+    )
+
+    result = await manager._monitor_data_freshness_tool(
+        {
+            "table_names": ["orders"],
+            "freshness_threshold_hours": None,
+        }
+    )
+
+    assert result == {"ok": True}
+    
manager.data_governance_tools.monitor_data_freshness.assert_awaited_once_with(
+        tables=["orders"],
+        time_threshold_hours=24,
+        catalog_name=None,
+        db_name=None,
+    )
+
+
[email protected]
+async def test_unknown_data_freshness_is_not_compared_as_a_number():
+    governance = DataGovernanceTools(SimpleNamespace())
+
+    issues = await governance._identify_data_flow_issues(
+        {
+            "orders": {
+                "status": "unknown",
+                "staleness_hours": None,
+            }
+        }
+    )
+
+    assert issues == []
+
+
 @pytest.mark.asyncio
 async def 
test_doris_oauth_list_tools_uses_configured_default_scope_visibility(tmp_path):
     manager, _connection_manager = _real_tool_manager_for_routing(tmp_path)


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to