This is an automated email from the ASF dual-hosted git repository.

fanng pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/main by this push:
     new 168137633a [#8138] feat(mcp-server): Support statistic operation for 
MCP server. (#8214)
168137633a is described below

commit 168137633a7cb9681e828055fd4e2a3c15d27fd0
Author: Mini Yu <[email protected]>
AuthorDate: Mon Aug 25 10:06:06 2025 +0800

    [#8138] feat(mcp-server): Support statistic operation for MCP server. 
(#8214)
    
    ### What changes were proposed in this pull request?
    
    Support statistics related operations for MCP server.
    
    ### Why are the changes needed?
    
    It's a big feature for MCP server.
    
    Fix: #8138
    
    ### Does this PR introduce _any_ user-facing change?
    
    N/A.
    
    ### How was this patch tested?
    
    UTs and test locally.
---
 docs/gravitino-mcp-server.md                       |  48 +++++--
 mcp-server/mcp_server/client/fileset_operation.py  |   2 +-
 .../mcp_server/client/gravitino_operation.py       |  11 ++
 mcp-server/mcp_server/client/job_operation.py      |   4 +-
 mcp-server/mcp_server/client/model_operation.py    |   4 +-
 .../plain/plain_rest_client_fileset_operation.py   |   2 +-
 .../plain/plain_rest_client_job_operation.py       |   4 +-
 .../plain/plain_rest_client_model_operation.py     |   4 +-
 .../client/plain/plain_rest_client_operation.py    |   9 ++
 .../plain/plain_rest_client_statistic_operation.py |  53 ++++++++
 .../plain/plain_rest_client_tag_operation.py       |   2 +-
 .../plain/plain_rest_client_topic_operation.py     |   4 +-
 .../mcp_server/client/statistic_operation.py       |  72 ++++++++++
 mcp-server/mcp_server/client/tag_operation.py      |   2 +-
 mcp-server/mcp_server/client/topic_operation.py    |   4 +-
 mcp-server/mcp_server/tools/__init__.py            |   2 +
 mcp-server/mcp_server/tools/fileset.py             |   4 +-
 mcp-server/mcp_server/tools/job.py                 |  10 +-
 mcp-server/mcp_server/tools/model.py               |   4 +-
 mcp-server/mcp_server/tools/statistic.py           | 147 +++++++++++++++++++++
 mcp-server/mcp_server/tools/tag.py                 |  10 +-
 mcp-server/mcp_server/tools/topic.py               |   4 +-
 mcp-server/tests/unit/tools/mock_operation.py      |  45 +++++--
 mcp-server/tests/unit/tools/test_fileset.py        |  77 +++++++++++
 mcp-server/tests/unit/tools/test_job.py            |  10 +-
 mcp-server/tests/unit/tools/test_model.py          | 108 +++++++++++++++
 mcp-server/tests/unit/tools/test_statistic.py      |  72 ++++++++++
 mcp-server/tests/unit/tools/test_tag.py            | 100 ++++++++++++++
 mcp-server/tests/unit/tools/test_topic.py          |  59 +++++++++
 29 files changed, 813 insertions(+), 64 deletions(-)

diff --git a/docs/gravitino-mcp-server.md b/docs/gravitino-mcp-server.md
index 46d3b505cf..ca0f021184 100644
--- a/docs/gravitino-mcp-server.md
+++ b/docs/gravitino-mcp-server.md
@@ -55,17 +55,43 @@ Or start a HTTP MCP server by `uv run mcp_server --metalake 
test --uri http://12
 
 Gravitino MCP server supports the following tools, and you could export tool 
by tag.
 
-| Tool name                       | Description                                
                         | Tag       | Since version |
-|---------------------------------|---------------------------------------------------------------------|-----------|---------------|
-| `get_list_of_catalogs`          | Retrieve a list of all catalogs in the 
system.                      | `catalog` | 1.0.0         |
-| `get_list_of_schemas`           | Retrieve a list of schemas belonging to a 
specific catalog.         | `schema`  | 1.0.0         |
-| `get_list_of_tables`            | Retrieve a list of tables within a 
specific catalog and schema.     | `table`   | 1.0.0         |
-| `get_table_metadata_details`    | Retrieve comprehensive metadata details 
for a specific table.       | `table`   | 1.0.0         |
-| `get_list_of_policies`          | Retrieve a list of policies in the system. 
                         | `policy`  | 1.0.0         |
-| `get_policy_detail_information` | Retrieve detailed information for a 
specific policy by policy name. | `policy`  | 1.0.0         |
-| `list_policies_for_metadata`    | List all policies associated with a 
specific metadata item.         | `policy`  | 1.0.0         |
-| `list_metadata_by_policy`       | List all metadata items associated with a 
specific policy.          | `policy`  | 1.0.0         |
-| `get_policy_for_metadata`       | Get a policy associated with a specific 
metadata item.              | `policy`  | 1.0.0         |
+| Tool name                           | Description                            
                                        | Tag          | Since version |
+|-------------------------------------|--------------------------------------------------------------------------------|--------------|---------------|
+| `get_list_of_catalogs`              | Retrieve a list of all catalogs in the 
system.                                 | `catalog`    | 1.0.0         |
+| `get_list_of_schemas`               | Retrieve a list of schemas belonging 
to a specific catalog.                    | `schema`     | 1.0.0         |
+| `get_list_of_tables`                | Retrieve a list of tables within a 
specific catalog and schema.                | `table`      | 1.0.0         |
+| `get_table_metadata_details`        | Retrieve comprehensive metadata 
details for a specific table.                  | `table`      | 1.0.0         |
+| `list_of_models`                    | Retrieve a list of models within a 
specific catalog and schema.                | `model`      | 1.0.0         |
+| `load_model`                        | Retrieve comprehensive metadata 
details for a specific model.                  | `model`      | 1.0.0         | 
+| `list_model_versions`               | Retrieve a list of versions for a 
specific model.                              | `model`      | 1.0.0         |
+| `load_model_version`                | Retrieve comprehensive metadata 
details for a specific model version.          | `model`      | 1.0.0         | 
+| `load_model_version_by_alias`       | Retrieve comprehensive metadata 
details for a specific model version by alias. | `model`      | 1.0.0         |
+| `metadata_type_to_fullname_formats` | Retrieve the metadata type to fullname 
formats mapping.                        | `metadata`   | 1.0.0         |
+| `list_of_topics`                    | Retrieve a list of topics within a 
specific catalog and schema.                | `topic`      | 1.0.0         |
+| `load_topic`                        | Retrieve comprehensive metadata 
details for a specific topic.                  | `topic`      | 1.0.0         |
+| `list_of_filesets`                  | Retrieve a list of filesets within a 
specific catalog and schema.              | `fileset`    | 1.0.0         |
+| `load_fileset`                      | Retrieve comprehensive metadata 
details for a specific fileset.                | `fileset`    | 1.0.0         |
+| `list_files_in_fileset`             | Retrieve a list of files within a 
specific fileset.                            | `fileset`    | 1.0.0         |
+| `list_of_jobs`                      | Retrieve a list of jobs                
                                        | `job`        | 1.0.0         |
+| `get_job_by_id`                     | Retrieve a job by its ID.              
                                        | `job`        | 1.0.0         |
+| `list_of_job_templates`             | Retrieve a list of job templates.      
                                        | `job`        | 1.0.0         |
+| `get_job_template_by_name`          | Retrieve a job template by its name.   
                                        | `job`        | 1.0.0         |
+| `run_job`                           | Run a job with the specified 
parameters.                                       | `job`        | 1.0.0        
 |
+| `cancel_job`                        | Cancel a running job by its ID.        
                                        | `job`        | 1.0.0         |
+| `get_tag_by_name`                   | Retrieve a tag by its name.            
                                        | `tag`        | 1.0.0         |
+| `list_of_tags`                      | Retrieve a list of tags.               
                                        | `tag`        | 1.0.0         |
+| `list_tags_for_metadata`            | Retrieve a list of tags associated 
with a specific metadata item.              | `tag`        | 1.0.0         |
+| `list_metadata_by_tag`              | Retrieve a list of metadata items 
associated with a specific tag.              | `tag`        | 1.0.0         |
+| `associate_tag_with_metadata`       | Associate tags with a specific 
metadata item.                                  | `tag`        | 1.0.0         |
+| `disassociate_tag_from_metadata`    | Disassociate tags from a specific 
metadata item.                               | `tag`        | 1.0.0         |
+| `list_statistics_for_metadata`      | Retrieve a list of statistics 
associated with a specific metadata item.        | `statistics` | 1.0.0         
|
+| `list_statistics_for_partition`     | Retrieve a list of statistics 
associated with a specific partition.            | `statistics` | 1.0.0         
|
+| `get_list_of_policies`              | Retrieve a list of policies in the 
system.                                     | `policy`     | 1.0.0         |
+| `get_policy_detail_information`     | Retrieve detailed information for a 
specific policy by policy name.            | `policy`     | 1.0.0         |
+| `list_policies_for_metadata`        | List all policies associated with a 
specific metadata item.                    | `policy`     | 1.0.0         |
+| `list_metadata_by_policy`           | List all metadata items associated 
with a specific policy.                     | `policy`     | 1.0.0         |
+| `get_policy_for_metadata`           | Get a policy associated with a 
specific metadata item.                         | `policy`     | 1.0.0         |
+
 
 ### Configuration
 
diff --git a/mcp-server/mcp_server/client/fileset_operation.py 
b/mcp-server/mcp_server/client/fileset_operation.py
index e41b3ec74f..a158c11cda 100644
--- a/mcp-server/mcp_server/client/fileset_operation.py
+++ b/mcp-server/mcp_server/client/fileset_operation.py
@@ -24,7 +24,7 @@ class FilesetOperation(ABC):
     """
 
     @abstractmethod
-    async def get_list_of_filesets(
+    async def list_of_filesets(
         self, catalog_name: str, schema_name: str
     ) -> str:
         """
diff --git a/mcp-server/mcp_server/client/gravitino_operation.py 
b/mcp-server/mcp_server/client/gravitino_operation.py
index 6b0c4fa12b..a2e7fa152e 100644
--- a/mcp-server/mcp_server/client/gravitino_operation.py
+++ b/mcp-server/mcp_server/client/gravitino_operation.py
@@ -23,6 +23,7 @@ from mcp_server.client.job_operation import JobOperation
 from mcp_server.client.model_operation import ModelOperation
 from mcp_server.client.policy_operation import PolicyOperation
 from mcp_server.client.schema_operation import SchemaOperation
+from mcp_server.client.statistic_operation import StatisticOperation
 from mcp_server.client.table_operation import TableOperation
 from mcp_server.client.tag_operation import TagOperation
 from mcp_server.client.topic_operation import TopicOperation
@@ -121,3 +122,13 @@ class GravitinoOperation(ABC):
             JobOperation: Interface for performing job-level operations
         """
         pass
+
+    @abstractmethod
+    def as_statistic_operation(self) -> StatisticOperation:
+        """
+        Access the statistic operation interface of this Gravitino operation.
+
+        Returns:
+            StatisticOperation: Interface for performing statistic-level 
operations
+        """
+        pass
diff --git a/mcp-server/mcp_server/client/job_operation.py 
b/mcp-server/mcp_server/client/job_operation.py
index c83f750bfc..1361ce0aa3 100644
--- a/mcp-server/mcp_server/client/job_operation.py
+++ b/mcp-server/mcp_server/client/job_operation.py
@@ -37,7 +37,7 @@ class JobOperation(ABC):
         pass
 
     @abstractmethod
-    async def get_list_of_jobs(self, job_template_name: str) -> str:
+    async def list_of_jobs(self, job_template_name: str) -> str:
         """
         Retrieve the list of jobs within the metalake
 
@@ -51,7 +51,7 @@ class JobOperation(ABC):
         pass
 
     @abstractmethod
-    async def get_list_of_job_templates(self) -> str:
+    async def list_of_job_templates(self) -> str:
         """
         Retrieve the list of job templates within the metalake
 
diff --git a/mcp-server/mcp_server/client/model_operation.py 
b/mcp-server/mcp_server/client/model_operation.py
index 9f6e4e9278..c63eda326a 100644
--- a/mcp-server/mcp_server/client/model_operation.py
+++ b/mcp-server/mcp_server/client/model_operation.py
@@ -24,9 +24,7 @@ class ModelOperation(ABC):
     """
 
     @abstractmethod
-    async def get_list_of_models(
-        self, catalog_name: str, schema_name: str
-    ) -> str:
+    async def list_of_models(self, catalog_name: str, schema_name: str) -> str:
         """
         Retrieve the list of models within a specified catalog.
 
diff --git 
a/mcp-server/mcp_server/client/plain/plain_rest_client_fileset_operation.py 
b/mcp-server/mcp_server/client/plain/plain_rest_client_fileset_operation.py
index 9548d7aab2..d0af2ebc9e 100644
--- a/mcp-server/mcp_server/client/plain/plain_rest_client_fileset_operation.py
+++ b/mcp-server/mcp_server/client/plain/plain_rest_client_fileset_operation.py
@@ -23,7 +23,7 @@ class PlainRESTClientFilesetOperation(FilesetOperation):
         self.metalake_name = metalake_name
         self.rest_client = rest_client
 
-    async def get_list_of_filesets(
+    async def list_of_filesets(
         self, catalog_name: str, schema_name: str
     ) -> str:
         response = await self.rest_client.get(
diff --git 
a/mcp-server/mcp_server/client/plain/plain_rest_client_job_operation.py 
b/mcp-server/mcp_server/client/plain/plain_rest_client_job_operation.py
index 687c974976..964bab841d 100644
--- a/mcp-server/mcp_server/client/plain/plain_rest_client_job_operation.py
+++ b/mcp-server/mcp_server/client/plain/plain_rest_client_job_operation.py
@@ -33,7 +33,7 @@ class PlainRESTClientJobOperation(JobOperation):
         )
         return extract_content_from_response(response, "job", {})
 
-    async def get_list_of_jobs(self, job_template_name: str) -> str:
+    async def list_of_jobs(self, job_template_name: str) -> str:
         url = f"/api/metalakes/{self.metalake_name}/jobs/runs"
         if job_template_name:
             url += f"?jobTemplateName={job_template_name}"
@@ -47,7 +47,7 @@ class PlainRESTClientJobOperation(JobOperation):
         )
         return extract_content_from_response(response, "jobTemplate", {})
 
-    async def get_list_of_job_templates(self) -> str:
+    async def list_of_job_templates(self) -> str:
         response = await self.rest_client.get(
             f"/api/metalakes/{self.metalake_name}/jobs/templates?details=true"
         )
diff --git 
a/mcp-server/mcp_server/client/plain/plain_rest_client_model_operation.py 
b/mcp-server/mcp_server/client/plain/plain_rest_client_model_operation.py
index 266865973b..3419d4fb52 100644
--- a/mcp-server/mcp_server/client/plain/plain_rest_client_model_operation.py
+++ b/mcp-server/mcp_server/client/plain/plain_rest_client_model_operation.py
@@ -27,9 +27,7 @@ class PlainRESTClientModelOperation(ModelOperation):
         self.metalake_name = metalake_name
         self.rest_client = rest_client
 
-    async def get_list_of_models(
-        self, catalog_name: str, schema_name: str
-    ) -> str:
+    async def list_of_models(self, catalog_name: str, schema_name: str) -> str:
         response = await self.rest_client.get(
             
f"/api/metalakes/{self.metalake_name}/catalogs/{catalog_name}/schemas/{schema_name}/models"
         )
diff --git a/mcp-server/mcp_server/client/plain/plain_rest_client_operation.py 
b/mcp-server/mcp_server/client/plain/plain_rest_client_operation.py
index 28798d699f..205562b6d8 100644
--- a/mcp-server/mcp_server/client/plain/plain_rest_client_operation.py
+++ b/mcp-server/mcp_server/client/plain/plain_rest_client_operation.py
@@ -45,6 +45,9 @@ from 
mcp_server.client.plain.plain_rest_client_policy_operation import (
 from mcp_server.client.plain.plain_rest_client_schema_operation import (
     PlainRESTClientSchemaOperation,
 )
+from mcp_server.client.plain.plain_rest_client_statistic_operation import (
+    PlainRESTClientStatisticOperation,
+)
 from mcp_server.client.plain.plain_rest_client_table_operation import (
     PlainRESTClientTableOperation,
 )
@@ -88,6 +91,9 @@ class PlainRESTClientOperation(GravitinoOperation):
         self._policy_operation = PlainRESTClientPolicyOperation(
             metalake_name, _rest_client
         )
+        self._statistic_operation = PlainRESTClientStatisticOperation(
+            metalake_name, _rest_client
+        )
 
     def as_catalog_operation(self) -> CatalogOperation:
         return self._catalog_operation
@@ -113,5 +119,8 @@ class PlainRESTClientOperation(GravitinoOperation):
     def as_job_operation(self) -> JobOperation:
         return self._job_operation
 
+    def as_statistic_operation(self):
+        return self._statistic_operation
+
     def as_policy_operation(self) -> PolicyOperation:
         return self._policy_operation
diff --git 
a/mcp-server/mcp_server/client/plain/plain_rest_client_statistic_operation.py 
b/mcp-server/mcp_server/client/plain/plain_rest_client_statistic_operation.py
new file mode 100644
index 0000000000..4417cd8b01
--- /dev/null
+++ 
b/mcp-server/mcp_server/client/plain/plain_rest_client_statistic_operation.py
@@ -0,0 +1,53 @@
+# 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.
+
+from mcp_server.client.plain.utils import extract_content_from_response
+from mcp_server.client.statistic_operation import StatisticOperation
+
+
+class PlainRESTClientStatisticOperation(StatisticOperation):
+    def __init__(self, metalake_name: str, rest_client):
+        self.metalake_name = metalake_name
+        self.rest_client = rest_client
+
+    async def list_of_statistics(
+        self, metalake_name: str, metadata_type: str, metadata_fullname: str
+    ) -> str:
+        response = await self.rest_client.get(
+            
f"/api/metalakes/{metalake_name}/objects/{metadata_type}/{metadata_fullname}/statistics"
+        )
+        return extract_content_from_response(response, "statistics", [])
+
+    # pylint: disable=R0917
+    async def list_statistic_for_partition(
+        self,
+        metalake_name: str,
+        metadata_type: str,
+        metadata_fullname: str,
+        from_partition_name: str,
+        to_partition_name: str,
+        from_inclusive: bool = True,
+        to_inclusive: bool = False,
+    ) -> str:
+        response = await self.rest_client.get(
+            
f"/api/metalakes/{metalake_name}/objects/{metadata_type}/{metadata_fullname}/statistics/"
+            
f"partitions?from={from_partition_name}&to={to_partition_name}&fromInclusive={from_inclusive}"
+            f"&toInclusive={to_inclusive}"
+        )
+        return extract_content_from_response(
+            response, "partitionStatistics", []
+        )
diff --git 
a/mcp-server/mcp_server/client/plain/plain_rest_client_tag_operation.py 
b/mcp-server/mcp_server/client/plain/plain_rest_client_tag_operation.py
index cd3a67bcb0..85bbc13c87 100644
--- a/mcp-server/mcp_server/client/plain/plain_rest_client_tag_operation.py
+++ b/mcp-server/mcp_server/client/plain/plain_rest_client_tag_operation.py
@@ -25,7 +25,7 @@ class PlainRESTClientTagOperation(TagOperation):
         self.metalake_name = metalake_name
         self.rest_client = rest_client
 
-    async def get_list_of_tags(self) -> str:
+    async def list_of_tags(self) -> str:
         response = await self.rest_client.get(
             f"/api/metalakes/{self.metalake_name}/tags?details=true"
         )
diff --git 
a/mcp-server/mcp_server/client/plain/plain_rest_client_topic_operation.py 
b/mcp-server/mcp_server/client/plain/plain_rest_client_topic_operation.py
index e9a4b26bc9..af8f105ae8 100644
--- a/mcp-server/mcp_server/client/plain/plain_rest_client_topic_operation.py
+++ b/mcp-server/mcp_server/client/plain/plain_rest_client_topic_operation.py
@@ -27,9 +27,7 @@ class PlainRESTClientTopicOperation(TopicOperation):
         self.metalake_name = metalake_name
         self.rest_client = rest_client
 
-    async def get_list_of_topics(
-        self, catalog_name: str, schema_name: str
-    ) -> str:
+    async def list_of_topics(self, catalog_name: str, schema_name: str) -> str:
         response = await self.rest_client.get(
             
f"/api/metalakes/{self.metalake_name}/catalogs/{catalog_name}/schemas/{schema_name}/topics"
         )
diff --git a/mcp-server/mcp_server/client/statistic_operation.py 
b/mcp-server/mcp_server/client/statistic_operation.py
new file mode 100644
index 0000000000..b9c2f1f99d
--- /dev/null
+++ b/mcp-server/mcp_server/client/statistic_operation.py
@@ -0,0 +1,72 @@
+# 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.
+
+from abc import ABC, abstractmethod
+
+
+class StatisticOperation(ABC):
+    """
+    Abstract base class for Gravitino statistic operations.
+    """
+
+    @abstractmethod
+    async def list_of_statistics(
+        self, metalake_name: str, metadata_type: str, metadata_fullname: str
+    ) -> str:
+        """
+        Retrieve the list of statistics for a specific metadata type and 
fullname within a metalake.
+        Args:
+            metalake_name: Name of the metalake
+            metadata_type: Type of metadata (e.g., table, column)
+            metadata_fullname: Full name of the metadata item
+
+        Returns:
+            str: JSON-formatted string containing statistics information
+        """
+        pass
+
+    # pylint: disable=R0917
+    @abstractmethod
+    async def list_statistic_for_partition(
+        self,
+        metalake_name: str,
+        metadata_type: str,
+        metadata_fullname: str,
+        from_partition_name: str,
+        to_partition_name: str,
+        from_inclusive: bool = True,
+        to_inclusive: bool = False,
+    ) -> str:
+        """
+        Retrieve statistics for a specific partition range of a metadata item.
+        Note: This method is currently only supported list statistics for 
partitions of tables.
+            So `metadata_type` should always be "table".
+
+        Args:
+            metalake_name: Name of the metalake
+            metadata_type: Type of metadata, should be "table" for partition 
statistics
+            metadata_fullname: Full name of the metadata item, the format 
should be
+                "{catalog}.{schema}.{table}".
+            from_partition_name: Starting partition name
+            to_partition_name: Ending partition name
+            from_inclusive: Whether to include the starting partition
+            to_inclusive: Whether to include the ending partition
+
+        Returns:
+            str: JSON-formatted string containing statistics for the specified 
partitions
+        """
+        pass
diff --git a/mcp-server/mcp_server/client/tag_operation.py 
b/mcp-server/mcp_server/client/tag_operation.py
index b00a06b8f4..afb4df7b32 100644
--- a/mcp-server/mcp_server/client/tag_operation.py
+++ b/mcp-server/mcp_server/client/tag_operation.py
@@ -54,7 +54,7 @@ class TagOperation(ABC):
         pass
 
     @abstractmethod
-    async def get_list_of_tags(self) -> str:
+    async def list_of_tags(self) -> str:
         """
         Retrieve the list of tags within the metalake
 
diff --git a/mcp-server/mcp_server/client/topic_operation.py 
b/mcp-server/mcp_server/client/topic_operation.py
index 47a2f4f3b6..6bc0bc94ab 100644
--- a/mcp-server/mcp_server/client/topic_operation.py
+++ b/mcp-server/mcp_server/client/topic_operation.py
@@ -24,9 +24,7 @@ class TopicOperation(ABC):
     """
 
     @abstractmethod
-    async def get_list_of_topics(
-        self, catalog_name: str, schema_name: str
-    ) -> str:
+    async def list_of_topics(self, catalog_name: str, schema_name: str) -> str:
         """
         Retrieve the list of topics within a specified catalog.
 
diff --git a/mcp-server/mcp_server/tools/__init__.py 
b/mcp-server/mcp_server/tools/__init__.py
index 69fabbf31c..5f754b495b 100644
--- a/mcp-server/mcp_server/tools/__init__.py
+++ b/mcp-server/mcp_server/tools/__init__.py
@@ -24,6 +24,7 @@ from mcp_server.tools.metadata import load_metadata_tool
 from mcp_server.tools.model import load_model_tools
 from mcp_server.tools.policy import load_policy_tools
 from mcp_server.tools.schema import load_schema_tools
+from mcp_server.tools.statistic import load_statistic_tools
 from mcp_server.tools.table import load_table_tools
 from mcp_server.tools.tag import load_tag_tool
 from mcp_server.tools.topic import load_topic_tools
@@ -39,4 +40,5 @@ def load_tools(mcp: FastMCP):
     load_fileset_tools(mcp)
     load_tag_tool(mcp)
     load_metadata_tool(mcp)
+    load_statistic_tools(mcp)
     load_policy_tools(mcp)
diff --git a/mcp-server/mcp_server/tools/fileset.py 
b/mcp-server/mcp_server/tools/fileset.py
index 27400de207..c3997ac16d 100644
--- a/mcp-server/mcp_server/tools/fileset.py
+++ b/mcp-server/mcp_server/tools/fileset.py
@@ -20,7 +20,7 @@ from fastmcp import Context, FastMCP
 
 def load_fileset_tools(mcp: FastMCP):
     @mcp.tool(tags={"fileset"})
-    async def get_list_of_filesets(
+    async def list_of_filesets(
         ctx: Context,
         catalog_name: str,
         schema_name: str,
@@ -50,7 +50,7 @@ def load_fileset_tools(mcp: FastMCP):
             ]
         """
         client = ctx.request_context.lifespan_context.rest_client()
-        return await client.as_fileset_operation().get_list_of_filesets(
+        return await client.as_fileset_operation().list_of_filesets(
             catalog_name, schema_name
         )
 
diff --git a/mcp-server/mcp_server/tools/job.py 
b/mcp-server/mcp_server/tools/job.py
index d1e5e24414..c0d141d2ab 100644
--- a/mcp-server/mcp_server/tools/job.py
+++ b/mcp-server/mcp_server/tools/job.py
@@ -20,7 +20,7 @@ from fastmcp import Context, FastMCP
 
 def load_job_tool(mcp: FastMCP):
     @mcp.tool(tags={"job"})
-    async def get_list_of_jobs(
+    async def list_of_jobs(
         ctx: Context,
         job_template_name: str = None,
     ) -> str:
@@ -55,9 +55,7 @@ def load_job_tool(mcp: FastMCP):
                 audit: An object containing audit information, including 
creator and creation time.
         """
         client = ctx.request_context.lifespan_context.rest_client()
-        return await client.as_job_operation().get_list_of_jobs(
-            job_template_name
-        )
+        return await client.as_job_operation().list_of_jobs(job_template_name)
 
     @mcp.tool(tags={"job"})
     async def get_job_by_id(
@@ -93,7 +91,7 @@ def load_job_tool(mcp: FastMCP):
         return await client.as_job_operation().get_job_by_id(job_id)
 
     @mcp.tool(tags={"job"})
-    async def get_list_of_job_templates(
+    async def list_of_job_templates(
         ctx: Context,
     ) -> str:
         """
@@ -132,7 +130,7 @@ def load_job_tool(mcp: FastMCP):
                 scripts: A list of scripts associated with the job template 
and can be called by the executable.
         """
         client = ctx.request_context.lifespan_context.rest_client()
-        return await client.as_job_operation().get_list_of_job_templates()
+        return await client.as_job_operation().list_of_job_templates()
 
     @mcp.tool(tags={"job"})
     async def get_job_template_by_name(
diff --git a/mcp-server/mcp_server/tools/model.py 
b/mcp-server/mcp_server/tools/model.py
index 08e56343da..4e5f21fdb5 100644
--- a/mcp-server/mcp_server/tools/model.py
+++ b/mcp-server/mcp_server/tools/model.py
@@ -20,7 +20,7 @@ from fastmcp import Context, FastMCP
 
 def load_model_tools(mcp: FastMCP):
     @mcp.tool(tags={"model"})
-    async def get_list_of_models(
+    async def list_of_models(
         ctx: Context,
         catalog_name: str,
         schema_name: str,
@@ -62,7 +62,7 @@ def load_model_tools(mcp: FastMCP):
             ]
         """
         client = ctx.request_context.lifespan_context.rest_client()
-        return await client.as_model_operation().get_list_of_models(
+        return await client.as_model_operation().list_of_models(
             catalog_name, schema_name
         )
 
diff --git a/mcp-server/mcp_server/tools/statistic.py 
b/mcp-server/mcp_server/tools/statistic.py
new file mode 100644
index 0000000000..76dd1d9a6f
--- /dev/null
+++ b/mcp-server/mcp_server/tools/statistic.py
@@ -0,0 +1,147 @@
+# 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.
+
+from fastmcp import Context, FastMCP
+
+
+def load_statistic_tools(mcp: FastMCP):
+    @mcp.tool(tags={"statistic"})
+    async def list_statistics_for_metadata(
+        ctx: Context,
+        metalake_name: str,
+        metadata_type: str,
+        metadata_fullname: str,
+    ) -> str:
+        """
+        Retrieve a list of statistics for a specific metadata object. 
Currently,
+            this tool only supports statistics for tables, so `metadata_type`
+            should always be "table" and metadata_fullname should be in the 
format
+            "{catalog}.{schema}.{table}". For more information about the 
metadata
+            type and full name formats, please refer to the tool
+            'metadata_type_to_fullname_formats'.
+
+        Args:
+            ctx (Context): The request context.
+            metalake_name (str): The name of the metalake.
+            metadata_type (str): The type of metadata (e.g., table, column). 
For
+                more, please refer to too 'metadata_type_to_fullname_formats'
+            metadata_fullname (str): The full name of the metadata object. For
+                more, please refer to tool 'metadata_type_to_fullname_formats'.
+
+
+        Returns:
+            str: A JSON string containing the list of statistics.
+
+        Example Return Value:
+            [
+              {
+                "name": "custom-key1",
+                "value": "value1",
+                "reserved": false,
+                "modifiable": true,
+                "audit": {
+                  "creator": "anonymous",
+                  "createTime": "2025-08-20T07:33:41.233089Z",
+                  "lastModifier": "anonymous",
+                  "lastModifiedTime": "2025-08-20T07:33:41.233104Z"
+                }
+              }
+            ]
+
+            name: The name of the statistic.
+            value: The value of the statistic.
+            reserved: Indicates if the statistic is reserved.
+            modifiable: Indicates if the statistic can be modified.
+            audit: Metadata about the statistic's creation and modification.
+        """
+        client = ctx.request_context.lifespan_context.rest_client()
+        return await client.as_statistic_operation().list_of_statistics(
+            metalake_name, metadata_type, metadata_fullname
+        )
+
+    # pylint: disable=R0917
+    @mcp.tool(tags={"statistic"})
+    async def list_statistics_for_partition(
+        ctx: Context,
+        metalake_name: str,
+        metadata_type: str,
+        metadata_fullname: str,
+        from_partition_name: str,
+        to_partition_name: str,
+        from_inclusive: bool = True,
+        to_inclusive: bool = False,
+    ) -> str:
+        """
+        Retrieve statistics for a specific partition range of a metadata item.
+        Note: This method is currently only supported list statistics for 
partitions of tables.
+            So `metadata_type` should always be "table".
+
+        Args:
+            ctx (Context): The request context.
+            metalake_name (str): The name of the metalake.
+            metadata_type (str): The type of metadata, should be "table" for 
partition statistics.
+            metadata_fullname (str): The full name of the metadata item, the 
format should be
+                "{catalog}.{schema}.{table}".
+            from_partition_name (str): Starting partition name.
+            to_partition_name (str): Ending partition name.
+            from_inclusive (bool): Whether to include the starting partition.
+            to_inclusive (bool): Whether to include the ending partition.
+
+        Returns:
+            str: A JSON string containing statistics for the specified 
partitions.
+
+        Example Return Value:
+            [
+              {
+                "partitionName": "partitionName_92f3fa736e47",
+                "statistics": [
+                  {
+                    "name": "custom-key1",
+                    "value": "value1",
+                    "reserved": false,
+                    "modifiable": true,
+                    "audit": {
+                      "creator": "anonymous",
+                      "createTime": "2025-08-20T07:33:41.233089Z",
+                      "lastModifier": "anonymous",
+                      "lastModifiedTime": "2025-08-20T07:33:41.233104Z"
+                    }
+                  }
+                ]
+              }
+            ]
+
+            partitionName: The name of the partition.
+            statistics: A list of statistics for the partition, each statistic 
includes:
+                - name: The name of the statistic.
+                - value: The value of the statistic.
+                - reserved: Indicates if the statistic is reserved.
+                - modifiable: Indicates if the statistic can be modified.
+                - audit: Metadata about the statistic's creation and 
modification.
+        """
+        client = ctx.request_context.lifespan_context.rest_client()
+        return (
+            await client.as_statistic_operation().list_statistic_for_partition(
+                metalake_name,
+                metadata_type,
+                metadata_fullname,
+                from_partition_name,
+                to_partition_name,
+                from_inclusive,
+                to_inclusive,
+            )
+        )
diff --git a/mcp-server/mcp_server/tools/tag.py 
b/mcp-server/mcp_server/tools/tag.py
index d708b27a5b..3a96dfb93a 100644
--- a/mcp-server/mcp_server/tools/tag.py
+++ b/mcp-server/mcp_server/tools/tag.py
@@ -57,14 +57,14 @@ def load_tag_tool(mcp: FastMCP):
         )
 
     @mcp.tool(tags={"tag"})
-    async def get_tag_by_name(ctx: Context, name: str) -> str:
+    async def get_tag_by_name(ctx: Context, tag_name: str) -> str:
         """
         Load a tag by its name.
 
         Args:
             ctx (Context): The request context object containing lifespan 
context
                            and connector information.
-            name (str): Name of the tag to get
+            tag_name (str): Name of the tag to get
 
         Returns:
             str: JSON-formatted string containing the tag information
@@ -85,10 +85,10 @@ def load_tag_tool(mcp: FastMCP):
             }
         """
         client = ctx.request_context.lifespan_context.rest_client()
-        return await client.as_tag_operation().get_tag_by_name(name)
+        return await client.as_tag_operation().get_tag_by_name(tag_name)
 
     @mcp.tool(tags={"tag"})
-    async def get_list_of_tags(ctx: Context) -> str:
+    async def list_of_tags(ctx: Context) -> str:
         """
         Retrieve the list of tags within the metalake.
 
@@ -118,7 +118,7 @@ def load_tag_tool(mcp: FastMCP):
             ]
         """
         client = ctx.request_context.lifespan_context.rest_client()
-        return await client.as_tag_operation().get_list_of_tags()
+        return await client.as_tag_operation().list_of_tags()
 
     # Disable the alter_tag tool by default as it can be destructive.
     @mcp.tool(tags={"tag"}, enabled=False)
diff --git a/mcp-server/mcp_server/tools/topic.py 
b/mcp-server/mcp_server/tools/topic.py
index 552b79956a..93d2aa57d5 100644
--- a/mcp-server/mcp_server/tools/topic.py
+++ b/mcp-server/mcp_server/tools/topic.py
@@ -20,7 +20,7 @@ from fastmcp import Context, FastMCP
 
 def load_topic_tools(mcp: FastMCP):
     @mcp.tool(tags={"topic"})
-    async def get_list_of_topic(
+    async def list_of_topics(
         ctx: Context,
         catalog_name: str,
         schema_name: str,
@@ -80,7 +80,7 @@ def load_topic_tools(mcp: FastMCP):
             ]
         """
         client = ctx.request_context.lifespan_context.rest_client()
-        return await client.as_topic_operation().get_list_of_topics(
+        return await client.as_topic_operation().list_of_topics(
             catalog_name, schema_name
         )
 
diff --git a/mcp-server/tests/unit/tools/mock_operation.py 
b/mcp-server/tests/unit/tools/mock_operation.py
index 0831dfb69b..d76ba94b60 100644
--- a/mcp-server/tests/unit/tools/mock_operation.py
+++ b/mcp-server/tests/unit/tools/mock_operation.py
@@ -27,6 +27,7 @@ from mcp_server.client import (
 )
 from mcp_server.client.fileset_operation import FilesetOperation
 from mcp_server.client.job_operation import JobOperation
+from mcp_server.client.statistic_operation import StatisticOperation
 
 
 class MockOperation(GravitinoOperation):
@@ -57,6 +58,9 @@ class MockOperation(GravitinoOperation):
     def as_job_operation(self) -> JobOperation:
         return MockJobOperation()
 
+    def as_statistic_operation(self) -> StatisticOperation:
+        return MockStatisticOperation()
+
     def as_policy_operation(self) -> PolicyOperation:
         return MockPolicyOperation()
 
@@ -84,7 +88,7 @@ class MockTableOperation(TableOperation):
 
 
 class MockFilesetOperation(FilesetOperation):
-    async def get_list_of_filesets(
+    async def list_of_filesets(
         self, catalog_name: str, schema_name: str
     ) -> str:
         return "mock_filesets"
@@ -143,9 +147,7 @@ class MockPolicyOperation(PolicyOperation):
 
 
 class MockModelOperation(ModelOperation):
-    async def get_list_of_models(
-        self, catalog_name: str, schema_name: str
-    ) -> str:
+    async def list_of_models(self, catalog_name: str, schema_name: str) -> str:
         return "mock_models"
 
     async def load_model(
@@ -170,9 +172,7 @@ class MockModelOperation(ModelOperation):
 
 
 class MockTopicOperation(TopicOperation):
-    async def get_list_of_topics(
-        self, catalog_name: str, schema_name: str
-    ) -> str:
+    async def list_of_topics(self, catalog_name: str, schema_name: str) -> str:
         return "mock_topics"
 
     async def load_topic(
@@ -182,7 +182,7 @@ class MockTopicOperation(TopicOperation):
 
 
 class MockTagOperation(TagOperation):
-    async def get_list_of_tags(self) -> str:
+    async def list_of_tags(self) -> str:
         return "mock_tags"
 
     async def create_tag(
@@ -204,7 +204,7 @@ class MockTagOperation(TagOperation):
         metadata_full_name: str,
         metadata_type: str,
         tags_to_associate: list,
-        tags_to_disassociate,
+        tags_to_disassociate: list,
     ) -> str:
         return f"mock_associated_tags: {tags_to_associate} with metadata 
{metadata_full_name} of type {metadata_type}"
 
@@ -218,13 +218,13 @@ class MockTagOperation(TagOperation):
 
 
 class MockJobOperation(JobOperation):
-    async def get_list_of_jobs(self, job_template_name: str = "") -> str:
+    async def list_of_jobs(self, job_template_name: str = "") -> str:
         return "mock_jobs"
 
     async def get_job_by_id(self, job_id: str) -> str:
         return f"mock_job: {job_id}"
 
-    async def get_list_of_job_templates(self) -> str:
+    async def list_of_job_templates(self) -> str:
         return "mock_job_templates"
 
     async def get_job_template_by_name(self, name: str) -> str:
@@ -235,3 +235,26 @@ class MockJobOperation(JobOperation):
 
     async def cancel_job(self, job_id: str) -> str:
         return f"mock_job_cancelled: {job_id}"
+
+
+class MockStatisticOperation(StatisticOperation):
+    async def list_of_statistics(
+        self, metalake_name: str, metadata_type: str, metadata_fullname: str
+    ) -> str:
+        return f"mock_statistics: {metalake_name}, {metadata_type}, 
{metadata_fullname}"
+
+    # pylint: disable=R0917
+    async def list_statistic_for_partition(
+        self,
+        metalake_name: str,
+        metadata_type: str,
+        metadata_fullname: str,
+        from_partition_name: str,
+        to_partition_name: str,
+        from_inclusive: bool = True,
+        to_inclusive: bool = False,
+    ) -> str:
+        return (
+            f"mock_statistics_for_partition: {metalake_name}, {metadata_type}, 
{metadata_fullname},"
+            f" {from_partition_name}, {to_partition_name}, {from_inclusive}, 
{to_inclusive}"
+        )
diff --git a/mcp-server/tests/unit/tools/test_fileset.py 
b/mcp-server/tests/unit/tools/test_fileset.py
new file mode 100644
index 0000000000..a0c0371491
--- /dev/null
+++ b/mcp-server/tests/unit/tools/test_fileset.py
@@ -0,0 +1,77 @@
+# 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.
+
+import asyncio
+import unittest
+
+from fastmcp import Client
+
+from mcp_server.client.factory import RESTClientFactory
+from mcp_server.core import Setting
+from mcp_server.server import GravitinoMCPServer
+from tests.unit.tools import MockOperation
+
+
+class TestFilesetTool(unittest.TestCase):
+    def setUp(self):
+        RESTClientFactory.set_rest_client(MockOperation)
+        server = GravitinoMCPServer(Setting("mock_metalake"))
+        self.mcp = server.mcp
+
+    def test_list_filesets(self):
+        async def _test_list_filesets(mcp_server):
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "list_of_filesets",
+                    {"catalog_name": "mock", "schema_name": "mock"},
+                )
+                self.assertEqual("mock_filesets", result.content[0].text)
+
+        asyncio.run(_test_list_filesets(self.mcp))
+
+    def test_load_fileset(self):
+        async def _test_load_fileset(mcp_server):
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "load_fileset",
+                    {
+                        "catalog_name": "mock",
+                        "schema_name": "mock",
+                        "fileset_name": "mock",
+                    },
+                )
+                self.assertEqual("mock_fileset", result.content[0].text)
+
+        asyncio.run(_test_load_fileset(self.mcp))
+
+    def test_list_files_in_fileset(self):
+        async def _test_list_files_in_fileset(mcp_server):
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "list_files_in_fileset",
+                    {
+                        "catalog_name": "mock",
+                        "schema_name": "mock",
+                        "fileset_name": "mock",
+                        "location_name": "mock_location",
+                    },
+                )
+                self.assertEqual(
+                    "mock_files_in_fileset", result.content[0].text
+                )
+
+        asyncio.run(_test_list_files_in_fileset(self.mcp))
diff --git a/mcp-server/tests/unit/tools/test_job.py 
b/mcp-server/tests/unit/tools/test_job.py
index 22132b7f29..41440e71b9 100644
--- a/mcp-server/tests/unit/tools/test_job.py
+++ b/mcp-server/tests/unit/tools/test_job.py
@@ -35,7 +35,7 @@ class TestJobTool(unittest.TestCase):
     def test_list_job_templates(self):
         async def _test_list_job_templates(mcp_server):
             async with Client(mcp_server) as client:
-                result = await client.call_tool("get_list_of_job_templates")
+                result = await client.call_tool("list_of_job_templates")
                 self.assertEqual("mock_job_templates", result.content[0].text)
 
         asyncio.run(_test_list_job_templates(self.mcp))
@@ -64,13 +64,13 @@ class TestJobTool(unittest.TestCase):
 
         asyncio.run(_test_get_job_by_id(self.mcp))
 
-    def test_get_list_of_jobs(self):
-        async def _test_get_list_of_jobs(mcp_server):
+    def test_list_of_jobs(self):
+        async def _test_list_of_jobs(mcp_server):
             async with Client(mcp_server) as client:
-                result = await client.call_tool("get_list_of_jobs")
+                result = await client.call_tool("list_of_jobs")
                 self.assertEqual("mock_jobs", result.content[0].text)
 
-        asyncio.run(_test_get_list_of_jobs(self.mcp))
+        asyncio.run(_test_list_of_jobs(self.mcp))
 
     def test_run_job(self):
         async def _test_run_job(mcp_server):
diff --git a/mcp-server/tests/unit/tools/test_model.py 
b/mcp-server/tests/unit/tools/test_model.py
new file mode 100644
index 0000000000..ff27e64d3b
--- /dev/null
+++ b/mcp-server/tests/unit/tools/test_model.py
@@ -0,0 +1,108 @@
+# 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.
+
+import asyncio
+import unittest
+
+from fastmcp import Client
+
+from mcp_server.client.factory import RESTClientFactory
+from mcp_server.core import Setting
+from mcp_server.server import GravitinoMCPServer
+from tests.unit.tools import MockOperation
+
+
+class TestModelTool(unittest.TestCase):
+    def setUp(self):
+        RESTClientFactory.set_rest_client(MockOperation)
+        server = GravitinoMCPServer(Setting("mock_metalake"))
+        self.mcp = server.mcp
+
+    def test_list_models(self):
+        async def _test_list_models(mcp_server):
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "list_of_models",
+                    {"catalog_name": "mock", "schema_name": "mock"},
+                )
+                self.assertEqual("mock_models", result.content[0].text)
+
+        asyncio.run(_test_list_models(self.mcp))
+
+    def test_load_model(self):
+        async def _test_load_model(mcp_server):
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "load_model",
+                    {
+                        "catalog_name": "mock",
+                        "schema_name": "mock",
+                        "model_name": "mock",
+                    },
+                )
+                self.assertEqual("mock_model", result.content[0].text)
+
+        asyncio.run(_test_load_model(self.mcp))
+
+    def test_list_model_versions(self):
+        async def _test_list_model_versions(mcp_server):
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "list_model_versions",
+                    {
+                        "catalog_name": "mock",
+                        "schema_name": "mock",
+                        "model_name": "mock",
+                    },
+                )
+                self.assertEqual("mock_model_versions", result.content[0].text)
+
+        asyncio.run(_test_list_model_versions(self.mcp))
+
+    def test_load_model_version(self):
+        async def _test_load_model_version(mcp_server):
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "load_model_version",
+                    {
+                        "catalog_name": "mock",
+                        "schema_name": "mock",
+                        "model_name": "mock",
+                        "version": 1,
+                    },
+                )
+                self.assertEqual("mock_model_version", result.content[0].text)
+
+        asyncio.run(_test_load_model_version(self.mcp))
+
+    def test_load_model_version_by_alias(self):
+        async def _test_load_model_version_by_alias(mcp_server):
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "load_model_version_by_alias",
+                    {
+                        "catalog_name": "mock",
+                        "schema_name": "mock",
+                        "model_name": "mock",
+                        "alias": "latest",
+                    },
+                )
+                self.assertEqual(
+                    "mock_model_version_by_alias", result.content[0].text
+                )
+
+        asyncio.run(_test_load_model_version_by_alias(self.mcp))
diff --git a/mcp-server/tests/unit/tools/test_statistic.py 
b/mcp-server/tests/unit/tools/test_statistic.py
new file mode 100644
index 0000000000..1a8527c6bb
--- /dev/null
+++ b/mcp-server/tests/unit/tools/test_statistic.py
@@ -0,0 +1,72 @@
+# 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.
+
+import asyncio
+import unittest
+
+from fastmcp import Client
+
+from mcp_server.client.factory import RESTClientFactory
+from mcp_server.core import Setting
+from mcp_server.server import GravitinoMCPServer
+from tests.unit.tools import MockOperation
+
+
+class TestStatisticTool(unittest.TestCase):
+    def setUp(self):
+        RESTClientFactory.set_rest_client(MockOperation)
+        server = GravitinoMCPServer(Setting("mock_job"))
+        self.mcp = server.mcp
+
+    def test_list_of_statistics(self):
+        async def _test_list_of_statistics(mcp_server):
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "list_statistics_for_metadata",
+                    {
+                        "metalake_name": "mock_metalake",
+                        "metadata_type": "mock_type",
+                        "metadata_fullname": "mock_fullname",
+                    },
+                )
+                self.assertEqual(
+                    "mock_statistics: mock_metalake, mock_type, mock_fullname",
+                    result.content[0].text,
+                )
+
+        asyncio.run(_test_list_of_statistics(self.mcp))
+
+    def test_list_statistics_for_partition(self):
+        async def _test_list_statistics_for_partition(mcp_server):
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "list_statistics_for_partition",
+                    {
+                        "metalake_name": "mock_metalake",
+                        "metadata_type": "mock_type",
+                        "metadata_fullname": "mock_fullname",
+                        "from_partition_name": "from_partition",
+                        "to_partition_name": "to_partition",
+                    },
+                )
+                self.assertEqual(
+                    "mock_statistics_for_partition: mock_metalake, mock_type, 
mock_fullname, "
+                    "from_partition, to_partition, True, False",
+                    result.content[0].text,
+                )
+
+        asyncio.run(_test_list_statistics_for_partition(self.mcp))
diff --git a/mcp-server/tests/unit/tools/test_tag.py 
b/mcp-server/tests/unit/tools/test_tag.py
new file mode 100644
index 0000000000..ee5011987e
--- /dev/null
+++ b/mcp-server/tests/unit/tools/test_tag.py
@@ -0,0 +1,100 @@
+# 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.
+
+import asyncio
+import unittest
+
+from fastmcp import Client
+
+from mcp_server.client.factory import RESTClientFactory
+from mcp_server.core import Setting
+from mcp_server.server import GravitinoMCPServer
+from tests.unit.tools import MockOperation
+
+
+class TestTagTool(unittest.TestCase):
+    def setUp(self):
+        RESTClientFactory.set_rest_client(MockOperation)
+        server = GravitinoMCPServer(Setting("mock_metalake"))
+        self.mcp = server.mcp
+
+    def test_list_tags(self):
+        async def _test_list_tags(mcp_server):
+            async with Client(mcp_server) as client:
+                result = await client.call_tool("list_of_tags")
+                self.assertEqual("mock_tags", result.content[0].text)
+
+        asyncio.run(_test_list_tags(self.mcp))
+
+    def test_get_tag_by_name(self):
+        async def _test_get_tag_by_name(mcp_server):
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "get_tag_by_name",
+                    {
+                        "tag_name": "mock",
+                    },
+                )
+                self.assertEqual("mock_tag: mock", result.content[0].text)
+
+        asyncio.run(_test_get_tag_by_name(self.mcp))
+
+    def test_associate_tag_with_metadata(self):
+        async def _test_associate_tag_with_metadata(mcp_server):
+
+            tags_to_associate = ["tag1", "tag2"]
+            metadata_full_name = "catalog.schema.table"
+            metadata_type = "table"
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "associate_tag_with_metadata",
+                    {
+                        "metadata_full_name": metadata_full_name,
+                        "metadata_type": metadata_type,
+                        "tags_to_associate": tags_to_associate,
+                    },
+                )
+                self.assertEqual(
+                    f"mock_associated_tags: {tags_to_associate} with metadata"
+                    f" {metadata_full_name} of type {metadata_type}",
+                    result.content[0].text,
+                )
+
+        asyncio.run(_test_associate_tag_with_metadata(self.mcp))
+
+    def test_disassociate_tag_from_metadata(self):
+        async def _test_disassociate_tag_from_metadata(mcp_server):
+
+            tags_to_disassociate = ["tag1", "tag2"]
+            metadata_full_name = "catalog.schema.table"
+            metadata_type = "table"
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "disassociate_tag_from_metadata",
+                    {
+                        "metadata_full_name": metadata_full_name,
+                        "metadata_type": metadata_type,
+                        "tags_to_disassociate": tags_to_disassociate,
+                    },
+                )
+                self.assertEqual(
+                    f"mock_associated_tags: [] with metadata"
+                    f" {metadata_full_name} of type {metadata_type}",
+                    result.content[0].text,
+                )
+
+        asyncio.run(_test_disassociate_tag_from_metadata(self.mcp))
diff --git a/mcp-server/tests/unit/tools/test_topic.py 
b/mcp-server/tests/unit/tools/test_topic.py
new file mode 100644
index 0000000000..28422d0634
--- /dev/null
+++ b/mcp-server/tests/unit/tools/test_topic.py
@@ -0,0 +1,59 @@
+# 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.
+
+import asyncio
+import unittest
+
+from fastmcp import Client
+
+from mcp_server.client.factory import RESTClientFactory
+from mcp_server.core import Setting
+from mcp_server.server import GravitinoMCPServer
+from tests.unit.tools.mock_operation import MockOperation
+
+
+class TestTopicTool(unittest.TestCase):
+    def setUp(self):
+        RESTClientFactory.set_rest_client(MockOperation)
+        server = GravitinoMCPServer(Setting("mock_metalake"))
+        self.mcp = server.mcp
+
+    def test_list_topics(self):
+        async def _test_list_topics(mcp_server):
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "list_of_topics",
+                    {"catalog_name": "mock", "schema_name": "mock"},
+                )
+                self.assertEqual("mock_topics", result.content[0].text)
+
+        asyncio.run(_test_list_topics(self.mcp))
+
+    def test_load_topic(self):
+        async def _test_load_topic(mcp_server):
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "load_topic",
+                    {
+                        "catalog_name": "mock",
+                        "schema_name": "mock",
+                        "topic_name": "mock",
+                    },
+                )
+                self.assertEqual("mock_topic", result.content[0].text)
+
+        asyncio.run(_test_load_topic(self.mcp))

Reply via email to