This is an automated email from the ASF dual-hosted git repository. FreeOnePlus pushed a commit to branch agent/fix-profile-api-probe in repository https://gitbox.apache.org/repos/asf/doris-mcp-server.git
commit 0bc2367b5c78c6b0067d76575158dce4de33d0ba Author: FreeOnePlus <[email protected]> AuthorDate: Sat Aug 1 13:44:53 2026 +0800 fix: probe Query Profile with an owned query --- 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]
