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]