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 d85a3a1  feat: add bounded hierarchical domain discovery (#168)
d85a3a1 is described below

commit d85a3a12d0604353ceab90cabccc6549f14c96eb
Author: Yijia Su <[email protected]>
AuthorDate: Fri Jul 31 04:05:13 2026 +0800

    feat: add bounded hierarchical domain discovery (#168)
---
 README.md                                 |  26 +-
 docs/tool-registry.md                     |  45 +--
 doris_mcp_server/auth/operation_policy.py |  14 +-
 doris_mcp_server/tools/domain_manifest.py | 580 ++++++++++++++++++++++++++++++
 doris_mcp_server/tools/domain_models.py   |  74 +++-
 doris_mcp_server/tools/tool_registry.py   |  20 +-
 doris_mcp_server/tools/tools_manager.py   |  28 +-
 test/protocol/test_mcp_v2_protocol.py     |  65 ++--
 test/security/test_auth_cross_matrix.py   |  61 ++--
 test/security/test_operation_policy.py    |  37 ++
 test/tools/test_custom_tool_provider.py   |  48 +--
 test/tools/test_domain_manifest.py        | 535 +++++++++++++++++++++++++++
 test/tools/test_domain_models.py          |  39 +-
 test/tools/test_tool_registry.py          |  14 +-
 test/tools/test_tools_manager.py          |   9 +-
 test/tools/test_tools_operation_guard.py  |  19 +-
 16 files changed, 1446 insertions(+), 168 deletions(-)

diff --git a/README.md b/README.md
index 4629b3a..fd9ea39 100644
--- a/README.md
+++ b/README.md
@@ -388,12 +388,17 @@ cp .env.example .env
 
 ### Available MCP Tools
 
-The canonical catalog is generated from the same `ToolDefinitionRegistry` used
-for MCP JSON Schemas, execution dispatch, authorization policy, and audit
-metadata. See [docs/tool-registry.md](docs/tool-registry.md). The checked-in
-catalog is verified against the runtime registry by the test suite.
-
-**Doris-backed OAuth note:** The generated catalog describes global server 
capabilities. Doris-backed OAuth uses configuration gates for its operation 
surface. MCP resources are available with resource metadata caching disabled. 
Reviewed metadata tools are callable when `DORIS_OAUTH_DB_TOOLS_ENABLED=true`; 
`exec_query` and `get_sql_explain` are callable when their Doris OAuth 
query/explain gates are enabled. These MySQL-channel operations run through the 
logged-in Doris user pool, so Dor [...]
+MCP `tools/list` exposes eight stable read-only domain tools. Call a domain
+with an empty object to progressively discover its exact authorized child
+tools, schemas, version support, availability, evidence, and risk annotations.
+See [docs/tool-registry.md](docs/tool-registry.md). The checked-in catalog is
+generated from the same validated domain definitions used at runtime.
+
+`tool:list` authorizes domain-manifest discovery for OAuth sessions; it does
+not authorize child execution. Children without discovery permission are
+omitted, while authorized but unavailable children remain visible with
+`callable=false`. Doris RBAC remains the final data authorization backend for
+all Doris object access.
 
 ### 4. Run the Service
 
@@ -1594,11 +1599,10 @@ Tool(
 )
 ```
 
-The registry derives the execution handler, safe audit fields, and generated
-documentation from that definition. Add the name to exactly one policy class:
-metadata, query, explain, or restricted. Run the registry tests and refresh
-`docs/tool-registry.md` from `ToolDefinitionRegistry.render_markdown()`; the
-test suite rejects documentation drift.
+Custom providers are internal capability sources and do not create additional
+top-level MCP tools. Integrate each provider capability into one formal domain
+child with a support contract, authorization policy, input/output schemas, and
+a deterministic handler binding. The test suite rejects public catalog drift.
 
 ### 4. Advanced Features
 
diff --git a/docs/tool-registry.md b/docs/tool-registry.md
index 3d3e09a..a005484 100644
--- a/docs/tool-registry.md
+++ b/docs/tool-registry.md
@@ -1,33 +1,18 @@
-# Doris MCP Tool Registry
+# Doris MCP Domain Tool Registry
 
-<!-- Generated from ToolDefinitionRegistry; do not edit by hand. -->
+<!-- Generated from DomainManifestService; do not edit by hand. -->
 
-| Tool | Policy | Risk | Handler | Audit event | Parameters |
-|---|---|---|---|---|---|
-| `exec_query` | `query` | `query` | `_exec_query_tool` | 
`mcp.tool.call.exec_query` | `catalog_name`, `db_name`, `max_bytes`, 
`max_rows`, `sql`*, `timeout` |
-| `get_table_schema` | `metadata` | `metadata` | `_get_table_schema_tool` | 
`mcp.tool.call.get_table_schema` | `catalog_name`, `db_name`, `table_name`* |
-| `get_db_table_list` | `metadata` | `metadata` | `_get_db_table_list_tool` | 
`mcp.tool.call.get_db_table_list` | `catalog_name`, `db_name` |
-| `get_db_list` | `metadata` | `metadata` | `_get_db_list_tool` | 
`mcp.tool.call.get_db_list` | `catalog_name` |
-| `get_table_comment` | `metadata` | `metadata` | `_get_table_comment_tool` | 
`mcp.tool.call.get_table_comment` | `catalog_name`, `db_name`, `table_name`* |
-| `get_table_column_comments` | `metadata` | `metadata` | 
`_get_table_column_comments_tool` | `mcp.tool.call.get_table_column_comments` | 
`catalog_name`, `db_name`, `table_name`* |
-| `get_table_indexes` | `metadata` | `metadata` | `_get_table_indexes_tool` | 
`mcp.tool.call.get_table_indexes` | `catalog_name`, `db_name`, `table_name`* |
-| `get_recent_audit_logs` | `restricted` | `high` | 
`_get_recent_audit_logs_tool` | `mcp.tool.call.get_recent_audit_logs` | `days`, 
`limit` |
-| `get_catalog_list` | `metadata` | `metadata` | `_get_catalog_list_tool` | 
`mcp.tool.call.get_catalog_list` | `random_string`* |
-| `get_sql_explain` | `explain` | `explain` | `_get_sql_explain_tool` | 
`mcp.tool.call.get_sql_explain` | `catalog_name`, `db_name`, `sql`*, `verbose` |
-| `get_sql_profile` | `restricted` | `high` | `_get_sql_profile_tool` | 
`mcp.tool.call.get_sql_profile` | `catalog_name`, `db_name`, `sql`*, `timeout` |
-| `get_table_data_size` | `restricted` | `high` | `_get_table_data_size_tool` 
| `mcp.tool.call.get_table_data_size` | `db_name`, `single_replica`, 
`table_name` |
-| `get_monitoring_metrics` | `restricted` | `high` | 
`_get_monitoring_metrics_tool` | `mcp.tool.call.get_monitoring_metrics` | 
`content_type`, `include_raw_metrics`, `monitor_type`, `priority`, `role` |
-| `get_memory_stats` | `restricted` | `high` | `_get_memory_stats_tool` | 
`mcp.tool.call.get_memory_stats` | `data_type`, `include_details`, 
`time_range`, `tracker_names`, `tracker_type` |
-| `get_table_basic_info` | `restricted` | `high` | 
`_get_table_basic_info_tool` | `mcp.tool.call.get_table_basic_info` | 
`catalog_name`, `db_name`, `table_name`* |
-| `analyze_columns` | `restricted` | `high` | `_analyze_columns_tool` | 
`mcp.tool.call.analyze_columns` | `analysis_types`, `catalog_name`, `columns`*, 
`db_name`, `detailed_response`, `sample_size`, `table_name`* |
-| `analyze_table_storage` | `restricted` | `high` | 
`_analyze_table_storage_tool` | `mcp.tool.call.analyze_table_storage` | 
`catalog_name`, `db_name`, `detailed_response`, `table_name`* |
-| `trace_column_lineage` | `restricted` | `high` | 
`_trace_column_lineage_tool` | `mcp.tool.call.trace_column_lineage` | 
`analysis_depth`, `catalog_name`, `include_transformations`, `target_columns`* |
-| `monitor_data_freshness` | `restricted` | `high` | 
`_monitor_data_freshness_tool` | `mcp.tool.call.monitor_data_freshness` | 
`catalog_name`, `db_name`, `freshness_threshold_hours`, 
`include_update_patterns`, `table_names` |
-| `analyze_data_access_patterns` | `restricted` | `high` | 
`_analyze_data_access_patterns_tool` | 
`mcp.tool.call.analyze_data_access_patterns` | `days`, `include_system_users`, 
`min_query_threshold` |
-| `analyze_data_flow_dependencies` | `restricted` | `high` | 
`_analyze_data_flow_dependencies_tool` | 
`mcp.tool.call.analyze_data_flow_dependencies` | `analysis_depth`, 
`catalog_name`, `db_name`, `include_views`, `target_table` |
-| `analyze_slow_queries_topn` | `restricted` | `high` | 
`_analyze_slow_queries_topn_tool` | `mcp.tool.call.analyze_slow_queries_topn` | 
`days`, `include_patterns`, `min_execution_time_ms`, `top_n` |
-| `analyze_resource_growth_curves` | `restricted` | `high` | 
`_analyze_resource_growth_curves_tool` | 
`mcp.tool.call.analyze_resource_growth_curves` | `days`, `detailed_response`, 
`include_predictions`, `resource_types` |
-| `exec_adbc_query` | `restricted` | `high` | `_exec_adbc_query_tool` | 
`mcp.tool.call.exec_adbc_query` | `max_bytes`, `max_rows`, `return_format`, 
`sql`*, `timeout` |
-| `get_adbc_connection_info` | `restricted` | `high` | 
`_get_adbc_connection_info_tool` | `mcp.tool.call.get_adbc_connection_info` | 
None |
+| Domain tool | Read-only boundary | Child count | Discovery |
+|---|---|---:|---|
+| `doris_catalog` | Explore Doris catalogs, databases, tables, schemas, 
comments, indexes, key models, storage metadata, and object sizes. | 5 | Call 
with `{}` |
+| `doris_query` | Execute read-only Doris SQL, inspect plans and profiles, 
review slow queries, and use the optional ADBC provider. | 7 | Call with `{}` |
+| `doris_cluster` | Inspect Doris health, nodes, tasks, monitoring, memory, 
cache, compaction, workload, compute groups, and runtime capabilities. | 11 | 
Call with `{}` |
+| `doris_pipeline` | Inspect ingestion, load health, materialized views, data 
freshness, and upstream or downstream dependencies. | 5 | Call with `{}` |
+| `doris_search` | Search Doris data with text, vector, or hybrid retrieval 
and inspect or diagnose search analyzers and indexes. | 4 | Call with `{}` |
+| `doris_governance` | Analyze columns and storage, inspect lineage and access 
evidence, read audit metadata, and review UDF or authentication mappings. | 8 | 
Call with `{}` |
+| `doris_lakehouse` | Inspect external catalogs, lakehouse tables, snapshots, 
partitions, pushdown behavior, and Variant semi-structured columns. | 3 | Call 
with `{}` |
+| `doris_semantic` | Discover validated Ossie models bound to Doris and read 
permission-filtered semantic summaries, context, and mapping status. | 4 | Call 
with `{}` |
 
-Required parameters are marked with `*`. Tool descriptions and JSON Schemas 
are exposed directly by MCP `tools/list` from the same registry entries.
+MCP `tools/list` exposes only these eight stable read-only domains. Call a 
domain with an empty object to receive its authorized, bounded child manifest. 
The manifest contains exact child names, schemas, version support, 
availability, evidence, and risk annotations.
+
+A discoverable child that is not supported by the current runtime remains in 
the manifest with `callable=false`. A child without discovery permission is 
omitted entirely. Internal handlers, custom providers, and pre-1.0 flat names 
are not advertised by `tools/list`.
diff --git a/doris_mcp_server/auth/operation_policy.py 
b/doris_mcp_server/auth/operation_policy.py
index 8bfad49..4f1d515 100644
--- a/doris_mcp_server/auth/operation_policy.py
+++ b/doris_mcp_server/auth/operation_policy.py
@@ -218,6 +218,15 @@ def _tool_policy(tool_name: str, auth_context: Any = None) 
-> OperationPolicy:
             error_code="UNKNOWN_OPERATION",
         )
 
+    if registry_policy.policy_class == "domain":
+        return OperationPolicy(
+            name=f"tool:{tool_name}",
+            required_scope="tool:list",
+            doris_oauth_policy="allow",
+            channel="manifest",
+            risk="metadata",
+        )
+
     if registry_policy.policy_class == "metadata":
         if not _metadata_tools_enabled(auth_context):
             return _denied_tool_policy(
@@ -352,7 +361,8 @@ def _has_scope(auth_context: Any, required_scope: str | 
None) -> bool:
 def _external_oauth_required_scope(operation: str) -> str:
     if operation.startswith("tool:"):
         tool_name = operation.split(":", 1)[1]
-        if policy_definition_for_tool(tool_name) is None:
+        policy = policy_definition_for_tool(tool_name)
+        if policy is None:
             raise OperationAuthorizationError(
                 f"Unknown MCP operation: {operation}",
                 status_code=403,
@@ -360,6 +370,8 @@ def _external_oauth_required_scope(operation: str) -> str:
                 required_scope=f"tool:call:{tool_name}",
                 operation=operation,
             )
+        if policy.policy_class == "domain":
+            return "tool:list"
         return f"tool:call:{tool_name}"
 
     required_scope = EXTERNAL_OAUTH_OPERATION_SCOPES.get(operation)
diff --git a/doris_mcp_server/tools/domain_manifest.py 
b/doris_mcp_server/tools/domain_manifest.py
new file mode 100644
index 0000000..919c51a
--- /dev/null
+++ b/doris_mcp_server/tools/domain_manifest.py
@@ -0,0 +1,580 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Bounded progressive discovery for the eight read-only Doris domains."""
+
+from __future__ import annotations
+
+import hashlib
+import json
+from collections.abc import Callable, Mapping, Sequence
+from datetime import UTC, datetime
+from typing import Any, Protocol
+
+from mcp.types import Tool, ToolAnnotations
+from pydantic import ValidationError
+
+from ..auth.operation_policy import (
+    authorize_operation,
+    filter_tools_for_auth_context,
+)
+from ..utils.logger import get_audit_logger
+from ..utils.security import get_current_auth_context
+from .domain_catalog import DORIS_DOMAIN_CATALOG, DorisDomainCatalog
+from .domain_models import (
+    Availability,
+    AvailabilityStatus,
+    ChildManifestEntry,
+    ChildToolDefinition,
+    DiscoveryEnvelope,
+    DomainDefinition,
+    DomainErrorCode,
+    DomainToolRequest,
+    ErrorEnvelope,
+    ManifestVersionSupport,
+    StandardError,
+)
+
+MAX_CHILD_DESCRIPTION_CHARACTERS = 800
+MAX_CHILD_SCHEMA_BYTES = 4 * 1024
+MAX_SCHEMA_ENUM_VALUES = 32
+MANIFEST_FORMAT_VERSION = "1"
+
+DOMAIN_DISCOVERY_DESCRIPTION_SUFFIX = (
+    " Call with an empty object to discover the exact child tools and schemas "
+    "available in this domain. Then call this same tool with the returned "
+    "child_tool name and arguments."
+)
+
+DOMAIN_TOOL_INPUT_SCHEMA: dict[str, Any] = {
+    "type": "object",
+    "properties": {
+        "child_tool": {
+            "type": "string",
+            "minLength": 1,
+            "description": (
+                "Exact child tool name returned by this domain manifest."
+            ),
+        },
+        "arguments": {
+            "type": "object",
+            "description": (
+                "Arguments validated against the selected child tool schema."
+            ),
+        },
+        "manifest_version": {
+            "type": "string",
+            "minLength": 1,
+            "description": (
+                "Optional manifest version returned by domain discovery."
+            ),
+        },
+    },
+    "additionalProperties": False,
+}
+
+# The complete child contract is validated by DiscoveryEnvelope before it 
reaches
+# the protocol layer. This compact outer schema avoids repeating the manifest
+# implementation schema eight times in tools/list.
+DOMAIN_DISCOVERY_OUTPUT_SCHEMA: dict[str, Any] = {
+    "type": "object",
+    "properties": {
+        "mode": {"type": "string", "const": "manifest"},
+        "domain": {"type": "string"},
+        "manifest_version": {"type": "string"},
+        "generated_at": {"type": "string", "format": "date-time"},
+        "children": {
+            "type": "array",
+            "items": {"type": "object"},
+        },
+    },
+    "required": [
+        "mode",
+        "domain",
+        "manifest_version",
+        "generated_at",
+        "children",
+    ],
+    "additionalProperties": False,
+}
+
+
+class DomainManifestError(ValueError):
+    """Raised when a domain manifest cannot be rendered safely."""
+
+
+class DomainManifestBudgetError(DomainManifestError):
+    """Raised when a public domain manifest exceeds a hard context budget."""
+
+
+class DomainAvailabilityProvider(Protocol):
+    """Resolve the current capability state for one authorized child."""
+
+    async def availability_for(
+        self,
+        domain: DomainDefinition,
+        child: ChildToolDefinition,
+        auth_context: Any | None,
+    ) -> Availability:
+        """Return authoritative availability for a single child."""
+
+
+class PendingCapabilityProvider:
+    """Fail-closed provider used until runtime capability snapshots are 
enabled."""
+
+    async def availability_for(
+        self,
+        domain: DomainDefinition,
+        child: ChildToolDefinition,
+        auth_context: Any | None,
+    ) -> Availability:
+        del domain, child, auth_context
+        return pending_capability_availability()
+
+
+ChildDiscoveryPolicy = Callable[
+    [Any | None, DomainDefinition, ChildToolDefinition],
+    bool,
+]
+
+
+def pending_capability_availability() -> Availability:
+    """Return the explicit pre-probe state without guessing backend support."""
+    return Availability(
+        status=AvailabilityStatus.UNKNOWN,
+        callable=False,
+        reason_code="CAPABILITY_SNAPSHOT_PENDING",
+        evidence_sources=("catalog_contract",),
+    )
+
+
+def authorized_child_discovery(
+    auth_context: Any | None,
+    domain: DomainDefinition,
+    child: ChildToolDefinition,
+) -> bool:
+    """Apply explicit child grants without changing legacy local-session 
access."""
+    del domain
+    if auth_context is None:
+        return True
+
+    controls = {
+        str(value)
+        for collection_name in ("permissions", "oauth_scopes")
+        for value in (getattr(auth_context, collection_name, None) or ())
+    }
+    child_controls = {
+        value
+        for value in controls
+        if value.startswith(("child:call:", "child:discover:"))
+    }
+    if not child_controls:
+        return True
+
+    discover_policy = child.authorization_policy.replace(
+        "child:call:",
+        "child:discover:",
+        1,
+    )
+    return (
+        child.authorization_policy in child_controls
+        or discover_policy in child_controls
+    )
+
+
+class DomainManifestService:
+    """Render top-level domain tools and deterministic authorized manifests."""
+
+    def __init__(
+        self,
+        *,
+        catalog: DorisDomainCatalog = DORIS_DOMAIN_CATALOG,
+        availability_provider: DomainAvailabilityProvider | None = None,
+        discovery_policy: ChildDiscoveryPolicy = authorized_child_discovery,
+        clock: Callable[[], datetime] | None = None,
+    ) -> None:
+        catalog.validate_integrity()
+        self._catalog = catalog
+        self._availability_provider = (
+            availability_provider or PendingCapabilityProvider()
+        )
+        self._discovery_policy = discovery_policy
+        self._clock = clock or (lambda: datetime.now(UTC))
+        self._domain_names = frozenset(
+            domain.name for domain in self._catalog.domains
+        )
+        self._tools = tuple(
+            _build_top_level_tool(domain)
+            for domain in self._catalog.domains
+        )
+        self.validate_default_manifest_budgets()
+
+    @property
+    def domain_names(self) -> frozenset[str]:
+        return self._domain_names
+
+    def handles(self, name: str) -> bool:
+        """Return whether a name is one of the exact top-level domains."""
+        return name in self._domain_names
+
+    def list_tools(self) -> list[Tool]:
+        """Return all stable read-only top-level domain tools."""
+        return list(self._tools)
+
+    def render_markdown(self) -> str:
+        """Render deterministic public documentation from the domain 
catalog."""
+        lines = [
+            "# Doris MCP Domain Tool Registry",
+            "",
+            "<!-- Generated from DomainManifestService; do not edit by hand. 
-->",
+            "",
+            "| Domain tool | Read-only boundary | Child count | Discovery |",
+            "|---|---|---:|---|",
+        ]
+        for domain in self._catalog.domains:
+            lines.append(
+                f"| `{domain.name}` | {domain.description} | "
+                f"{len(domain.children)} | Call with `{{}}` |"
+            )
+        lines.extend(
+            [
+                "",
+                "MCP `tools/list` exposes only these eight stable read-only "
+                "domains. Call a domain with an empty object to receive its "
+                "authorized, bounded child manifest. The manifest contains "
+                "exact child names, schemas, version support, availability, "
+                "evidence, and risk annotations.",
+                "",
+                "A discoverable child that is not supported by the current "
+                "runtime remains in the manifest with `callable=false`. A 
child "
+                "without discovery permission is omitted entirely. Internal "
+                "handlers, custom providers, and pre-1.0 flat names are not "
+                "advertised by `tools/list`.",
+                "",
+            ]
+        )
+        return "\n".join(lines)
+
+    async def call(
+        self,
+        domain_name: str,
+        arguments: Mapping[str, Any],
+        auth_context: Any | None,
+    ) -> DiscoveryEnvelope | ErrorEnvelope:
+        """Handle discovery now and reject child execution until the 
dispatcher."""
+        domain = self._catalog.resolve_domain(domain_name)
+        try:
+            request = DomainToolRequest.model_validate(dict(arguments))
+        except ValidationError:
+            return ErrorEnvelope(
+                mode="error",
+                domain=domain.name,
+                error=StandardError(
+                    code=DomainErrorCode.CHILD_ARGUMENTS_INVALID,
+                    message="Domain tool arguments are invalid.",
+                    retryable=False,
+                    details={"rediscover": True},
+                ),
+            )
+
+        if not request.is_discovery:
+            return ErrorEnvelope(
+                mode="error",
+                domain=domain.name,
+                child_tool=request.child_tool,
+                manifest_version=request.manifest_version,
+                error=StandardError(
+                    code=DomainErrorCode.CHILD_CAPABILITY_UNAVAILABLE,
+                    message=(
+                        "Exact child execution is not enabled by this runtime "
+                        "stage."
+                    ),
+                    retryable=False,
+                    details={"rediscover": False},
+                ),
+            )
+
+        return await self.discover(domain, auth_context)
+
+    async def discover(
+        self,
+        domain: DomainDefinition,
+        auth_context: Any | None,
+    ) -> DiscoveryEnvelope:
+        """Build the current authorized manifest in stable catalog order."""
+        entries: list[ChildManifestEntry] = []
+        for child in domain.children:
+            if not self._discovery_policy(auth_context, domain, child):
+                continue
+            availability = await self._availability_provider.availability_for(
+                domain,
+                child,
+                auth_context,
+            )
+            entries.append(_render_child(child, availability))
+
+        return _build_manifest(
+            domain,
+            tuple(entries),
+            generated_at=self._clock(),
+        )
+
+    def validate_default_manifest_budgets(self) -> None:
+        """Fail startup if the maximum public catalog cannot fit its 
budgets."""
+        generated_at = datetime(2000, 1, 1, tzinfo=UTC)
+        for domain in self._catalog.domains:
+            entries = tuple(
+                _render_child(child, pending_capability_availability())
+                for child in domain.children
+            )
+            _build_manifest(
+                domain,
+                entries,
+                generated_at=generated_at,
+            )
+
+
+class DomainManifestManagerMixin:
+    """Integrate domain discovery without growing the legacy handler 
manager."""
+
+    _domain_manifest_service: DomainManifestService
+
+    @property
+    def domain_manifest_service(self) -> DomainManifestService:
+        """Return the discovery service, including minimal legacy fixtures."""
+        service = getattr(self, "_domain_manifest_service", None)
+        if service is None:
+            service = DomainManifestService()
+            self._domain_manifest_service = service
+        return service
+
+    async def list_tools(self) -> list[Tool]:
+        """List the eight stable read-only domains visible to this 
principal."""
+        return filter_tools_for_auth_context(
+            get_current_auth_context(),
+            self.domain_manifest_service.list_tools(),
+        )
+
+    async def _call_domain_tool(
+        self,
+        name: str,
+        arguments: dict[str, Any],
+    ) -> str:
+        """Return a bounded domain manifest without exposing internal 
handlers."""
+        auth_context = get_current_auth_context()
+        authorize_operation(auth_context, f"tool:{name}")
+        result = await self.domain_manifest_service.call(
+            name,
+            arguments,
+            auth_context,
+        )
+        get_audit_logger().info(
+            "event=mcp.tool.call.%s tool=%s category=domain risk=metadata "
+            "status=%s argument_names=%s",
+            name,
+            name,
+            "error" if result.mode == "error" else "success",
+            ",".join(sorted(arguments)),
+        )
+        return result.to_canonical_json()
+
+
+def _build_top_level_tool(domain: DomainDefinition) -> Tool:
+    annotations = domain.annotations
+    return Tool(
+        name=domain.name,
+        title=domain.title,
+        description=domain.description + DOMAIN_DISCOVERY_DESCRIPTION_SUFFIX,
+        input_schema=DOMAIN_TOOL_INPUT_SCHEMA,
+        output_schema=DOMAIN_DISCOVERY_OUTPUT_SCHEMA,
+        annotations=ToolAnnotations(
+            title=domain.title,
+            read_only_hint=annotations.read_only,
+            destructive_hint=annotations.destructive,
+            idempotent_hint=annotations.idempotent,
+            open_world_hint=annotations.open_world,
+        ),
+    )
+
+
+def _render_child(
+    child: ChildToolDefinition,
+    availability: Availability,
+) -> ChildManifestEntry:
+    description = _dynamic_description(
+        child.canonical_description,
+        availability,
+    )
+    _validate_child_budget(child, description)
+    return ChildManifestEntry(
+        name=child.name,
+        title=child.title,
+        description=description,
+        input_schema=child.input_schema,
+        output_schema=child.output_schema,
+        version_support=ManifestVersionSupport.from_contract(
+            child.support_contract
+        ),
+        availability=availability,
+        annotations=child.annotations,
+    )
+
+
+def _dynamic_description(
+    canonical_description: str,
+    availability: Availability,
+) -> str:
+    context = _availability_context(availability)
+    return (
+        f"[{availability.status.value.upper()} | {context}] "
+        f"{canonical_description}"
+    )
+
+
+def _availability_context(availability: Availability) -> str:
+    context: list[str] = []
+    if availability.detected_versions:
+        component = next(
+            (
+                candidate
+                for candidate in ("master_fe", "fe", "be")
+                if candidate in availability.detected_versions
+            ),
+            sorted(availability.detected_versions)[0],
+        )
+        versions = availability.detected_versions[component]
+        if versions:
+            context.append(f"Doris {versions[0]}")
+    if availability.active_variant is not None:
+        context.append(availability.active_variant)
+    if not context:
+        context.append(
+            availability.reason_code.lower().replace("_", " ")
+        )
+    return " | ".join(context)
+
+
+def _build_manifest(
+    domain: DomainDefinition,
+    entries: tuple[ChildManifestEntry, ...],
+    *,
+    generated_at: datetime,
+) -> DiscoveryEnvelope:
+    version = _manifest_version(domain, entries)
+    envelope = DiscoveryEnvelope(
+        mode="manifest",
+        domain=domain.name,
+        manifest_version=version,
+        generated_at=generated_at,
+        children=entries,
+    )
+    size = len(envelope.to_canonical_json().encode("utf-8"))
+    if size > domain.max_manifest_bytes:
+        raise DomainManifestBudgetError(
+            f"domain {domain.name!r} manifest is {size} bytes; "
+            f"limit is {domain.max_manifest_bytes} bytes"
+        )
+    return envelope
+
+
+def _manifest_version(
+    domain: DomainDefinition,
+    entries: Sequence[ChildManifestEntry],
+) -> str:
+    payload = {
+        "format_version": MANIFEST_FORMAT_VERSION,
+        "domain_contract": domain.to_wire(),
+        "children": [entry.to_wire() for entry in entries],
+    }
+    canonical = json.dumps(
+        payload,
+        ensure_ascii=False,
+        separators=(",", ":"),
+        sort_keys=True,
+    )
+    digest = hashlib.sha256(canonical.encode("utf-8")).hexdigest()[:12]
+    prefix = domain.name.removeprefix("doris_")
+    return f"{prefix}.{digest}"
+
+
+def _validate_child_budget(
+    child: ChildToolDefinition,
+    description: str,
+) -> None:
+    feature_id = child.handler_name.removeprefix("child:")
+    child_wire = child.to_wire()
+    if len(description) > MAX_CHILD_DESCRIPTION_CHARACTERS:
+        raise DomainManifestBudgetError(
+            f"{feature_id} description exceeds "
+            f"{MAX_CHILD_DESCRIPTION_CHARACTERS} characters"
+        )
+    for label, schema in (
+        ("input", child_wire["input_schema"]),
+        ("output", child_wire["output_schema"]),
+    ):
+        size = len(
+            json.dumps(
+                schema,
+                ensure_ascii=False,
+                separators=(",", ":"),
+                sort_keys=True,
+            ).encode("utf-8")
+        )
+        if size > MAX_CHILD_SCHEMA_BYTES:
+            raise DomainManifestBudgetError(
+                f"{feature_id} {label} schema is {size} bytes; "
+                f"limit is {MAX_CHILD_SCHEMA_BYTES} bytes"
+            )
+        _validate_enum_budget(feature_id, label, schema)
+
+
+def _validate_enum_budget(
+    feature_id: str,
+    label: str,
+    value: Any,
+) -> None:
+    if isinstance(value, Mapping):
+        enum = value.get("enum")
+        if isinstance(enum, Sequence) and not isinstance(enum, str | bytes):
+            if len(enum) > MAX_SCHEMA_ENUM_VALUES:
+                raise DomainManifestBudgetError(
+                    f"{feature_id} {label} schema enum exceeds "
+                    f"{MAX_SCHEMA_ENUM_VALUES} values"
+                )
+        for child in value.values():
+            _validate_enum_budget(feature_id, label, child)
+    elif isinstance(value, Sequence) and not isinstance(value, str | bytes):
+        for child in value:
+            _validate_enum_budget(feature_id, label, child)
+
+
+__all__ = [
+    "DOMAIN_DISCOVERY_DESCRIPTION_SUFFIX",
+    "DOMAIN_DISCOVERY_OUTPUT_SCHEMA",
+    "DOMAIN_TOOL_INPUT_SCHEMA",
+    "MAX_CHILD_DESCRIPTION_CHARACTERS",
+    "MAX_CHILD_SCHEMA_BYTES",
+    "MAX_SCHEMA_ENUM_VALUES",
+    "ChildDiscoveryPolicy",
+    "DomainAvailabilityProvider",
+    "DomainManifestBudgetError",
+    "DomainManifestError",
+    "DomainManifestManagerMixin",
+    "DomainManifestService",
+    "PendingCapabilityProvider",
+    "authorized_child_discovery",
+    "pending_capability_availability",
+]
diff --git a/doris_mcp_server/tools/domain_models.py 
b/doris_mcp_server/tools/domain_models.py
index 715b92b..d0fc335 100644
--- a/doris_mcp_server/tools/domain_models.py
+++ b/doris_mcp_server/tools/domain_models.py
@@ -221,6 +221,49 @@ class ChildSupportContract(ContractModel):
         return self
 
 
+class ManifestVersionSupport(ContractModel):
+    """Compact public version ranges projected from an internal support 
contract."""
+
+    rule_id: RuleIdentifier
+    supported_ranges: Annotated[tuple[NonEmptyText, ...], Field(min_length=1)]
+    excluded_ranges: tuple[NonEmptyText, ...] = ()
+    tested_versions: tuple[NonEmptyText, ...] = ()
+
+    @classmethod
+    def from_contract(
+        cls,
+        contract: ChildSupportContract,
+    ) -> ManifestVersionSupport:
+        """Project execution details into the bounded public manifest shape."""
+        supported_ranges = tuple(
+            dict.fromkeys(
+                supported_range
+                for variant in contract.variants
+                for supported_range in variant.supported_ranges
+            )
+        )
+        excluded_ranges = tuple(
+            dict.fromkeys(
+                excluded_range
+                for variant in contract.variants
+                for excluded_range in variant.excluded_ranges
+            )
+        )
+        return cls(
+            rule_id=contract.rule_id,
+            supported_ranges=supported_ranges,
+            excluded_ranges=excluded_ranges,
+            tested_versions=contract.tested_versions,
+        )
+
+    @model_validator(mode="after")
+    def _validate_summary(self) -> Self:
+        _require_unique(self.supported_ranges, "supported_ranges")
+        _require_unique(self.excluded_ranges, "excluded_ranges")
+        _require_unique(self.tested_versions, "tested_versions")
+        return self
+
+
 class CompositeStep(ContractModel):
     """One private deterministic step in a composite child DAG."""
 
@@ -409,7 +452,7 @@ class DomainToolRequest(ContractModel):
 
     @model_validator(mode="after")
     def _validate_request_mode(self) -> Self:
-        if self.child_tool is None and self.arguments:
+        if self.child_tool is None and self.arguments is not None:
             raise ValueError("arguments require child_tool")
         return self
 
@@ -430,7 +473,7 @@ class ChildManifestEntry(ContractModel):
     description: NonEmptyText
     input_schema: JsonObject
     output_schema: JsonObject
-    version_support: ChildSupportContract
+    version_support: ManifestVersionSupport
     availability: Availability
     annotations: ToolContractAnnotations
 
@@ -454,6 +497,10 @@ class ChildManifestEntry(ContractModel):
         _check_object_schema(self.output_schema, "output_schema")
         return self
 
+    def to_wire(self) -> JsonObject:
+        """Omit optional empty metadata from the bounded public manifest."""
+        return _compact_manifest_entry_wire(super().to_wire())
+
 
 class DiscoveryEnvelope(ContractModel):
     """Stable structured result for an empty domain discovery call."""
@@ -474,6 +521,12 @@ class DiscoveryEnvelope(ContractModel):
         )
         return self
 
+    def to_wire(self) -> JsonObject:
+        """Serialize children through their compact public wire projection."""
+        payload = super().to_wire()
+        payload["children"] = [child.to_wire() for child in self.children]
+        return payload
+
 
 class ExecutionMetadata(ContractModel):
     """Bounded metadata for an exact child execution result."""
@@ -548,6 +601,22 @@ class ErrorEnvelope(ContractModel):
     error: StandardError
 
 
+def _compact_manifest_entry_wire(payload: JsonObject) -> JsonObject:
+    """Remove optional empty fields without requiring a newer Pydantic."""
+    for section_name, optional_fields in (
+        ("version_support", ("excluded_ranges", "tested_versions")),
+        (
+            "availability",
+            ("detected_versions", "evidence_sources", "limitations"),
+        ),
+    ):
+        section = cast(dict[str, JsonValue], payload[section_name])
+        for field_name in optional_fields:
+            if not section.get(field_name):
+                section.pop(field_name, None)
+    return payload
+
+
 def _require_unique(values: tuple[str, ...], field_name: str) -> None:
     if len(values) != len(set(values)):
         raise ValueError(f"{field_name} must not contain duplicates")
@@ -624,6 +693,7 @@ __all__ = [
     "ExecutionEnvelope",
     "ExecutionMetadata",
     "JsonObject",
+    "ManifestVersionSupport",
     "StandardError",
     "ToolContractAnnotations",
     "UnavailableBehavior",
diff --git a/doris_mcp_server/tools/tool_registry.py 
b/doris_mcp_server/tools/tool_registry.py
index a8f460d..3e59c69 100644
--- a/doris_mcp_server/tools/tool_registry.py
+++ b/doris_mcp_server/tools/tool_registry.py
@@ -27,7 +27,13 @@ from mcp.types import Tool
 
 from .tool_provider import CustomTool, ToolRateLimit
 
-ToolPolicyClass = Literal["metadata", "query", "explain", "restricted"]
+ToolPolicyClass = Literal[
+    "domain",
+    "metadata",
+    "query",
+    "explain",
+    "restricted",
+]
 ToolHandler = Callable[[dict[str, Any]], Awaitable[dict[str, Any]]]
 
 DORIS_OAUTH_METADATA_TOOL_NAMES = (
@@ -154,6 +160,8 @@ class ToolDefinition:
 
 
 def _policy_for_name(name: str) -> ToolPolicyDefinition:
+    if _is_read_only_domain_tool(name):
+        return ToolPolicyDefinition("domain", "manifest", "metadata")
     if name in DORIS_OAUTH_METADATA_TOOL_SET:
         return ToolPolicyDefinition("metadata", "mysql_metadata", "metadata")
     if name in DORIS_OAUTH_QUERY_TOOL_SET:
@@ -381,10 +389,18 @@ class ToolDefinitionRegistry:
 def policy_definition_for_tool(name: str) -> ToolPolicyDefinition | None:
     """Return policy metadata for a registered canonical tool or legacy 
alias."""
     if (
-        name in DORIS_OAUTH_METADATA_TOOL_SET
+        _is_read_only_domain_tool(name)
+        or name in DORIS_OAUTH_METADATA_TOOL_SET
         or name in DORIS_OAUTH_QUERY_TOOL_SET
         or name in DORIS_OAUTH_EXPLAIN_TOOL_SET
         or name in RESTRICTED_TOOL_NAMES
     ):
         return _policy_for_name(name)
     return None
+
+
+def _is_read_only_domain_tool(name: str) -> bool:
+    """Resolve domain names lazily to keep the catalog as the sole 
authority."""
+    from .doris_feature_matrix import EXPECTED_DOMAIN_CHILDREN
+
+    return name in EXPECTED_DOMAIN_CHILDREN
diff --git a/doris_mcp_server/tools/tools_manager.py 
b/doris_mcp_server/tools/tools_manager.py
index 6f054c1..2776649 100644
--- a/doris_mcp_server/tools/tools_manager.py
+++ b/doris_mcp_server/tools/tools_manager.py
@@ -26,12 +26,9 @@ from collections.abc import Iterable
 from datetime import datetime
 from typing import Any
 
-from mcp.types import Tool
-
 from ..auth.operation_policy import (
     OperationAuthorizationError,
     authorize_operation,
-    filter_tools_for_auth_context,
 )
 from ..result_limits import configured_default_result_rows
 from ..utils.adbc_query_tools import DorisADBCQueryTools
@@ -48,6 +45,11 @@ from ..utils.query_executor import DorisQueryExecutor
 from ..utils.schema_extractor import MetadataExtractor
 from ..utils.security import get_current_auth_context
 from ..utils.security_analytics_tools import SecurityAnalyticsTools
+from .domain_manifest import (
+    DomainAvailabilityProvider,
+    DomainManifestManagerMixin,
+    DomainManifestService,
+)
 from .tool_catalog import build_tool_registry
 from .tool_provider import CustomToolProvider, ToolProviderRuntime
 from .tool_registry import ToolDefinition, ToolDefinitionRegistry
@@ -55,7 +57,7 @@ from .tool_registry import ToolDefinition, 
ToolDefinitionRegistry
 logger = get_logger(__name__)
 
 
-class DorisToolsManager:
+class DorisToolsManager(DomainManifestManagerMixin):
     """Apache Doris Tools Manager"""
 
     def __init__(
@@ -63,6 +65,7 @@ class DorisToolsManager:
         connection_manager: DorisConnectionManager,
         *,
         tool_providers: Iterable[CustomToolProvider] | None = None,
+        domain_availability_provider: DomainAvailabilityProvider | None = None,
     ) -> None:
         self.connection_manager = connection_manager
         config = getattr(connection_manager, "config", None)
@@ -89,6 +92,9 @@ class DorisToolsManager:
         # Initialize ADBC query tools
         self.adbc_query_tools = DorisADBCQueryTools(connection_manager)
         self._tool_registry = self._build_tool_registry()
+        self._domain_manifest_service = DomainManifestService(
+            availability_provider=domain_availability_provider,
+        )
 
         logger.info(
             "DorisToolsManager initialized with business logic processors, 
v0.5.0 "
@@ -154,17 +160,11 @@ class DorisToolsManager:
             self._tool_registry = registry
         return registry
 
-    async def list_tools(self) -> list[Tool]:
-        """List tools visible to the current authorization context."""
-        return filter_tools_for_auth_context(
-            get_current_auth_context(),
-            self.tool_registry.listed_tools(),
-        )
-
     async def call_tool(self, name: str, arguments: dict[str, Any]) -> str:
-        """
-        Call the specified query tool (tool routing and scheduling center)
-        """
+        """Call a domain or an internal migration tool by exact name."""
+        if self.domain_manifest_service.handles(name):
+            return await self._call_domain_tool(name, arguments)
+
         authorize_operation(get_current_auth_context(), f"tool:{name}")
         definition: ToolDefinition | None = None
         start_time = time.time()
diff --git a/test/protocol/test_mcp_v2_protocol.py 
b/test/protocol/test_mcp_v2_protocol.py
index f7941dc..4ce86de 100644
--- a/test/protocol/test_mcp_v2_protocol.py
+++ b/test/protocol/test_mcp_v2_protocol.py
@@ -49,6 +49,9 @@ from doris_mcp_server.protocol import (
     create_doris_mcp_server,
     create_transport_security,
 )
+from doris_mcp_server.tools.doris_feature_matrix import (
+    EXPECTED_DOMAIN_CHILDREN,
+)
 from test.protocol.schema_validation_server import 
create_schema_validation_server
 from test.protocol.stdio_capability_server import OneToolManager as 
ProfileToolManager
 from test.protocol.tool_registry_server import create_registry_test_server
@@ -363,7 +366,7 @@ def modern_tool_headers(name: str) -> dict[str, str]:
 
 
 @pytest.mark.asyncio
-async def 
test_http_uses_production_tool_registry_for_list_validation_and_dispatch():
+async def test_http_uses_production_domain_catalog_for_discovery_validation():
     server = create_registry_test_server()
     app = server.streamable_http_app(
         json_response=True,
@@ -390,14 +393,22 @@ async def 
test_http_uses_production_tool_registry_for_list_validation_and_dispat
             tool["name"]: tool
             for tool in listed.json()["result"]["tools"]
         }
-        assert len(tools) == 25
-        assert tools["exec_query"]["inputSchema"]["required"] == ["sql"]
+        assert set(tools) == set(EXPECTED_DOMAIN_CHILDREN)
+        assert set(tools["doris_catalog"]["inputSchema"]["properties"]) == {
+            "child_tool",
+            "arguments",
+            "manifest_version",
+        }
         assert "get_monitoring_metrics_info" not in tools
 
         invalid = await client.post(
             "/mcp",
-            json=modern_tool_request(2, "exec_query", {}),
-            headers=modern_tool_headers("exec_query"),
+            json=modern_tool_request(
+                2,
+                "doris_catalog",
+                {"unexpected": True},
+            ),
+            headers=modern_tool_headers("doris_catalog"),
         )
         assert invalid.status_code == 400
         assert invalid.json()["error"]["code"] == -32602
@@ -406,18 +417,16 @@ async def 
test_http_uses_production_tool_registry_for_list_validation_and_dispat
             "/mcp",
             json=modern_tool_request(
                 3,
-                "exec_query",
-                {"sql": "SELECT 1"},
+                "doris_catalog",
+                {},
             ),
-            headers=modern_tool_headers("exec_query"),
+            headers=modern_tool_headers("doris_catalog"),
         )
         assert called.status_code == 200
         structured = called.json()["result"]["structuredContent"]
-        assert structured["registry_dispatch"] is True
-        assert structured["sql_length"] == len("SELECT 1")
-        assert structured["_execution_info"]["canonical_tool_name"] == (
-            "exec_query"
-        )
+        assert structured["mode"] == "manifest"
+        assert structured["domain"] == "doris_catalog"
+        assert len(structured["children"]) == 5
 
 
 @pytest.mark.asyncio
@@ -1303,7 +1312,7 @@ async def 
test_true_subprocess_stdio_does_not_advertise_or_serve_subscriptions()
 
 
 @pytest.mark.asyncio
-async def test_true_subprocess_stdio_uses_production_tool_registry():
+async def test_true_subprocess_stdio_uses_production_domain_catalog():
     server_script = Path(__file__).with_name("tool_registry_server.py")
     server_params = StdioServerParameters(
         command=sys.executable,
@@ -1315,28 +1324,34 @@ async def 
test_true_subprocess_stdio_uses_production_tool_registry():
             tool.name: tool
             for tool in (await modern.list_tools(cache_mode="bypass")).tools
         }
-        assert len(tools) == 25
-        assert tools["exec_query"].input_schema["required"] == ["sql"]
+        assert set(tools) == set(EXPECTED_DOMAIN_CHILDREN)
+        assert set(tools["doris_catalog"].input_schema["properties"]) == {
+            "child_tool",
+            "arguments",
+            "manifest_version",
+        }
         assert "get_monitoring_metrics_info" not in tools
 
         with pytest.raises(MCPError) as invalid:
-            await modern.call_tool("exec_query", {})
+            await modern.call_tool(
+                "doris_catalog",
+                {"unexpected": True},
+            )
         assert invalid.value.code == -32602
 
-        called = await modern.call_tool("exec_query", {"sql": "SELECT 1"})
-        assert called.structured_content["registry_dispatch"] is True
-        assert called.structured_content["_execution_info"][
-            "canonical_tool_name"
-        ] == "exec_query"
+        called = await modern.call_tool("doris_catalog", {})
+        assert called.structured_content["mode"] == "manifest"
+        assert called.structured_content["domain"] == "doris_catalog"
 
     async with Client(stdio_client(server_params), mode="legacy") as legacy:
         tools = {
             tool.name: tool
             for tool in (await legacy.list_tools()).tools
         }
-        assert len(tools) == 25
-        called = await legacy.call_tool("exec_query", {"sql": "SELECT 1"})
-        assert called.structured_content["registry_dispatch"] is True
+        assert set(tools) == set(EXPECTED_DOMAIN_CHILDREN)
+        called = await legacy.call_tool("doris_catalog", {})
+        assert called.structured_content["mode"] == "manifest"
+        assert called.structured_content["domain"] == "doris_catalog"
 
 
 @pytest.mark.asyncio
diff --git a/test/security/test_auth_cross_matrix.py 
b/test/security/test_auth_cross_matrix.py
index c851c22..ffd3615 100644
--- a/test/security/test_auth_cross_matrix.py
+++ b/test/security/test_auth_cross_matrix.py
@@ -17,6 +17,9 @@ from doris_mcp_server.auth.operation_policy import (
 )
 from doris_mcp_server.http_transport import DorisMCPHTTPTransport
 from doris_mcp_server.protocol import create_transport_security
+from doris_mcp_server.tools.doris_feature_matrix import (
+    EXPECTED_DOMAIN_CHILDREN,
+)
 from doris_mcp_server.tools.tool_registry import (
     DORIS_OAUTH_EXPLAIN_TOOL_SET,
     DORIS_OAUTH_METADATA_TOOL_SET,
@@ -47,7 +50,15 @@ TOOL_OPERATION_SCOPES = tuple(
     (f"tool:{tool_name}", f"tool:call:{tool_name}")
     for tool_name in ALL_TOOL_NAMES
 )
-ALL_OPERATION_SCOPES = BASE_OPERATION_SCOPES + TOOL_OPERATION_SCOPES
+DOMAIN_OPERATION_SCOPES = tuple(
+    (f"tool:{domain_name}", "tool:list")
+    for domain_name in EXPECTED_DOMAIN_CHILDREN
+)
+ALL_OPERATION_SCOPES = (
+    BASE_OPERATION_SCOPES
+    + TOOL_OPERATION_SCOPES
+    + DOMAIN_OPERATION_SCOPES
+)
 
 
 def _context(auth_method: str, scopes: Iterable[str] = ()) -> AuthContext:
@@ -88,7 +99,6 @@ def _external_oauth_config() -> EffectiveAuthConfig:
         external_oauth_resource="https://mcp.example.test/mcp";,
         external_oauth_scopes=(
             "tool:list",
-            "tool:call:exec_query",
         ),
         external_oauth_required_scopes=("tool:list",),
     )
@@ -260,11 +270,8 @@ def 
test_external_oauth_rejects_unknown_tool_before_dispatch() -> None:
 @pytest.mark.asyncio
 async def 
test_streamable_http_enforces_external_oauth_tool_scope_before_dispatch() -> 
None:
     contexts = {
+        "no-scope": _context("external_oauth"),
         "list-only": _context("external_oauth", ["tool:list"]),
-        "exec-query": _context(
-            "external_oauth",
-            ["tool:list", "tool:call:exec_query"],
-        ),
     }
 
     class SecurityManager:
@@ -299,29 +306,31 @@ async def 
test_streamable_http_enforces_external_oauth_tool_scope_before_dispatc
             headers=_modern_headers("tools/list", token="list-only"),
         )
         assert listed.status_code == 200
-        assert len(listed.json()["result"]["tools"]) == 25
+        assert {
+            tool["name"] for tool in listed.json()["result"]["tools"]
+        } == set(EXPECTED_DOMAIN_CHILDREN)
 
         denied = await client.post(
             "/mcp",
             json=_modern_request(
                 2,
                 "tools/call",
-                name="exec_query",
-                arguments={"sql": "SELECT 1"},
+                name="doris_catalog",
+                arguments={},
             ),
             headers=_modern_headers(
                 "tools/call",
-                name="exec_query",
-                token="list-only",
+                name="doris_catalog",
+                token="no-scope",
             ),
         )
         assert denied.status_code == 403
         assert denied.json()["error"] == "PERMISSION_DENIED"
-        assert denied.json()["required_scope"] == "tool:call:exec_query"
+        assert denied.json()["required_scope"] == "tool:list"
         assert 'error="insufficient_scope"' in denied.headers[
             "www-authenticate"
         ]
-        assert 'scope="tool:call:exec_query"' in denied.headers[
+        assert 'scope="tool:list"' in denied.headers[
             "www-authenticate"
         ]
 
@@ -330,19 +339,22 @@ async def 
test_streamable_http_enforces_external_oauth_tool_scope_before_dispatc
             json=_modern_request(
                 3,
                 "tools/call",
-                name="exec_query",
-                arguments={"sql": "SELECT 1"},
+                name="doris_catalog",
+                arguments={},
             ),
             headers=_modern_headers(
                 "tools/call",
-                name="exec_query",
-                token="exec-query",
+                name="doris_catalog",
+                token="list-only",
             ),
         )
         assert allowed.status_code == 200
-        assert allowed.json()["result"]["structuredContent"][
-            "registry_dispatch"
-        ] is True
+        assert allowed.json()["result"]["structuredContent"]["mode"] == (
+            "manifest"
+        )
+        assert allowed.json()["result"]["structuredContent"]["domain"] == (
+            "doris_catalog"
+        )
 
 
 @pytest.mark.asyncio
@@ -359,10 +371,13 @@ async def 
test_true_subprocess_stdio_keeps_local_tool_and_resource_paths_scope_f
 
     async with Client(stdio_client(server_params)) as client:
         tools = await client.list_tools(cache_mode="bypass")
-        assert len(tools.tools) == 25
+        assert {tool.name for tool in tools.tools} == set(
+            EXPECTED_DOMAIN_CHILDREN
+        )
 
-        called = await client.call_tool("exec_query", {"sql": "SELECT 1"})
-        assert called.structured_content["registry_dispatch"] is True
+        called = await client.call_tool("doris_catalog", {})
+        assert called.structured_content["mode"] == "manifest"
+        assert called.structured_content["domain"] == "doris_catalog"
 
         resources = await client.list_resources(cache_mode="bypass")
         assert resources.resources == []
diff --git a/test/security/test_operation_policy.py 
b/test/security/test_operation_policy.py
index 5d99366..e865345 100644
--- a/test/security/test_operation_policy.py
+++ b/test/security/test_operation_policy.py
@@ -11,6 +11,9 @@ from doris_mcp_server.auth.operation_policy import (
     authorize_operation,
     filter_tools_for_auth_context,
 )
+from doris_mcp_server.tools.doris_feature_matrix import (
+    EXPECTED_DOMAIN_CHILDREN,
+)
 from doris_mcp_server.utils.security import AuthContext
 
 
@@ -53,6 +56,40 @@ def 
test_missing_auth_context_passes_for_legacy_stdio_paths():
     authorize_operation(None, "tool:get_sql_profile")
 
 
[email protected]("domain_name", EXPECTED_DOMAIN_CHILDREN)
+def test_doris_oauth_allows_read_only_domain_discovery_with_tool_list_scope(
+    domain_name,
+):
+    authorize_operation(
+        doris_context(["tool:list"]),
+        f"tool:{domain_name}",
+    )
+
+
[email protected]("domain_name", EXPECTED_DOMAIN_CHILDREN)
+def test_doris_oauth_domain_discovery_rejects_missing_tool_list_scope(
+    domain_name,
+):
+    with pytest.raises(OperationAuthorizationError) as exc:
+        authorize_operation(
+            doris_context([]),
+            f"tool:{domain_name}",
+        )
+
+    assert exc.value.error_code == "PERMISSION_DENIED"
+    assert exc.value.required_scope == "tool:list"
+
+
+def test_domain_discovery_does_not_expand_default_oauth_scope_set() -> None:
+    policy = DorisOAuthScopePolicy()
+
+    assert {
+        f"tool:call:{domain_name}"
+        for domain_name in EXPECTED_DOMAIN_CHILDREN
+    }.isdisjoint(policy.server_allowed_scopes)
+    assert "tool:list" in policy.server_allowed_scopes
+
+
 @pytest.mark.parametrize("tool_name", sorted(P4_DORIS_OAUTH_METADATA_TOOLS))
 def test_doris_oauth_rejects_metadata_tools_when_db_gate_false(tool_name):
     with pytest.raises(OperationAuthorizationError) as exc:
diff --git a/test/tools/test_custom_tool_provider.py 
b/test/tools/test_custom_tool_provider.py
index 3ffd7c2..fa386fd 100644
--- a/test/tools/test_custom_tool_provider.py
+++ b/test/tools/test_custom_tool_provider.py
@@ -32,6 +32,9 @@ from doris_mcp_server.protocol import (
     create_doris_mcp_server,
     create_transport_security,
 )
+from doris_mcp_server.tools.doris_feature_matrix import (
+    EXPECTED_DOMAIN_CHILDREN,
+)
 from doris_mcp_server.tools.tool_provider import (
     CustomTool,
     LocalToolRateLimiter,
@@ -104,7 +107,7 @@ class RecordingProvider:
 
 
 @pytest.mark.asyncio
-async def 
test_provider_tool_is_listed_dispatched_audited_and_lifecycle_managed():
+async def 
test_provider_tool_is_internal_dispatched_audited_and_lifecycle_managed():
     provider = RecordingProvider()
     manager = DorisToolsManager(
         _connection_manager(),
@@ -123,7 +126,8 @@ async def 
test_provider_tool_is_listed_dispatched_audited_and_lifecycle_managed(
     )
     await manager.close()
 
-    assert "lookup_business_order" in {tool.name for tool in listed}
+    assert {tool.name for tool in listed} == set(EXPECTED_DOMAIN_CHILDREN)
+    assert "lookup_business_order" not in {tool.name for tool in listed}
     provider.handler.assert_awaited_once_with({"order_id": "order-42"})
     assert payload["ok"] is True
     assert payload["source"] == "business-api"
@@ -373,7 +377,7 @@ def _modern_headers(method: str, *, name: str | None = 
None) -> dict[str, str]:
 
 
 @pytest.mark.asyncio
-async def 
test_custom_tool_uses_real_streamable_http_list_validation_and_call():
+async def test_custom_tool_does_not_bypass_real_http_domain_discovery():
     provider = RecordingProvider()
     manager = DorisToolsManager(
         _connection_manager(),
@@ -413,29 +417,17 @@ async def 
test_custom_tool_uses_real_streamable_http_list_validation_and_call():
             json=_modern_request(1, "tools/list"),
             headers=_modern_headers("tools/list"),
         )
-        invalid = await client.post(
+        discovered = await client.post(
             "/mcp",
             json=_modern_request(
                 2,
                 "tools/call",
-                name="lookup_business_order",
-            ),
-            headers=_modern_headers(
-                "tools/call",
-                name="lookup_business_order",
-            ),
-        )
-        called = await client.post(
-            "/mcp",
-            json=_modern_request(
-                3,
-                "tools/call",
-                name="lookup_business_order",
-                arguments={"order_id": "order-42"},
+                name="doris_catalog",
+                arguments={},
             ),
             headers=_modern_headers(
                 "tools/call",
-                name="lookup_business_order",
+                name="doris_catalog",
             ),
         )
 
@@ -443,14 +435,10 @@ async def 
test_custom_tool_uses_real_streamable_http_list_validation_and_call():
     listed_tools = {
         tool["name"]: tool for tool in listed.json()["result"]["tools"]
     }
-    assert "lookup_business_order" in listed_tools
-    assert listed_tools["lookup_business_order"]["inputSchema"]["required"] == 
[
-        "order_id"
-    ]
-    assert invalid.status_code == 400
-    assert invalid.json()["error"]["code"] == -32602
-    assert called.status_code == 200
-    structured = called.json()["result"]["structuredContent"]
-    assert structured["ok"] is True
-    assert structured["source"] == "business-api"
-    assert "_execution_info" not in structured
+    assert set(listed_tools) == set(EXPECTED_DOMAIN_CHILDREN)
+    assert "lookup_business_order" not in listed_tools
+    assert discovered.status_code == 200
+    structured = discovered.json()["result"]["structuredContent"]
+    assert structured["mode"] == "manifest"
+    assert structured["domain"] == "doris_catalog"
+    provider.handler.assert_not_awaited()
diff --git a/test/tools/test_domain_manifest.py 
b/test/tools/test_domain_manifest.py
new file mode 100644
index 0000000..5ea0366
--- /dev/null
+++ b/test/tools/test_domain_manifest.py
@@ -0,0 +1,535 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Contract tests for bounded progressive domain discovery."""
+
+from __future__ import annotations
+
+import json
+from datetime import UTC, datetime, timedelta
+from pathlib import Path
+from typing import Any
+
+import pytest
+
+from doris_mcp_server.schema_validation import ToolSchemaGuard
+from doris_mcp_server.tools.domain_catalog import (
+    DORIS_DOMAIN_CATALOG,
+    DorisDomainCatalog,
+)
+from doris_mcp_server.tools.domain_manifest import (
+    DOMAIN_DISCOVERY_DESCRIPTION_SUFFIX,
+    MAX_CHILD_DESCRIPTION_CHARACTERS,
+    MAX_CHILD_SCHEMA_BYTES,
+    MAX_SCHEMA_ENUM_VALUES,
+    DomainManifestBudgetError,
+    DomainManifestManagerMixin,
+    DomainManifestService,
+)
+from doris_mcp_server.tools.domain_models import (
+    MAX_DOMAIN_MANIFEST_BYTES,
+    Availability,
+    AvailabilityStatus,
+    ChildToolDefinition,
+    DomainDefinition,
+    DomainErrorCode,
+)
+from doris_mcp_server.tools.doris_feature_matrix import (
+    EXPECTED_DOMAIN_CHILDREN,
+)
+from doris_mcp_server.utils.security import AuthContext
+
+FIXED_TIME = datetime(2026, 7, 31, 8, 30, tzinfo=UTC)
+REPO_ROOT = Path(__file__).resolve().parents[2]
+
+
+class StaticAvailabilityProvider:
+    def __init__(
+        self,
+        availability: Availability,
+    ) -> None:
+        self.availability = availability
+        self.calls: list[str] = []
+
+    async def availability_for(
+        self,
+        domain: DomainDefinition,
+        child: ChildToolDefinition,
+        auth_context: Any | None,
+    ) -> Availability:
+        del auth_context
+        self.calls.append(f"{domain.name}.{child.name}")
+        return self.availability
+
+
+def _availability(
+    *,
+    status: AvailabilityStatus = AvailabilityStatus.AVAILABLE,
+    callable_: bool = True,
+    reason_code: str = "RUNTIME_PROBE_CONFIRMED",
+    active_variant: str | None = "information_schema",
+    detected_versions: dict[str, tuple[str, ...]] | None = None,
+    evidence_sources: tuple[str, ...] = ("version", "sql_probe"),
+) -> Availability:
+    return Availability(
+        status=status,
+        callable=callable_,
+        reason_code=reason_code,
+        detected_versions=(
+            detected_versions
+            if detected_versions is not None
+            else {"fe": ("4.0.7",)}
+        ),
+        active_variant=active_variant,
+        evidence_sources=evidence_sources,
+    )
+
+
+def _service(
+    provider: StaticAvailabilityProvider | None = None,
+    *,
+    clock_time: datetime = FIXED_TIME,
+) -> DomainManifestService:
+    return DomainManifestService(
+        availability_provider=provider,
+        clock=lambda: clock_time,
+    )
+
+
+def _replace_domain(
+    domain_name: str,
+    replacement: DomainDefinition,
+) -> DorisDomainCatalog:
+    domains = tuple(
+        replacement if domain.name == domain_name else domain
+        for domain in DORIS_DOMAIN_CATALOG.domains
+    )
+    return DORIS_DOMAIN_CATALOG.model_copy(update={"domains": domains})
+
+
+def _replace_child(
+    domain: DomainDefinition,
+    child_index: int,
+    replacement: ChildToolDefinition,
+) -> DomainDefinition:
+    children = list(domain.children)
+    children[child_index] = replacement
+    return domain.model_copy(update={"children": tuple(children)})
+
+
+def test_top_level_catalog_is_exactly_eight_stable_read_only_domains() -> None:
+    service = _service()
+    tools = service.list_tools()
+
+    assert [tool.name for tool in tools] == list(EXPECTED_DOMAIN_CHILDREN)
+    assert service.domain_names == frozenset(EXPECTED_DOMAIN_CHILDREN)
+    assert service.handles("doris_catalog") is True
+    assert service.handles("exec_query") is False
+    assert len(tools) == 8
+    assert len({tool.name for tool in tools}) == 8
+    assert len({str(tool.input_schema) for tool in tools}) == 1
+    assert len({str(tool.output_schema) for tool in tools}) == 1
+    ToolSchemaGuard().compile_catalog(tools)
+
+    for tool in tools:
+        assert tool.description is not None
+        assert tool.description.endswith(DOMAIN_DISCOVERY_DESCRIPTION_SUFFIX)
+        assert tool.annotations is not None
+        assert tool.annotations.read_only_hint is True
+        assert tool.annotations.destructive_hint is False
+        assert tool.annotations.idempotent_hint is True
+        assert tool.annotations.open_world_hint is False
+        for child_name in EXPECTED_DOMAIN_CHILDREN[tool.name]:
+            assert child_name not in tool.description
+
+
+def test_manager_mixin_lazily_builds_discovery_service() -> None:
+    manager = DomainManifestManagerMixin()
+
+    assert manager.domain_manifest_service.handles("doris_catalog")
+    assert manager.domain_manifest_service is manager.domain_manifest_service
+
+
+def test_checked_in_public_registry_is_generated_from_domain_catalog() -> None:
+    checked_in = (REPO_ROOT / "docs" / "tool-registry.md").read_text(
+        encoding="utf-8"
+    )
+
+    assert checked_in == _service().render_markdown()
+
+
[email protected]
+async def test_manager_mixin_lists_and_calls_domain_discovery() -> None:
+    manager = DomainManifestManagerMixin()
+
+    listed = await manager.list_tools()
+    payload = json.loads(
+        await manager._call_domain_tool("doris_catalog", {})
+    )
+
+    assert [tool.name for tool in listed] == list(EXPECTED_DOMAIN_CHILDREN)
+    assert payload["mode"] == "manifest"
+    assert payload["domain"] == "doris_catalog"
+
+
[email protected]
+async def test_empty_calls_return_complete_bounded_manifests() -> None:
+    service = _service()
+
+    for domain_name, expected_children in EXPECTED_DOMAIN_CHILDREN.items():
+        first = await service.call(domain_name, {}, None)
+        second = await service.call(domain_name, {}, None)
+
+        assert first.mode == "manifest"
+        assert first.domain == domain_name
+        assert first.generated_at == FIXED_TIME
+        assert first.manifest_version == second.manifest_version
+        assert [child.name for child in first.children] == list(
+            expected_children
+        )
+        assert (
+            len(first.to_canonical_json().encode("utf-8"))
+            <= MAX_DOMAIN_MANIFEST_BYTES
+        )
+        for child in first.children:
+            assert child.availability.status is AvailabilityStatus.UNKNOWN
+            assert child.availability.callable is False
+            assert child.availability.evidence_sources == (
+                "catalog_contract",
+            )
+            assert child.description.startswith(
+                "[UNKNOWN | capability snapshot pending] "
+            )
+        first_child = first.to_wire()["children"][0]
+        assert "excluded_ranges" not in first_child["version_support"]
+        assert "tested_versions" not in first_child["version_support"]
+        assert "detected_versions" not in first_child["availability"]
+        assert "limitations" not in first_child["availability"]
+
+
[email protected]
+async def test_non_child_permissions_do_not_hide_read_only_catalog() -> None:
+    context = AuthContext(
+        auth_method="token",
+        permissions=["catalog:read"],
+    )
+
+    manifest = await _service().call("doris_catalog", {}, context)
+
+    assert len(manifest.children) == 5
+
+
[email protected]
+async def test_manifest_version_excludes_timestamp_but_tracks_content() -> 
None:
+    available = StaticAvailabilityProvider(_availability())
+    first = await _service(available).call("doris_catalog", {}, None)
+    later = await _service(
+        StaticAvailabilityProvider(_availability()),
+        clock_time=FIXED_TIME + timedelta(minutes=5),
+    ).call("doris_catalog", {}, None)
+    unavailable = await _service(
+        StaticAvailabilityProvider(
+            _availability(
+                status=AvailabilityStatus.UNAVAILABLE,
+                callable_=False,
+                reason_code="DORIS_VERSION_UNSUPPORTED",
+                active_variant=None,
+            )
+        )
+    ).call("doris_catalog", {}, None)
+
+    assert first.manifest_version == later.manifest_version
+    assert first.generated_at != later.generated_at
+    assert first.manifest_version != unavailable.manifest_version
+
+
[email protected]
+async def test_dynamic_description_and_structured_availability_agree() -> None:
+    provider = StaticAvailabilityProvider(
+        _availability(
+            status=AvailabilityStatus.DEGRADED,
+            callable_=True,
+            reason_code="FALLBACK_ACTIVE",
+            active_variant="audit_sql_inference",
+            detected_versions={},
+            evidence_sources=("audit_log",),
+        )
+    )
+
+    manifest = await _service(provider).call(
+        "doris_governance",
+        {},
+        None,
+    )
+
+    assert manifest.children
+    for child in manifest.children:
+        assert child.description.startswith(
+            "[DEGRADED | audit_sql_inference] "
+        )
+        assert child.availability.status is AvailabilityStatus.DEGRADED
+        assert child.availability.callable is True
+        assert child.availability.active_variant == "audit_sql_inference"
+
+
[email protected]
+async def test_detected_version_has_priority_in_dynamic_description() -> None:
+    provider = StaticAvailabilityProvider(
+        _availability(
+            detected_versions={"be": ("4.0.6",), "fe": ("4.0.7",)},
+        )
+    )
+
+    manifest = await _service(provider).call("doris_cluster", {}, None)
+
+    assert manifest.children[0].description.startswith(
+        "[AVAILABLE | Doris 4.0.7 | information_schema] "
+    )
+
+
[email protected]
+async def test_realistic_runtime_evidence_stays_within_manifest_budget() -> 
None:
+    provider = StaticAvailabilityProvider(
+        _availability(
+            detected_versions={
+                "master_fe": ("4.1.3",),
+                "fe": ("4.1.3",),
+                "be": ("4.1.3",),
+            },
+            evidence_sources=(
+                "version_probe",
+                "sql_probe",
+                "provider_config",
+            ),
+        )
+    )
+
+    manifest = await _service(provider).call("doris_cluster", {}, None)
+
+    assert (
+        len(manifest.to_canonical_json().encode("utf-8"))
+        <= MAX_DOMAIN_MANIFEST_BYTES
+    )
+
+
[email protected]
+async def test_explicit_child_grants_hide_every_unauthorized_child() -> None:
+    provider = StaticAvailabilityProvider(_availability())
+    context = AuthContext(
+        auth_method="external_oauth",
+        oauth_scopes=[
+            "tool:call:doris_catalog",
+            "child:call:doris_catalog:list_tables",
+        ],
+    )
+
+    catalog = await _service(provider).call(
+        "doris_catalog",
+        {},
+        context,
+    )
+    cluster = await _service(provider).call(
+        "doris_cluster",
+        {},
+        context,
+    )
+
+    assert [child.name for child in catalog.children] == ["list_tables"]
+    assert cluster.children == ()
+    assert provider.calls == ["doris_catalog.list_tables"]
+    assert "list_catalogs" not in catalog.to_canonical_json()
+
+
[email protected]
+async def test_explicit_discovery_permission_is_accepted() -> None:
+    context = AuthContext(
+        permissions=["child:discover:doris_semantic:get_semantic_context"],
+    )
+
+    manifest = await _service().call("doris_semantic", {}, context)
+
+    assert [child.name for child in manifest.children] == [
+        "get_semantic_context"
+    ]
+
+
[email protected]
+async def test_authorized_unavailable_children_remain_discoverable() -> None:
+    provider = StaticAvailabilityProvider(
+        _availability(
+            status=AvailabilityStatus.MISCONFIGURED,
+            callable_=False,
+            reason_code="PROVIDER_NOT_CONFIGURED",
+            active_variant=None,
+            detected_versions={},
+            evidence_sources=("provider_config",),
+        )
+    )
+
+    manifest = await _service(provider).call(
+        "doris_semantic",
+        {},
+        None,
+    )
+
+    assert len(manifest.children) == 4
+    assert all(not child.availability.callable for child in manifest.children)
+    assert all(
+        child.description.startswith(
+            "[MISCONFIGURED | provider not configured] "
+        )
+        for child in manifest.children
+    )
+
+
[email protected]
+async def test_public_version_support_omits_internal_execution_contracts() -> 
None:
+    manifest = await _service().call("doris_cluster", {}, None)
+    serialized = manifest.to_canonical_json()
+
+    for child in manifest.children:
+        support = child.version_support.to_wire()
+        assert support["rule_id"]
+        assert support["supported_ranges"]
+        assert "variants" not in support
+        assert "required_probes" not in support
+        assert "source_references" not in support
+    assert '"required_providers"' not in serialized
+    assert '"handler_name"' not in serialized
+    assert '"authorization_policy"' not in serialized
+
+
[email protected]
+async def test_invalid_or_execution_requests_fail_with_stable_envelopes() -> 
None:
+    service = _service()
+
+    empty_arguments = await service.call(
+        "doris_catalog",
+        {"arguments": {}},
+        None,
+    )
+    invalid = await service.call(
+        "doris_catalog",
+        {"arguments": {"database": "analytics"}},
+        None,
+    )
+    execution = await service.call(
+        "doris_catalog",
+        {
+            "child_tool": "list_tables",
+            "arguments": {"database": "analytics"},
+            "manifest_version": "catalog.previous",
+        },
+        None,
+    )
+
+    assert empty_arguments.mode == "error"
+    assert (
+        empty_arguments.error.code
+        is DomainErrorCode.CHILD_ARGUMENTS_INVALID
+    )
+    assert invalid.mode == "error"
+    assert invalid.error.code is DomainErrorCode.CHILD_ARGUMENTS_INVALID
+    assert execution.mode == "error"
+    assert (
+        execution.error.code
+        is DomainErrorCode.CHILD_CAPABILITY_UNAVAILABLE
+    )
+    assert execution.child_tool == "list_tables"
+    assert execution.manifest_version == "catalog.previous"
+
+
+def test_startup_rejects_manifest_over_domain_budget() -> None:
+    domain = DORIS_DOMAIN_CATALOG.resolve_domain("doris_cluster")
+    constrained = domain.model_copy(update={"max_manifest_bytes": 100})
+
+    with pytest.raises(DomainManifestBudgetError, match="limit is 100 bytes"):
+        DomainManifestService(
+            catalog=_replace_domain(domain.name, constrained)
+        )
+
+
[email protected](
+    ("replacement", "message"),
+    [
+        (
+            {"canonical_description": "x" * MAX_CHILD_DESCRIPTION_CHARACTERS},
+            "description exceeds",
+        ),
+        (
+            {
+                "input_schema": {
+                    "type": "object",
+                    "description": "x" * MAX_CHILD_SCHEMA_BYTES,
+                }
+            },
+            "input schema is",
+        ),
+        (
+            {
+                "input_schema": {
+                    "type": "object",
+                    "properties": {
+                        "value": {
+                            "type": "string",
+                            "enum": [
+                                str(index)
+                                for index in range(
+                                    MAX_SCHEMA_ENUM_VALUES + 1
+                                )
+                            ],
+                        }
+                    },
+                }
+            },
+            "schema enum exceeds",
+        ),
+    ],
+)
+def test_startup_rejects_child_level_budget_violations(
+    replacement: dict[str, Any],
+    message: str,
+) -> None:
+    domain = DORIS_DOMAIN_CATALOG.resolve_domain("doris_catalog")
+    child = domain.children[0].model_copy(update=replacement)
+    modified = _replace_child(domain, 0, child)
+
+    with pytest.raises(DomainManifestBudgetError, match=message):
+        DomainManifestService(
+            catalog=_replace_domain(domain.name, modified)
+        )
+
+
[email protected]
+async def test_runtime_availability_cannot_overflow_manifest_budget() -> None:
+    provider = StaticAvailabilityProvider(
+        _availability(
+            evidence_sources=tuple(
+                f"evidence_{index}" for index in range(500)
+            )
+        )
+    )
+
+    with pytest.raises(DomainManifestBudgetError, match="manifest is"):
+        await _service(provider).call("doris_cluster", {}, None)
+
+
[email protected]
+async def test_naive_runtime_clock_fails_closed() -> None:
+    service = _service(clock_time=datetime(2026, 7, 31, 8, 30))
+
+    with pytest.raises(ValueError, match="must include a timezone"):
+        await service.call("doris_catalog", {}, None)
diff --git a/test/tools/test_domain_models.py b/test/tools/test_domain_models.py
index ecf8b08..2c8ab74 100644
--- a/test/tools/test_domain_models.py
+++ b/test/tools/test_domain_models.py
@@ -40,6 +40,7 @@ from doris_mcp_server.tools.domain_models import (
     ExecutionEnvelope,
     ExecutionMetadata,
     JsonObject,
+    ManifestVersionSupport,
     StandardError,
     ToolContractAnnotations,
     UnavailableBehavior,
@@ -137,7 +138,9 @@ def _manifest_child() -> ChildManifestEntry:
         description="[AVAILABLE] List visible tables in one Doris database.",
         input_schema=child.input_schema,
         output_schema=child.output_schema,
-        version_support=child.support_contract,
+        version_support=ManifestVersionSupport.from_contract(
+            child.support_contract
+        ),
         availability=_availability(),
         annotations=child.annotations,
     )
@@ -229,6 +232,30 @@ def test_support_contract_rejects_duplicate_variant_ids() 
-> None:
         _support_contract(_variant(), _variant())
 
 
+def test_manifest_version_support_is_a_compact_ordered_projection() -> None:
+    support = ManifestVersionSupport.from_contract(
+        _support_contract(
+            _variant(name="native"),
+            CapabilityVariant(
+                name="fallback",
+                supported_ranges=(">=3.0.0",),
+                excluded_ranges=("==4.0.6-rc1",),
+                required_probes=("audit_readable",),
+            ),
+        )
+    )
+
+    assert support.to_wire() == {
+        "rule_id": "catalog.list_tables.v1",
+        "supported_ranges": [
+            "project-supported Doris releases",
+            ">=3.0.0",
+        ],
+        "excluded_ranges": ["==4.0.6-rc1"],
+        "tested_versions": ["4.0.5-rc01"],
+    }
+
+
 def test_child_rejects_empty_handler_name() -> None:
     with pytest.raises(ValidationError, match="handler_name"):
         _child(handler_name="")
@@ -382,18 +409,20 @@ def 
test_degraded_availability_may_be_callable_or_disabled() -> None:
 
 def test_domain_request_distinguishes_discovery_and_execution() -> None:
     discovery = DomainToolRequest()
-    empty_discovery = DomainToolRequest(arguments={})
     execution = DomainToolRequest(child_tool="list_tables")
 
     assert discovery.is_discovery is True
-    assert empty_discovery.is_discovery is True
     assert execution.is_discovery is False
     assert execution.execution_arguments == {}
 
 
 def test_domain_request_rejects_arguments_without_child() -> None:
-    with pytest.raises(ValidationError, match="arguments require child_tool"):
-        DomainToolRequest(arguments={"database": "analytics"})
+    for arguments in ({}, {"database": "analytics"}):
+        with pytest.raises(
+            ValidationError,
+            match="arguments require child_tool",
+        ):
+            DomainToolRequest(arguments=arguments)
 
 
 def test_request_arguments_and_result_data_are_deeply_immutable() -> None:
diff --git a/test/tools/test_tool_registry.py b/test/tools/test_tool_registry.py
index 5a5631c..a51bff4 100644
--- a/test/tools/test_tool_registry.py
+++ b/test/tools/test_tool_registry.py
@@ -20,7 +20,6 @@ from __future__ import annotations
 
 import inspect
 import json
-from pathlib import Path
 from unittest.mock import AsyncMock, Mock, patch
 
 import pytest
@@ -39,8 +38,6 @@ from doris_mcp_server.tools.tool_registry import (
 )
 from doris_mcp_server.tools.tools_manager import DorisToolsManager
 
-REPO_ROOT = Path(__file__).resolve().parents[2]
-
 
 @pytest.fixture
 def tools_manager() -> DorisToolsManager:
@@ -183,13 +180,14 @@ def 
test_registry_rejects_duplicate_unknown_and_missing_handler_definitions(
         )
 
 
-def test_checked_in_tool_catalog_is_generated_from_registry(
+def test_internal_migration_registry_rendering_is_deterministic(
     tools_manager: DorisToolsManager,
 ) -> None:
-    checked_in = (REPO_ROOT / "docs" / "tool-registry.md").read_text(
-        encoding="utf-8"
-    )
-    assert checked_in == tools_manager.tool_registry.render_markdown()
+    first = tools_manager.tool_registry.render_markdown()
+    second = tools_manager.tool_registry.render_markdown()
+
+    assert first == second
+    assert first.startswith("# Doris MCP Tool Registry")
 
 
 def test_tools_manager_has_no_parallel_decorator_or_dispatch_registry() -> 
None:
diff --git a/test/tools/test_tools_manager.py b/test/tools/test_tools_manager.py
index ca01dd7..5ed3ed4 100644
--- a/test/tools/test_tools_manager.py
+++ b/test/tools/test_tools_manager.py
@@ -25,6 +25,9 @@ from unittest.mock import AsyncMock, Mock, patch
 import pytest
 
 from doris_mcp_server.schema_validation import ToolSchemaGuard
+from doris_mcp_server.tools.doris_feature_matrix import (
+    EXPECTED_DOMAIN_CHILDREN,
+)
 from doris_mcp_server.tools.tools_manager import DorisToolsManager
 from doris_mcp_server.utils.config import DorisConfig
 
@@ -76,12 +79,8 @@ class TestDorisToolsManager:
         """Test getting available tools"""
         tools = await tools_manager.list_tools()
 
-        # Should have core tools
         tool_names = [tool.name for tool in tools]
-        assert "exec_query" in tool_names
-        assert "get_db_list" in tool_names
-        assert "get_db_table_list" in tool_names
-        assert "get_table_schema" in tool_names
+        assert tool_names == list(EXPECTED_DOMAIN_CHILDREN)
 
     @pytest.mark.asyncio
     async def test_exec_query_tool(self, tools_manager):
diff --git a/test/tools/test_tools_operation_guard.py 
b/test/tools/test_tools_operation_guard.py
index 8a679c9..27c7557 100644
--- a/test/tools/test_tools_operation_guard.py
+++ b/test/tools/test_tools_operation_guard.py
@@ -12,6 +12,9 @@ from doris_mcp_server.auth.operation_policy import (
     OperationAuthorizationError,
 )
 from doris_mcp_server.protocol import create_doris_mcp_server
+from doris_mcp_server.tools.doris_feature_matrix import (
+    EXPECTED_DOMAIN_CHILDREN,
+)
 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
@@ -619,7 +622,9 @@ async def 
test_user_roles_fall_back_to_current_user_grants():
 
 
 @pytest.mark.asyncio
-async def 
test_doris_oauth_list_tools_uses_configured_default_scope_visibility(tmp_path):
+async def test_doris_oauth_list_tools_exposes_only_domain_discovery_scopes(
+    tmp_path,
+):
     manager, _connection_manager = _real_tool_manager_for_routing(tmp_path)
     token = set_current_auth_context(
         doris_context(
@@ -645,17 +650,7 @@ async def 
test_doris_oauth_list_tools_uses_configured_default_scope_visibility(t
         reset_auth_context(token)
 
     names = {tool.name for tool in tools}
-    assert {
-        "exec_query",
-        "get_sql_explain",
-        "get_db_list",
-        "get_db_table_list",
-        "get_table_schema",
-        "get_table_comment",
-        "get_table_column_comments",
-        "get_table_indexes",
-        "get_catalog_list",
-    } <= names
+    assert names == set(EXPECTED_DOMAIN_CHILDREN)
     assert names.isdisjoint(HIGH_RISK_TOOLS)
 
 


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

Reply via email to