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 f6e7699 fix: preserve partial resource history access (#198)
f6e7699 is described below
commit f6e7699ceb5db2d07ff4c46b2c15559aa74ed462
Author: Yijia Su <[email protected]>
AuthorDate: Sat Aug 1 15:16:47 2026 +0800
fix: preserve partial resource history access (#198)
---
CHANGELOG.md | 3 +
docs/tool-registry.md | 2 +-
doris_mcp_server/tools/capability_detector.py | 73 ++++++++++++++++++++++++
doris_mcp_server/tools/doris_feature_matrix.py | 17 +++++-
test/tools/test_capability_detector.py | 37 +++++++++++++
test/tools/test_capability_registry.py | 77 ++++++++++++++++++++++++++
test/tools/test_doris_feature_matrix.py | 17 ++++++
7 files changed, 223 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 20641c7..70200b7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -68,6 +68,9 @@ under **Unreleased** until a new version is selected and
published.
- Declared the exact `storage`, `query_volume`, and `user_activity` selectors
for resource-growth analysis, including their recorded evidence sources, so
Hosts reject ambiguous values before dispatching a Doris query.
+- Kept resource-growth analysis callable in degraded mode when only audit-log
+ history or partition-creation history is readable, instead of hiding every
+ selector behind the audit-log capability probe.
- Updated JWT decoding type contracts for PyJWT 2.13 while preserving the
existing signature, claim, audience, issuer, and unsafe-debug validation
behavior.
diff --git a/docs/tool-registry.md b/docs/tool-registry.md
index a11acb3..0b7d47c 100644
--- a/docs/tool-registry.md
+++ b/docs/tool-registry.md
@@ -57,7 +57,7 @@
| `doris_cluster.get_compaction_status` |
`child:doris_cluster:get_compaction_status` |
`child:call:doris_cluster:get_compaction_status` | `compaction_task_tracker`,
`legacy_compaction_summary` |
| `doris_cluster.get_workload_group_status` |
`child:doris_cluster:get_workload_group_status` |
`child:call:doris_cluster:get_workload_group_status` | `workload_group_metrics`
|
| `doris_cluster.get_compute_group_status` |
`child:doris_cluster:get_compute_group_status` |
`child:call:doris_cluster:get_compute_group_status` | `compute_group` |
-| `doris_cluster.analyze_resource_growth` |
`child:doris_cluster:analyze_resource_growth` |
`child:call:doris_cluster:analyze_resource_growth` |
`historical_resource_metrics` |
+| `doris_cluster.analyze_resource_growth` |
`child:doris_cluster:analyze_resource_growth` |
`child:call:doris_cluster:analyze_resource_growth` |
`all_recorded_resource_history`, `audit_resource_history`,
`partition_creation_history` |
| `doris_cluster.get_runtime_capabilities` |
`child:doris_cluster:get_runtime_capabilities` |
`child:call:doris_cluster:get_runtime_capabilities` | `capability_snapshot` |
| `doris_pipeline.get_ingestion_status` |
`child:doris_pipeline:get_ingestion_status` |
`child:call:doris_pipeline:get_ingestion_status` | `load_jobs`,
`continuous_load` |
| `doris_pipeline.diagnose_ingestion` |
`child:doris_pipeline:diagnose_ingestion` |
`child:call:doris_pipeline:diagnose_ingestion` |
`deterministic_ingestion_diagnosis` |
diff --git a/doris_mcp_server/tools/capability_detector.py
b/doris_mcp_server/tools/capability_detector.py
index b59e1ae..191f987 100644
--- a/doris_mcp_server/tools/capability_detector.py
+++ b/doris_mcp_server/tools/capability_detector.py
@@ -237,6 +237,13 @@ _DOMAIN_PROBES: Mapping[str, tuple[tuple[str, tuple[str,
...]], ...]] = {
("SELECT `time` FROM internal.__internal_schema.audit_log LIMIT
1"),
("metrics_history_readable",),
),
+ (
+ (
+ "SELECT CREATE_TIME, DATA_LENGTH, INDEX_LENGTH "
+ "FROM information_schema.partitions LIMIT 1"
+ ),
+ ("resource_storage_history_readable",),
+ ),
),
"doris_pipeline": (
(
@@ -499,6 +506,7 @@ class DorisCapabilityDetector:
)
elif domain_name == "doris_cluster":
probes.update(await
self._safe_probe_cluster_services(auth_context))
+ probes.update(_combine_cluster_evidence_probes(probes))
elif domain_name == "doris_pipeline":
probes.update(_combine_pipeline_evidence_probes(probes))
elif domain_name == "doris_search":
@@ -1836,6 +1844,71 @@ def _combine_query_evidence_probe(
)
+def _combine_cluster_evidence_probes(
+ probes: Mapping[str, CapabilityProbeEvidence],
+) -> dict[str, CapabilityProbeEvidence]:
+ audit = probes.get("metrics_history_readable")
+ storage = probes.get("resource_storage_history_readable")
+ full = (
+ _combine_all_evidence(
+ "resource_history_all_sources_readable",
+ (audit, storage),
+ supported_reason="ALL_RESOURCE_HISTORY_SOURCES_READABLE",
+ )
+ if audit is not None and storage is not None
+ else CapabilityProbeEvidence(
+ probe_id="resource_history_all_sources_readable",
+ status=CapabilityProbeStatus.UNKNOWN,
+ reason_code="CAPABILITY_PROBE_PENDING",
+ evidence_sources=("runtime_probe",),
+ )
+ )
+
+ def partial_source(
+ probe_id: str,
+ source: CapabilityProbeEvidence | None,
+ *,
+ reason_code: str,
+ ) -> CapabilityProbeEvidence:
+ if source is None:
+ return CapabilityProbeEvidence(
+ probe_id=probe_id,
+ status=CapabilityProbeStatus.UNKNOWN,
+ reason_code="CAPABILITY_PROBE_PENDING",
+ evidence_sources=("runtime_probe",),
+ )
+ return CapabilityProbeEvidence(
+ probe_id=probe_id,
+ status=(
+ CapabilityProbeStatus.DEGRADED
+ if source.status is CapabilityProbeStatus.SUPPORTED
+ else source.status
+ ),
+ reason_code=(
+ reason_code
+ if source.status is CapabilityProbeStatus.SUPPORTED
+ else source.reason_code
+ ),
+ evidence_sources=source.evidence_sources or ("runtime_probe",),
+ )
+
+ audit_only = partial_source(
+ "resource_growth_audit_history_readable",
+ audit,
+ reason_code="AUDIT_RESOURCE_HISTORY_ONLY",
+ )
+ storage_only = partial_source(
+ "resource_growth_storage_history_readable",
+ storage,
+ reason_code="PARTITION_CREATION_HISTORY_ONLY",
+ )
+ return {
+ full.probe_id: full,
+ audit_only.probe_id: audit_only,
+ storage_only.probe_id: storage_only,
+ }
+
+
def _combine_pipeline_evidence_probes(
probes: Mapping[str, CapabilityProbeEvidence],
) -> dict[str, CapabilityProbeEvidence]:
diff --git a/doris_mcp_server/tools/doris_feature_matrix.py
b/doris_mcp_server/tools/doris_feature_matrix.py
index 66121ea..585dac2 100644
--- a/doris_mcp_server/tools/doris_feature_matrix.py
+++ b/doris_mcp_server/tools/doris_feature_matrix.py
@@ -1592,11 +1592,24 @@ FEATURE_DEFINITIONS = (
"analyze_resource_growth",
A,
_variant(
- "historical_resource_metrics",
+ "all_recorded_resource_history",
providers=("metrics_history_provider",),
- probes=("metrics_history_readable",),
+ probes=("resource_history_all_sources_readable",),
evidence_quality="recorded",
),
+ _variant(
+ "audit_resource_history",
+ providers=("metrics_history_provider",),
+ probes=("resource_growth_audit_history_readable",),
+ evidence_quality="partial",
+ callable_when_degraded=True,
+ ),
+ _variant(
+ "partition_creation_history",
+ probes=("resource_growth_storage_history_readable",),
+ evidence_quality="partial",
+ callable_when_degraded=True,
+ ),
),
_feature(
"doris_cluster",
diff --git a/test/tools/test_capability_detector.py
b/test/tools/test_capability_detector.py
index 8c07e1d..b474b22 100644
--- a/test/tools/test_capability_detector.py
+++ b/test/tools/test_capability_detector.py
@@ -169,6 +169,7 @@ async def
test_detector_builds_version_vector_and_extends_domains_lazily() -> No
)
query = await detector.detect_domain(base, "doris_query", None)
catalog = await detector.detect_domain(base, "doris_catalog", None)
+ cluster = await detector.detect_domain(base, "doris_cluster", None)
assert base.route.fingerprint == "route-a"
assert base.capability_generation == 3
@@ -211,9 +212,45 @@ async def
test_detector_builds_version_vector_and_extends_domains_lazily() -> No
catalog.probe("table_partition_statistics_readable").status
is CapabilityProbeStatus.SUPPORTED
)
+ assert (
+ cluster.probe("resource_history_all_sources_readable").status
+ is CapabilityProbeStatus.SUPPORTED
+ )
assert connection.statements.count("SELECT @@version_comment;") == 1
[email protected]
+async def test_cluster_history_keeps_storage_fallback_without_audit_access()
-> None:
+ connection = _ProbeConnection()
+ audit_probe = (
+ "SELECT `time` FROM internal.__internal_schema.audit_log LIMIT 1"
+ )
+ connection.failures[audit_probe] = RuntimeError(
+ "Access denied; user lacks SELECT privilege"
+ )
+ manager = _ProbeConnectionManager(connection)
+ detector = DorisCapabilityDetector(manager) # type: ignore[arg-type]
+ base = await detector.detect_base(
+ None,
+ capability_generation=1,
+ provider_generation="provider.cluster",
+ )
+
+ cluster = await detector.detect_domain(base, "doris_cluster", None)
+
+ assert (
+ cluster.probe("metrics_history_readable").status
+ is CapabilityProbeStatus.UNKNOWN
+ )
+ assert (
+ cluster.probe("resource_storage_history_readable").status
+ is CapabilityProbeStatus.SUPPORTED
+ )
+ storage = cluster.probe("resource_growth_storage_history_readable")
+ assert storage.status is CapabilityProbeStatus.DEGRADED
+ assert storage.reason_code == "PARTITION_CREATION_HISTORY_ONLY"
+
+
@pytest.mark.asyncio
async def test_lakehouse_probes_derive_target_sensitive_advanced_facets() ->
None:
connection = _ProbeConnection()
diff --git a/test/tools/test_capability_registry.py
b/test/tools/test_capability_registry.py
index 35bbb87..11d70c3 100644
--- a/test/tools/test_capability_registry.py
+++ b/test/tools/test_capability_registry.py
@@ -415,6 +415,83 @@ def
test_lakehouse_prefers_4_1_variants_and_falls_back_on_4_0() -> None:
assert variant_410.callable is True
[email protected](
+ ("partial_probe", "active_variant"),
+ [
+ (
+ "resource_growth_audit_history_readable",
+ "audit_resource_history",
+ ),
+ (
+ "resource_growth_storage_history_readable",
+ "partition_creation_history",
+ ),
+ ],
+)
+def test_resource_growth_keeps_each_partial_history_source_callable(
+ partial_probe: str,
+ active_variant: str,
+) -> None:
+ evaluator = CapabilityEvaluator(
+ matrix=DORIS_FEATURE_MATRIX,
+ bound_handlers=_BoundHandlers( # type: ignore[arg-type]
+ "doris_cluster.analyze_resource_growth"
+ ),
+ )
+ domain = DORIS_DOMAIN_CATALOG.resolve_domain("doris_cluster")
+ child = DORIS_DOMAIN_CATALOG.resolve_child(
+ "doris_cluster",
+ "analyze_resource_growth",
+ )
+ probes = {
+ "resource_history_all_sources_readable": CapabilityProbeEvidence(
+ probe_id="resource_history_all_sources_readable",
+ status=CapabilityProbeStatus.UNKNOWN,
+ reason_code="PROBE_PERMISSION_DENIED",
+ ),
+ **{
+ probe_id: CapabilityProbeEvidence(
+ probe_id=probe_id,
+ status=(
+ CapabilityProbeStatus.DEGRADED
+ if probe_id == partial_probe
+ else CapabilityProbeStatus.UNKNOWN
+ ),
+ reason_code=(
+ "PARTIAL_RESOURCE_HISTORY_ONLY"
+ if probe_id == partial_probe
+ else "PROBE_PERMISSION_DENIED"
+ ),
+ )
+ for probe_id in (
+ "resource_growth_audit_history_readable",
+ "resource_growth_storage_history_readable",
+ )
+ },
+ }
+ providers = CapabilityProviderRegistry(
+ {
+ "metrics_history_provider": CapabilityProviderEvidence(
+ provider_id="metrics_history_provider",
+ status=CapabilityProbeStatus.SUPPORTED,
+ reason_code="PROVIDER_CONFIGURED",
+ )
+ }
+ ).snapshot()
+
+ availability = evaluator.evaluate(
+ snapshot=_snapshot(probes=probes),
+ providers=providers,
+ domain=domain,
+ child=child,
+ auth_context=None,
+ )
+
+ assert availability.callable is True
+ assert availability.status is AvailabilityStatus.DEGRADED
+ assert availability.active_variant == active_variant
+
+
def test_evaluator_normalizes_system_object_probe_evidence_for_manifest() ->
None:
evaluator = CapabilityEvaluator(
matrix=DORIS_FEATURE_MATRIX,
diff --git a/test/tools/test_doris_feature_matrix.py
b/test/tools/test_doris_feature_matrix.py
index d101224..4316b2e 100644
--- a/test/tools/test_doris_feature_matrix.py
+++ b/test/tools/test_doris_feature_matrix.py
@@ -596,6 +596,23 @@ def
test_compaction_child_keeps_a_degraded_legacy_variant() -> None:
assert result.certified is True
+def test_resource_growth_declares_full_and_partial_evidence_variants() -> None:
+ feature = DORIS_FEATURE_MATRIX.get_feature(
+ "doris_cluster",
+ "analyze_resource_growth",
+ )
+
+ assert tuple(
+ variant.name for variant in feature.support_contract.variants
+ ) == (
+ "all_recorded_resource_history",
+ "audit_resource_history",
+ "partition_creation_history",
+ )
+ assert feature.support_contract.variants[1].callable_when_degraded is True
+ assert feature.support_contract.variants[2].callable_when_degraded is True
+
+
def test_lineage_native_and_audit_paths_are_both_explicit() -> None:
feature = DORIS_FEATURE_MATRIX.get_feature(
"doris_governance",
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]