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 7f29b3e  fix: probe Query Profile with an owned query (#196)
7f29b3e is described below

commit 7f29b3edcf4a9fa5514e8b579ec177a0a00f3650
Author: Yijia Su <[email protected]>
AuthorDate: Sat Aug 1 14:12:15 2026 +0800

    fix: probe Query Profile with an owned query (#196)
---
 CHANGELOG.md                                  |  3 +
 doris_mcp_server/tools/capability_detector.py | 87 +++++++++++++++++++++++++--
 test/tools/test_capability_detector.py        | 65 ++++++++++++++++++++
 3 files changed, 149 insertions(+), 6 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index db0de65..f6ce140 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -74,6 +74,9 @@ under **Unreleased** until a new version is selected and 
published.
 - Classified Doris runtime probe errors whose messages explicitly report
   denied access or missing privileges as permission failures, including Doris
   error 1105 responses, instead of exposing a generic probe failure.
+- Replaced the Query Profile capability check that used a synthetic query ID
+  with an owned, bounded profiled query and trace lookup, preventing valid 
Doris
+  Profile APIs from being hidden by a false-negative probe.
 - Added the Apache SkyWalking Eyes release gate, its bounded repository
   configuration, and the missing ASF license headers required for source
   release verification.
diff --git a/doris_mcp_server/tools/capability_detector.py 
b/doris_mcp_server/tools/capability_detector.py
index 15bd85d..b59e1ae 100644
--- a/doris_mcp_server/tools/capability_detector.py
+++ b/doris_mcp_server/tools/capability_detector.py
@@ -21,12 +21,14 @@ from __future__ import annotations
 import asyncio
 import hashlib
 import json
+import uuid
 from collections.abc import Mapping, Sequence
 from dataclasses import dataclass, replace
 from datetime import UTC, datetime, timedelta
 from enum import StrEnum
 from types import MappingProxyType
 from typing import Any
+from urllib.parse import quote
 
 from ..utils.adbc_query_tools import DorisADBCQueryTools
 from ..utils.db import DorisConnection, DorisConnectionManager, 
DorisRouteIdentity
@@ -875,7 +877,36 @@ class DorisCapabilityDetector:
                 reason_code="PROFILE_API_CREDENTIAL_ROUTE_UNAVAILABLE",
                 evidence_sources=("credential_route_policy",),
             )
+        trace_id = uuid.uuid4().hex
         try:
+            route = self.route_identity(auth_context)
+            session_id = f"capability-profile:{route.fingerprint[:16]}"
+            async with (
+                
self._connection_manager.get_connection_context_for_auth_context(
+                    session_id,
+                    auth_context,
+                ) as connection
+            ):
+                try:
+                    await connection.execute(
+                        f'SET session_context="trace_id:{trace_id}"',
+                        auth_context=None,
+                        mask_result=False,
+                    )
+                    await connection.execute(
+                        "SET enable_profile=true",
+                        auth_context=None,
+                        mask_result=False,
+                    )
+                    await connection.execute(
+                        "SELECT 1 AS capability_probe",
+                        auth_context=auth_context,
+                        mask_result=False,
+                        max_rows=1,
+                        max_bytes=1024,
+                    )
+                finally:
+                    connection.is_healthy = False
             config_resolver = getattr(
                 self._connection_manager,
                 "get_database_config_for_auth_context",
@@ -887,15 +918,28 @@ class DorisCapabilityDetector:
                 else database_config_for_request(self._connection_manager)
             )
             client = DorisHTTPClient.from_database_config(db_config)
+            hosts = configured_fe_http_hosts(db_config)
+            query_id = await self._resolve_profile_probe_query_id(
+                client,
+                hosts=hosts,
+                port=db_config.fe_http_port,
+                trace_id=trace_id,
+            )
+            if query_id is None:
+                return CapabilityProbeEvidence(
+                    probe_id="query_profile_api_readable",
+                    status=CapabilityProbeStatus.DEGRADED,
+                    reason_code="PROFILE_API_QUERY_ID_UNAVAILABLE",
+                    evidence_sources=("doris_fe_http", "runtime_probe"),
+                )
             response = await client.get_first_available(
                 role="fe",
-                hosts=configured_fe_http_hosts(db_config),
+                hosts=hosts,
                 port=db_config.fe_http_port,
-                path="/rest/v2/manager/query/query_info",
-                params={
-                    "query_id": ("0000000000000000-0000000000000000"),
-                    "is_all_node": "false",
-                },
+                path=(
+                    "/rest/v2/manager/query/profile/text/"
+                    f"{quote(query_id, safe='')}"
+                ),
                 headers={"Accept": "application/json, text/plain"},
             )
         except DorisHTTPPolicyError:
@@ -927,6 +971,37 @@ class DorisCapabilityDetector:
             evidence_sources=("doris_fe_http", "runtime_probe"),
         )
 
+    async def _resolve_profile_probe_query_id(
+        self,
+        client: DorisHTTPClient,
+        *,
+        hosts: tuple[str, ...],
+        port: int,
+        trace_id: str,
+    ) -> str | None:
+        for delay in (0.0, 0.2, 0.5):
+            if delay:
+                await asyncio.sleep(delay)
+            response = await client.get_first_available(
+                role="fe",
+                hosts=hosts,
+                port=port,
+                path=f"/rest/v2/manager/query/trace_id/{trace_id}",
+                headers={"Accept": "application/json"},
+            )
+            if response.status != 200:
+                continue
+            try:
+                payload = json.loads(response.text())
+            except json.JSONDecodeError:
+                continue
+            if not isinstance(payload, Mapping) or _profile_api_code(payload) 
!= 0:
+                continue
+            query_id = payload.get("data")
+            if isinstance(query_id, str) and query_id.strip():
+                return query_id.strip()
+        return None
+
     async def _probe_lineage_plugin_status(
         self,
         auth_context: Any | None,
diff --git a/test/tools/test_capability_detector.py 
b/test/tools/test_capability_detector.py
index 6c7fe7d..8c07e1d 100644
--- a/test/tools/test_capability_detector.py
+++ b/test/tools/test_capability_detector.py
@@ -782,6 +782,71 @@ def test_profile_api_probe_classification_is_fail_closed(
     assert reason == expected_reason
 
 
[email protected]
+async def test_profile_api_probe_uses_an_owned_profiled_query(
+    monkeypatch: pytest.MonkeyPatch,
+) -> None:
+    connection = _ProbeConnection()
+    manager = _ProbeConnectionManager(connection)
+    database_config = SimpleNamespace(
+        user="reader",
+        password="secret",
+        host="fe-1",
+        hosts=["fe-1"],
+        fe_http_host="fe-1",
+        fe_http_hosts=["fe-1"],
+        fe_http_port=8030,
+    )
+    manager.config.database = database_config
+    manager.selected_database_config = database_config
+    calls: list[dict[str, Any]] = []
+
+    class _HTTPClient:
+        async def get_first_available(self, **kwargs: Any) -> 
DorisHTTPResponse:
+            calls.append(kwargs)
+            if "/trace_id/" in kwargs["path"]:
+                body = b'{"code":0,"data":"query/id"}'
+            else:
+                body = b'{"code":0,"data":"profile"}'
+            return DorisHTTPResponse(
+                status=200,
+                headers={"content-type": "application/json"},
+                body=body,
+                url=f"http://fe-1:8030{kwargs['path']}",
+            )
+
+    monkeypatch.setattr(
+        "doris_mcp_server.tools.capability_detector."
+        "DorisHTTPClient.from_database_config",
+        lambda _config: _HTTPClient(),
+    )
+    detector = DorisCapabilityDetector(manager)  # type: ignore[arg-type]
+    base = await detector.detect_base(
+        None,
+        capability_generation=1,
+        provider_generation="provider.a",
+    )
+
+    query = await detector.detect_domain(base, "doris_query", None)
+
+    profile = query.probe("query_profile_api_readable")
+    assert profile is not None
+    assert profile.status is CapabilityProbeStatus.SUPPORTED
+    assert profile.reason_code == "PROFILE_API_READABLE"
+    assert any(
+        statement.startswith('SET session_context="trace_id:')
+        for statement in connection.statements
+    )
+    assert "SET enable_profile=true" in connection.statements
+    assert connection.is_healthy is False
+    assert len(calls) == 2
+    assert "/query/query_info" not in calls[0]["path"]
+    assert "/trace_id/" in calls[0]["path"]
+    assert calls[1]["path"].endswith("/profile/text/query%2Fid")
+    assert "params" not in calls[0]
+    assert "params" not in calls[1]
+
+
 @pytest.mark.asyncio
 async def test_detector_retains_visible_backend_with_unknown_version() -> None:
     connection = _ProbeConnection()


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

Reply via email to