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

mchades 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 fc1c4a2ede [#11567][#11574] feat(mcp-server): write tools for tag, 
catalog, schema, table, model, topic, fileset (#11621)
fc1c4a2ede is described below

commit fc1c4a2edee83fd7d9228ea390b951e4652595ee
Author: Qi Yu <[email protected]>
AuthorDate: Thu Jul 9 13:02:24 2026 +0800

    [#11567][#11574] feat(mcp-server): write tools for tag, catalog, schema, 
table, model, topic, fileset (#11621)
    
    ### What changes were proposed in this pull request?
    
    This is the **write/modify** PR (second in the stack), adding governed
    write tools to the MCP server.
    
    - **#11567**: enable tag write tools (create/alter/delete/associate),
    protected by Gravitino authorization.
    - **#11574**: implement write operations for catalog, schema, table,
    model, topic, and fileset (create/alter/drop, plus register/link/delete
    for models).
    
    Write tools are exposed by default; authorization is enforced by
    Gravitino core.
    
    > **Stacked on #11622.** This PR depends on the
    authentication/authorization/audit
    > foundation in #11622. Until #11622 merges, this PR's diff will also
    show those
    > commits; review after #11622 or focus on the two write commits here.
    Merge order:
    > #11622 first, then this PR.
    
    ### Why are the changes needed?
    
    The MCP server previously exposed only read-only tools. These write
    tools let agents manage Gravitino metadata through MCP, with per-request
    authorization (from #11622) ensuring each principal's permissions are
    enforced.
    
    Fix: #11567
    Fix: #11574
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. New MCP write tools for tag, catalog, schema, table, model, topic,
    and fileset.
    
    ### How was this patch tested?
    
    `./gradlew :mcp-server:formatCheckPython :mcp-server:pylint` clean
    (pylint 10.00/10) and 123 unit tests pass.
---
 docs/gravitino-mcp-server.md                       |  31 ++-
 mcp-server/mcp_server/client/catalog_operation.py  |  24 ++
 mcp-server/mcp_server/client/fileset_operation.py  |  31 +++
 mcp-server/mcp_server/client/model_operation.py    |  78 +++++++
 .../plain/plain_rest_client_catalog_operation.py   |  46 ++++
 .../plain/plain_rest_client_fileset_operation.py   |  68 +++++-
 .../plain/plain_rest_client_model_operation.py     | 147 ++++++++++++-
 .../plain/plain_rest_client_schema_operation.py    |  32 +++
 .../plain/plain_rest_client_table_operation.py     |  63 ++++++
 .../plain/plain_rest_client_tag_operation.py       |   9 +-
 .../plain/plain_rest_client_topic_operation.py     |  53 ++++-
 mcp-server/mcp_server/client/plain/utils.py        |   6 +
 mcp-server/mcp_server/client/schema_operation.py   |  18 ++
 mcp-server/mcp_server/client/table_operation.py    |  33 +++
 mcp-server/mcp_server/client/tag_operation.py      |   4 +-
 mcp-server/mcp_server/client/topic_operation.py    |  28 +++
 mcp-server/mcp_server/tools/catalog.py             | 109 +++++++++
 mcp-server/mcp_server/tools/fileset.py             | 104 ++++++++-
 mcp-server/mcp_server/tools/model.py               | 243 +++++++++++++++++++++
 mcp-server/mcp_server/tools/schema.py              |  79 +++++++
 mcp-server/mcp_server/tools/table.py               | 124 +++++++++++
 mcp-server/mcp_server/tools/tag.py                 |  18 +-
 mcp-server/mcp_server/tools/topic.py               |  83 +++++++
 mcp-server/tests/integration/test_authz_e2e.py     |   9 -
 mcp-server/tests/unit/client/test_url_encoding.py  | 139 +++++++++++-
 mcp-server/tests/unit/tools/mock_operation.py      | 182 +++++++++++++++
 mcp-server/tests/unit/tools/test_catalog.py        |  87 ++++++++
 mcp-server/tests/unit/tools/test_fileset.py        |  84 +++++++
 mcp-server/tests/unit/tools/test_model.py          | 162 ++++++++++++++
 mcp-server/tests/unit/tools/test_schema.py         |  69 ++++++
 mcp-server/tests/unit/tools/test_table.py          | 104 +++++++++
 mcp-server/tests/unit/tools/test_tag.py            |  52 ++++-
 mcp-server/tests/unit/tools/test_topic.py          |  56 +++++
 33 files changed, 2319 insertions(+), 56 deletions(-)

diff --git a/docs/gravitino-mcp-server.md b/docs/gravitino-mcp-server.md
index 1940e6d893..7d09a53730 100644
--- a/docs/gravitino-mcp-server.md
+++ b/docs/gravitino-mcp-server.md
@@ -64,20 +64,44 @@ Gravitino MCP server supports the following tools, and you 
could export tool by
 | Tool name                           | Description                            
                                        | Tag          | Since version |
 
|-------------------------------------|--------------------------------------------------------------------------------|--------------|---------------|
 | `get_list_of_catalogs`              | Retrieve a list of all catalogs in the 
system.                                 | `catalog`    | 1.0.0         |
+| `create_catalog`                    | Create a new catalog.                  
                                        | `catalog`    | 2.0.0         |
+| `alter_catalog`                     | Alter an existing catalog.             
                                        | `catalog`    | 2.0.0         |
+| `drop_catalog`                      | Drop a catalog.                        
                                        | `catalog`    | 2.0.0         |
+| `set_catalog_in_use`                | Enable or disable a catalog.           
                                        | `catalog`    | 2.0.0         |
 | `get_list_of_schemas`               | Retrieve a list of schemas belonging 
to a specific catalog.                    | `schema`     | 1.0.0         |
+| `create_schema`                     | Create a new schema.                   
                                        | `schema`     | 2.0.0         |
+| `alter_schema`                      | Alter an existing schema.              
                                        | `schema`     | 2.0.0         |
+| `drop_schema`                       | Drop a schema.                         
                                        | `schema`     | 2.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         |
+| `create_table`                      | Create a new table.                    
                                        | `table`      | 2.0.0         |
+| `alter_table`                       | Alter an existing table.               
                                        | `table`      | 2.0.0         |
+| `drop_table`                        | Drop a table.                          
                                        | `table`      | 2.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         | 
+| `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`                | 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         |
+| `register_model`                    | Register a new model.                  
                                        | `model`      | 2.0.0         |
+| `delete_model`                      | Delete a model.                        
                                        | `model`      | 2.0.0         |
+| `link_model_version`                | Link a new version to a model.         
                                        | `model`      | 2.0.0         |
+| `delete_model_version`              | Delete a model version.                
                                        | `model`      | 2.0.0         |
+| `delete_model_version_by_alias`     | Delete a model version by one of its 
aliases.                                  | `model`      | 2.0.0         |
+| `alter_model`                       | Alter an existing model.               
                                        | `model`      | 2.0.0         |
+| `alter_model_version`               | Alter a model version.                 
                                        | `model`      | 2.0.0         |
+| `alter_model_version_by_alias`      | Alter a model version by one of its 
aliases.                                   | `model`      | 2.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         |
+| `create_topic`                      | Create a new topic.                    
                                        | `topic`      | 2.0.0         |
+| `alter_topic`                       | Alter an existing topic.               
                                        | `topic`      | 2.0.0         |
+| `delete_topic`                      | Delete a topic.                        
                                        | `topic`      | 2.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         |
+| `create_fileset`                    | Create a new fileset.                  
                                        | `fileset`    | 2.0.0         |
+| `alter_fileset`                     | Alter an existing fileset.             
                                        | `fileset`    | 2.0.0         |
+| `drop_fileset`                      | Drop a fileset.                        
                                        | `fileset`    | 2.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         |
@@ -90,6 +114,9 @@ Gravitino MCP server supports the following tools, and you 
could export tool by
 | `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         |
+| `create_tag`                        | Create a new tag.                      
                                        | `tag`        | 2.0.0         |
+| `alter_tag`                         | Alter an existing tag.                 
                                        | `tag`        | 2.0.0         |
+| `delete_tag`                        | Delete a tag.                          
                                        | `tag`        | 2.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         |
diff --git a/mcp-server/mcp_server/client/catalog_operation.py 
b/mcp-server/mcp_server/client/catalog_operation.py
index 08cf7a5b97..9849f9e439 100644
--- a/mcp-server/mcp_server/client/catalog_operation.py
+++ b/mcp-server/mcp_server/client/catalog_operation.py
@@ -32,3 +32,27 @@ class CatalogOperation(ABC):
             str: JSON-formatted string containing catalog information.
         """
         pass
+
+    @abstractmethod
+    # pylint: disable=too-many-positional-arguments
+    async def create_catalog(
+        self,
+        name: str,
+        catalog_type: str,
+        provider: str,
+        comment: str,
+        properties: dict,
+    ) -> str:
+        pass
+
+    @abstractmethod
+    async def alter_catalog(self, catalog_name: str, updates: list) -> str:
+        pass
+
+    @abstractmethod
+    async def drop_catalog(self, catalog_name: str, force: bool) -> str:
+        pass
+
+    @abstractmethod
+    async def set_catalog_in_use(self, catalog_name: str, in_use: bool) -> str:
+        pass
diff --git a/mcp-server/mcp_server/client/fileset_operation.py 
b/mcp-server/mcp_server/client/fileset_operation.py
index a158c11cda..fd1922ab74 100644
--- a/mcp-server/mcp_server/client/fileset_operation.py
+++ b/mcp-server/mcp_server/client/fileset_operation.py
@@ -80,3 +80,34 @@ class FilesetOperation(ABC):
             str: JSON-formatted string containing list of files in the fileset
         """
         pass
+
+    @abstractmethod
+    # pylint: disable=too-many-positional-arguments
+    async def create_fileset(
+        self,
+        catalog_name: str,
+        schema_name: str,
+        name: str,
+        fileset_type: str,
+        storage_location: str,
+        comment: str,
+        properties: dict,
+        storage_locations: dict = None,
+    ) -> str:
+        pass
+
+    @abstractmethod
+    async def alter_fileset(
+        self,
+        catalog_name: str,
+        schema_name: str,
+        fileset_name: str,
+        updates: list,
+    ) -> str:
+        pass
+
+    @abstractmethod
+    async def drop_fileset(
+        self, catalog_name: str, schema_name: str, fileset_name: str
+    ) -> str:
+        pass
diff --git a/mcp-server/mcp_server/client/model_operation.py 
b/mcp-server/mcp_server/client/model_operation.py
index c63eda326a..45d5b520e3 100644
--- a/mcp-server/mcp_server/client/model_operation.py
+++ b/mcp-server/mcp_server/client/model_operation.py
@@ -106,3 +106,81 @@ class ModelOperation(ABC):
             str: JSON-formatted string containing full model version metadata
         """
         pass
+
+    @abstractmethod
+    # pylint: disable=too-many-positional-arguments
+    async def register_model(
+        self,
+        catalog_name: str,
+        schema_name: str,
+        name: str,
+        comment: str,
+        properties: dict,
+    ) -> str:
+        pass
+
+    @abstractmethod
+    async def delete_model(
+        self, catalog_name: str, schema_name: str, model_name: str
+    ) -> str:
+        pass
+
+    @abstractmethod
+    # pylint: disable=too-many-positional-arguments
+    async def link_model_version(
+        self,
+        catalog_name: str,
+        schema_name: str,
+        model_name: str,
+        uri: str,
+        aliases: list,
+        comment: str,
+        properties: dict,
+    ) -> str:
+        pass
+
+    @abstractmethod
+    async def delete_model_version(
+        self, catalog_name: str, schema_name: str, model_name: str, version: 
int
+    ) -> str:
+        pass
+
+    @abstractmethod
+    async def delete_model_version_by_alias(
+        self, catalog_name: str, schema_name: str, model_name: str, alias: str
+    ) -> str:
+        pass
+
+    @abstractmethod
+    async def alter_model(
+        self,
+        catalog_name: str,
+        schema_name: str,
+        model_name: str,
+        updates: list,
+    ) -> str:
+        pass
+
+    @abstractmethod
+    # pylint: disable=too-many-positional-arguments
+    async def alter_model_version(
+        self,
+        catalog_name: str,
+        schema_name: str,
+        model_name: str,
+        version: int,
+        updates: list,
+    ) -> str:
+        pass
+
+    @abstractmethod
+    # pylint: disable=too-many-positional-arguments
+    async def alter_model_version_by_alias(
+        self,
+        catalog_name: str,
+        schema_name: str,
+        model_name: str,
+        alias: str,
+        updates: list,
+    ) -> str:
+        pass
diff --git 
a/mcp-server/mcp_server/client/plain/plain_rest_client_catalog_operation.py 
b/mcp-server/mcp_server/client/plain/plain_rest_client_catalog_operation.py
index 843cf3bfe7..492b3ca9d9 100644
--- a/mcp-server/mcp_server/client/plain/plain_rest_client_catalog_operation.py
+++ b/mcp-server/mcp_server/client/plain/plain_rest_client_catalog_operation.py
@@ -21,6 +21,7 @@ from mcp_server.client import CatalogOperation
 from mcp_server.client.plain.utils import (
     encode_path_segment,
     extract_content_from_response,
+    extract_response,
 )
 
 
@@ -34,3 +35,48 @@ class PlainRESTClientCatalogOperation(CatalogOperation):
             
f"/api/metalakes/{encode_path_segment(self.metalake_name)}/catalogs?details=true"
         )
         return extract_content_from_response(response, "catalogs", [])
+
+    # pylint: disable=too-many-positional-arguments
+    async def create_catalog(
+        self,
+        name: str,
+        catalog_type: str,
+        provider: str,
+        comment: str,
+        properties: dict,
+    ) -> str:
+        response = await self.rest_client.post(
+            
f"/api/metalakes/{encode_path_segment(self.metalake_name)}/catalogs",
+            json={
+                "name": name,
+                "type": catalog_type,
+                "provider": provider,
+                "comment": comment,
+                "properties": properties,
+            },
+        )
+        return extract_content_from_response(response, "catalog", {})
+
+    async def alter_catalog(self, catalog_name: str, updates: list) -> str:
+        response = await self.rest_client.put(
+            f"/api/metalakes/{encode_path_segment(self.metalake_name)}"
+            f"/catalogs/{encode_path_segment(catalog_name)}",
+            json={"updates": updates},
+        )
+        return extract_content_from_response(response, "catalog", {})
+
+    async def drop_catalog(self, catalog_name: str, force: bool) -> str:
+        response = await self.rest_client.delete(
+            f"/api/metalakes/{encode_path_segment(self.metalake_name)}"
+            f"/catalogs/{encode_path_segment(catalog_name)}",
+            params={"force": force},
+        )
+        return extract_content_from_response(response, "dropped", False)
+
+    async def set_catalog_in_use(self, catalog_name: str, in_use: bool) -> str:
+        response = await self.rest_client.patch(
+            f"/api/metalakes/{encode_path_segment(self.metalake_name)}"
+            f"/catalogs/{encode_path_segment(catalog_name)}",
+            json={"inUse": in_use},
+        )
+        return extract_response(response)
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 4c81eece1d..de95819843 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
@@ -16,7 +16,10 @@
 # under the License.
 
 from mcp_server.client.fileset_operation import FilesetOperation
-from mcp_server.client.plain.utils import encode_path_segment
+from mcp_server.client.plain.utils import (
+    encode_path_segment,
+    extract_content_from_response,
+)
 
 
 class PlainRESTClientFilesetOperation(FilesetOperation):
@@ -32,7 +35,7 @@ class PlainRESTClientFilesetOperation(FilesetOperation):
             f"/catalogs/{encode_path_segment(catalog_name)}"
             f"/schemas/{encode_path_segment(schema_name)}/filesets"
         )
-        return response.json().get("identifiers", [])
+        return extract_content_from_response(response, "identifiers", [])
 
     async def load_fileset(
         self, catalog_name: str, schema_name: str, fileset_name: str
@@ -43,7 +46,7 @@ class PlainRESTClientFilesetOperation(FilesetOperation):
             f"/schemas/{encode_path_segment(schema_name)}"
             f"/filesets/{encode_path_segment(fileset_name)}"
         )
-        return response.json().get("fileset", {})
+        return extract_content_from_response(response, "fileset", {})
 
     # pylint: disable=too-many-positional-arguments
     async def list_files_in_fileset(
@@ -61,4 +64,61 @@ class PlainRESTClientFilesetOperation(FilesetOperation):
             f"/filesets/{encode_path_segment(fileset_name)}/files",
             params={"sub_path": sub_path, "location_name": location_name},
         )
-        return response.json().get("files", [])
+        return extract_content_from_response(response, "files", [])
+
+    # pylint: disable=too-many-positional-arguments
+    async def create_fileset(
+        self,
+        catalog_name: str,
+        schema_name: str,
+        name: str,
+        fileset_type: str,
+        storage_location: str,
+        comment: str,
+        properties: dict,
+        storage_locations: dict = None,
+    ) -> str:
+        request = {
+            "name": name,
+            "type": fileset_type,
+            "comment": comment,
+            "properties": properties,
+        }
+        if storage_location:
+            request["storageLocation"] = storage_location
+        if storage_locations:
+            request["storageLocations"] = storage_locations
+        response = await self.rest_client.post(
+            f"/api/metalakes/{encode_path_segment(self.metalake_name)}"
+            f"/catalogs/{encode_path_segment(catalog_name)}"
+            f"/schemas/{encode_path_segment(schema_name)}/filesets",
+            json=request,
+        )
+        return extract_content_from_response(response, "fileset", {})
+
+    async def alter_fileset(
+        self,
+        catalog_name: str,
+        schema_name: str,
+        fileset_name: str,
+        updates: list,
+    ) -> str:
+        response = await self.rest_client.put(
+            f"/api/metalakes/{encode_path_segment(self.metalake_name)}"
+            f"/catalogs/{encode_path_segment(catalog_name)}"
+            f"/schemas/{encode_path_segment(schema_name)}"
+            f"/filesets/{encode_path_segment(fileset_name)}",
+            json={"updates": updates},
+        )
+        return extract_content_from_response(response, "fileset", {})
+
+    async def drop_fileset(
+        self, catalog_name: str, schema_name: str, fileset_name: str
+    ) -> str:
+        response = await self.rest_client.delete(
+            f"/api/metalakes/{encode_path_segment(self.metalake_name)}"
+            f"/catalogs/{encode_path_segment(catalog_name)}"
+            f"/schemas/{encode_path_segment(schema_name)}"
+            f"/filesets/{encode_path_segment(fileset_name)}"
+        )
+        return extract_content_from_response(response, "dropped", False)
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 7fce30bd91..466dcb423c 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
@@ -16,7 +16,11 @@
 # under the License.
 
 from mcp_server.client import ModelOperation
-from mcp_server.client.plain.utils import encode_path_segment
+from mcp_server.client.plain.utils import (
+    encode_path_segment,
+    extract_content_from_response,
+    extract_response,
+)
 
 
 class PlainRESTClientModelOperation(ModelOperation):
@@ -34,7 +38,7 @@ class PlainRESTClientModelOperation(ModelOperation):
             f"/catalogs/{encode_path_segment(catalog_name)}"
             f"/schemas/{encode_path_segment(schema_name)}/models"
         )
-        return response.json().get("identifiers", [])
+        return extract_content_from_response(response, "identifiers", [])
 
     async def load_model(
         self, catalog_name: str, schema_name: str, model_name: str
@@ -45,7 +49,7 @@ class PlainRESTClientModelOperation(ModelOperation):
             f"/schemas/{encode_path_segment(schema_name)}"
             f"/models/{encode_path_segment(model_name)}"
         )
-        return response.json().get("model", {})
+        return extract_content_from_response(response, "model", {})
 
     async def list_model_versions(
         self, catalog_name: str, schema_name: str, model_name: str
@@ -56,7 +60,7 @@ class PlainRESTClientModelOperation(ModelOperation):
             f"/schemas/{encode_path_segment(schema_name)}"
             f"/models/{encode_path_segment(model_name)}/versions?details=true"
         )
-        return response.json().get("infos", [])
+        return extract_content_from_response(response, "infos", [])
 
     async def load_model_version(
         self, catalog_name: str, schema_name: str, model_name: str, version: 
int
@@ -68,7 +72,7 @@ class PlainRESTClientModelOperation(ModelOperation):
             f"/models/{encode_path_segment(model_name)}"
             f"/versions/{encode_path_segment(version)}"
         )
-        return response.json().get("modelVersion", {})
+        return extract_content_from_response(response, "modelVersion", {})
 
     async def load_model_version_by_alias(
         self, catalog_name: str, schema_name: str, model_name: str, alias: str
@@ -80,4 +84,135 @@ class PlainRESTClientModelOperation(ModelOperation):
             f"/models/{encode_path_segment(model_name)}"
             f"/aliases/{encode_path_segment(alias)}"
         )
-        return response.json().get("modelVersion", {})
+        return extract_content_from_response(response, "modelVersion", {})
+
+    # pylint: disable=too-many-positional-arguments
+    async def register_model(
+        self,
+        catalog_name: str,
+        schema_name: str,
+        name: str,
+        comment: str,
+        properties: dict,
+    ) -> str:
+        response = await self.rest_client.post(
+            f"/api/metalakes/{encode_path_segment(self.metalake_name)}"
+            f"/catalogs/{encode_path_segment(catalog_name)}"
+            f"/schemas/{encode_path_segment(schema_name)}/models",
+            json={"name": name, "comment": comment, "properties": properties},
+        )
+        return extract_content_from_response(response, "model", {})
+
+    async def delete_model(
+        self, catalog_name: str, schema_name: str, model_name: str
+    ) -> str:
+        response = await self.rest_client.delete(
+            f"/api/metalakes/{encode_path_segment(self.metalake_name)}"
+            f"/catalogs/{encode_path_segment(catalog_name)}"
+            f"/schemas/{encode_path_segment(schema_name)}"
+            f"/models/{encode_path_segment(model_name)}"
+        )
+        return extract_content_from_response(response, "dropped", False)
+
+    # pylint: disable=too-many-positional-arguments
+    async def link_model_version(
+        self,
+        catalog_name: str,
+        schema_name: str,
+        model_name: str,
+        uri: str,
+        aliases: list,
+        comment: str,
+        properties: dict,
+    ) -> str:
+        response = await self.rest_client.post(
+            f"/api/metalakes/{encode_path_segment(self.metalake_name)}"
+            f"/catalogs/{encode_path_segment(catalog_name)}"
+            f"/schemas/{encode_path_segment(schema_name)}"
+            f"/models/{encode_path_segment(model_name)}/versions",
+            json={
+                "uri": uri,
+                "aliases": aliases,
+                "comment": comment,
+                "properties": properties,
+            },
+        )
+        return extract_response(response)
+
+    async def delete_model_version(
+        self, catalog_name: str, schema_name: str, model_name: str, version: 
int
+    ) -> str:
+        response = await self.rest_client.delete(
+            f"/api/metalakes/{encode_path_segment(self.metalake_name)}"
+            f"/catalogs/{encode_path_segment(catalog_name)}"
+            f"/schemas/{encode_path_segment(schema_name)}"
+            f"/models/{encode_path_segment(model_name)}"
+            f"/versions/{encode_path_segment(version)}"
+        )
+        return extract_content_from_response(response, "dropped", False)
+
+    async def delete_model_version_by_alias(
+        self, catalog_name: str, schema_name: str, model_name: str, alias: str
+    ) -> str:
+        response = await self.rest_client.delete(
+            f"/api/metalakes/{encode_path_segment(self.metalake_name)}"
+            f"/catalogs/{encode_path_segment(catalog_name)}"
+            f"/schemas/{encode_path_segment(schema_name)}"
+            f"/models/{encode_path_segment(model_name)}"
+            f"/aliases/{encode_path_segment(alias)}"
+        )
+        return extract_content_from_response(response, "dropped", False)
+
+    async def alter_model(
+        self,
+        catalog_name: str,
+        schema_name: str,
+        model_name: str,
+        updates: list,
+    ) -> str:
+        response = await self.rest_client.put(
+            f"/api/metalakes/{encode_path_segment(self.metalake_name)}"
+            f"/catalogs/{encode_path_segment(catalog_name)}"
+            f"/schemas/{encode_path_segment(schema_name)}"
+            f"/models/{encode_path_segment(model_name)}",
+            json={"updates": updates},
+        )
+        return extract_content_from_response(response, "model", {})
+
+    # pylint: disable=too-many-positional-arguments
+    async def alter_model_version(
+        self,
+        catalog_name: str,
+        schema_name: str,
+        model_name: str,
+        version: int,
+        updates: list,
+    ) -> str:
+        response = await self.rest_client.put(
+            f"/api/metalakes/{encode_path_segment(self.metalake_name)}"
+            f"/catalogs/{encode_path_segment(catalog_name)}"
+            f"/schemas/{encode_path_segment(schema_name)}"
+            f"/models/{encode_path_segment(model_name)}"
+            f"/versions/{encode_path_segment(version)}",
+            json={"updates": updates},
+        )
+        return extract_content_from_response(response, "modelVersion", {})
+
+    # pylint: disable=too-many-positional-arguments
+    async def alter_model_version_by_alias(
+        self,
+        catalog_name: str,
+        schema_name: str,
+        model_name: str,
+        alias: str,
+        updates: list,
+    ) -> str:
+        response = await self.rest_client.put(
+            f"/api/metalakes/{encode_path_segment(self.metalake_name)}"
+            f"/catalogs/{encode_path_segment(catalog_name)}"
+            f"/schemas/{encode_path_segment(schema_name)}"
+            f"/models/{encode_path_segment(model_name)}"
+            f"/aliases/{encode_path_segment(alias)}",
+            json={"updates": updates},
+        )
+        return extract_content_from_response(response, "modelVersion", {})
diff --git 
a/mcp-server/mcp_server/client/plain/plain_rest_client_schema_operation.py 
b/mcp-server/mcp_server/client/plain/plain_rest_client_schema_operation.py
index 4c2c76ed44..fee6fe5b10 100644
--- a/mcp-server/mcp_server/client/plain/plain_rest_client_schema_operation.py
+++ b/mcp-server/mcp_server/client/plain/plain_rest_client_schema_operation.py
@@ -35,3 +35,35 @@ class PlainRESTClientSchemaOperation(SchemaOperation):
             f"/catalogs/{encode_path_segment(catalog_name)}/schemas"
         )
         return extract_content_from_response(response, "identifiers", [])
+
+    async def create_schema(
+        self, catalog_name: str, name: str, comment: str, properties: dict
+    ) -> str:
+        response = await self.rest_client.post(
+            f"/api/metalakes/{encode_path_segment(self.metalake_name)}"
+            f"/catalogs/{encode_path_segment(catalog_name)}/schemas",
+            json={"name": name, "comment": comment, "properties": properties},
+        )
+        return extract_content_from_response(response, "schema", {})
+
+    async def alter_schema(
+        self, catalog_name: str, schema_name: str, updates: list
+    ) -> str:
+        response = await self.rest_client.put(
+            f"/api/metalakes/{encode_path_segment(self.metalake_name)}"
+            f"/catalogs/{encode_path_segment(catalog_name)}"
+            f"/schemas/{encode_path_segment(schema_name)}",
+            json={"updates": updates},
+        )
+        return extract_content_from_response(response, "schema", {})
+
+    async def drop_schema(
+        self, catalog_name: str, schema_name: str, cascade: bool
+    ) -> str:
+        response = await self.rest_client.delete(
+            f"/api/metalakes/{encode_path_segment(self.metalake_name)}"
+            f"/catalogs/{encode_path_segment(catalog_name)}"
+            f"/schemas/{encode_path_segment(schema_name)}",
+            params={"cascade": cascade},
+        )
+        return extract_content_from_response(response, "dropped", False)
diff --git 
a/mcp-server/mcp_server/client/plain/plain_rest_client_table_operation.py 
b/mcp-server/mcp_server/client/plain/plain_rest_client_table_operation.py
index e1bee62cab..26ace2f05e 100644
--- a/mcp-server/mcp_server/client/plain/plain_rest_client_table_operation.py
+++ b/mcp-server/mcp_server/client/plain/plain_rest_client_table_operation.py
@@ -50,3 +50,66 @@ class PlainRESTClientTableOperation(TableOperation):
             f"/tables/{encode_path_segment(table_name)}"
         )
         return extract_content_from_response(response, "table", {})
+
+    # pylint: disable=too-many-positional-arguments
+    async def create_table(
+        self,
+        catalog_name: str,
+        schema_name: str,
+        name: str,
+        comment: str,
+        columns: list,
+        properties: dict,
+        partitioning: list = None,
+        distribution: dict = None,
+        sort_orders: list = None,
+        indexes: list = None,
+    ) -> str:
+        request = {
+            "name": name,
+            "comment": comment,
+            "columns": columns,
+            "properties": properties,
+        }
+        optional_fields = {
+            "partitioning": partitioning,
+            "distribution": distribution,
+            "sortOrders": sort_orders,
+            "indexes": indexes,
+        }
+        request.update({k: v for k, v in optional_fields.items() if v})
+        response = await self.rest_client.post(
+            f"/api/metalakes/{encode_path_segment(self.metalake_name)}"
+            f"/catalogs/{encode_path_segment(catalog_name)}"
+            f"/schemas/{encode_path_segment(schema_name)}/tables",
+            json=request,
+        )
+        return extract_content_from_response(response, "table", {})
+
+    async def alter_table(
+        self,
+        catalog_name: str,
+        schema_name: str,
+        table_name: str,
+        updates: list,
+    ) -> str:
+        response = await self.rest_client.put(
+            f"/api/metalakes/{encode_path_segment(self.metalake_name)}"
+            f"/catalogs/{encode_path_segment(catalog_name)}"
+            f"/schemas/{encode_path_segment(schema_name)}"
+            f"/tables/{encode_path_segment(table_name)}",
+            json={"updates": updates},
+        )
+        return extract_content_from_response(response, "table", {})
+
+    async def drop_table(
+        self, catalog_name: str, schema_name: str, table_name: str, purge: bool
+    ) -> str:
+        response = await self.rest_client.delete(
+            f"/api/metalakes/{encode_path_segment(self.metalake_name)}"
+            f"/catalogs/{encode_path_segment(catalog_name)}"
+            f"/schemas/{encode_path_segment(schema_name)}"
+            f"/tables/{encode_path_segment(table_name)}",
+            params={"purge": purge},
+        )
+        return extract_content_from_response(response, "dropped", False)
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 b5fabf9cb1..ab2b20c04b 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
@@ -15,7 +15,6 @@
 # specific language governing permissions and limitations
 # under the License.
 
-from mcp_server.client.plain.exception import GravitinoException
 from mcp_server.client.plain.utils import (
     encode_path_segment,
     extract_content_from_response,
@@ -62,15 +61,11 @@ class PlainRESTClientTagOperation(TagOperation):
         )
         return extract_content_from_response(response, "tag", {})
 
-    async def delete_tag(self, name: str) -> None:
+    async def delete_tag(self, name: str) -> str:
         response = await self.rest_client.delete(
             
f"/api/metalakes/{encode_path_segment(self.metalake_name)}/tags/{encode_path_segment(name)}"
         )
-        if response.status_code != 200:
-            raise GravitinoException(
-                f"Failed to delete tag {name}: {response.text}"
-            )
-        return None
+        return extract_content_from_response(response, "dropped", False)
 
     async def associate_tag_with_metadata(
         self,
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 ff04720197..68279e9adf 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
@@ -15,7 +15,10 @@
 # specific language governing permissions and limitations
 # under the License.
 
-from mcp_server.client.plain.utils import encode_path_segment
+from mcp_server.client.plain.utils import (
+    encode_path_segment,
+    extract_content_from_response,
+)
 from mcp_server.client.topic_operation import TopicOperation
 
 
@@ -34,7 +37,7 @@ class PlainRESTClientTopicOperation(TopicOperation):
             f"/catalogs/{encode_path_segment(catalog_name)}"
             f"/schemas/{encode_path_segment(schema_name)}/topics"
         )
-        return response.json().get("identifiers", [])
+        return extract_content_from_response(response, "identifiers", [])
 
     async def load_topic(
         self, catalog_name: str, schema_name: str, topic_name: str
@@ -45,4 +48,48 @@ class PlainRESTClientTopicOperation(TopicOperation):
             f"/schemas/{encode_path_segment(schema_name)}"
             f"/topics/{encode_path_segment(topic_name)}"
         )
-        return response.json().get("topic", {})
+        return extract_content_from_response(response, "topic", {})
+
+    # pylint: disable=too-many-positional-arguments
+    async def create_topic(
+        self,
+        catalog_name: str,
+        schema_name: str,
+        name: str,
+        comment: str,
+        properties: dict,
+    ) -> str:
+        response = await self.rest_client.post(
+            f"/api/metalakes/{encode_path_segment(self.metalake_name)}"
+            f"/catalogs/{encode_path_segment(catalog_name)}"
+            f"/schemas/{encode_path_segment(schema_name)}/topics",
+            json={"name": name, "comment": comment, "properties": properties},
+        )
+        return extract_content_from_response(response, "topic", {})
+
+    async def alter_topic(
+        self,
+        catalog_name: str,
+        schema_name: str,
+        topic_name: str,
+        updates: list,
+    ) -> str:
+        response = await self.rest_client.put(
+            f"/api/metalakes/{encode_path_segment(self.metalake_name)}"
+            f"/catalogs/{encode_path_segment(catalog_name)}"
+            f"/schemas/{encode_path_segment(schema_name)}"
+            f"/topics/{encode_path_segment(topic_name)}",
+            json={"updates": updates},
+        )
+        return extract_content_from_response(response, "topic", {})
+
+    async def delete_topic(
+        self, catalog_name: str, schema_name: str, topic_name: str
+    ) -> str:
+        response = await self.rest_client.delete(
+            f"/api/metalakes/{encode_path_segment(self.metalake_name)}"
+            f"/catalogs/{encode_path_segment(catalog_name)}"
+            f"/schemas/{encode_path_segment(schema_name)}"
+            f"/topics/{encode_path_segment(topic_name)}"
+        )
+        return extract_content_from_response(response, "dropped", False)
diff --git a/mcp-server/mcp_server/client/plain/utils.py 
b/mcp-server/mcp_server/client/plain/utils.py
index e5c998a57a..5f9b5eb41a 100644
--- a/mcp-server/mcp_server/client/plain/utils.py
+++ b/mcp-server/mcp_server/client/plain/utils.py
@@ -33,6 +33,12 @@ def extract_content_from_response(response, field: str, 
default="") -> str:
     return json.dumps(response_json.get(field, default))
 
 
+def extract_response(response) -> str:
+    response_json = response.json()
+    _handle_gravitino_exception(response_json)
+    return json.dumps(response_json)
+
+
 def _handle_gravitino_exception(response: dict):
     error_code = response.get("code", 0)
     if error_code != 0:
diff --git a/mcp-server/mcp_server/client/schema_operation.py 
b/mcp-server/mcp_server/client/schema_operation.py
index b90d7b96f2..438aa02e26 100644
--- a/mcp-server/mcp_server/client/schema_operation.py
+++ b/mcp-server/mcp_server/client/schema_operation.py
@@ -35,3 +35,21 @@ class SchemaOperation(ABC):
             str: JSON-formatted string containing catalog information.
         """
         pass
+
+    @abstractmethod
+    async def create_schema(
+        self, catalog_name: str, name: str, comment: str, properties: dict
+    ) -> str:
+        pass
+
+    @abstractmethod
+    async def alter_schema(
+        self, catalog_name: str, schema_name: str, updates: list
+    ) -> str:
+        pass
+
+    @abstractmethod
+    async def drop_schema(
+        self, catalog_name: str, schema_name: str, cascade: bool
+    ) -> str:
+        pass
diff --git a/mcp-server/mcp_server/client/table_operation.py 
b/mcp-server/mcp_server/client/table_operation.py
index 2d188ebf25..d2737bd5e0 100644
--- a/mcp-server/mcp_server/client/table_operation.py
+++ b/mcp-server/mcp_server/client/table_operation.py
@@ -55,3 +55,36 @@ class TableOperation(ABC):
             str: JSON-formatted string containing full table metadata
         """
         pass
+
+    @abstractmethod
+    # pylint: disable=too-many-positional-arguments
+    async def create_table(
+        self,
+        catalog_name: str,
+        schema_name: str,
+        name: str,
+        comment: str,
+        columns: list,
+        properties: dict,
+        partitioning: list = None,
+        distribution: dict = None,
+        sort_orders: list = None,
+        indexes: list = None,
+    ) -> str:
+        pass
+
+    @abstractmethod
+    async def alter_table(
+        self,
+        catalog_name: str,
+        schema_name: str,
+        table_name: str,
+        updates: list,
+    ) -> str:
+        pass
+
+    @abstractmethod
+    async def drop_table(
+        self, catalog_name: str, schema_name: str, table_name: str, purge: bool
+    ) -> str:
+        pass
diff --git a/mcp-server/mcp_server/client/tag_operation.py 
b/mcp-server/mcp_server/client/tag_operation.py
index afb4df7b32..43614ec497 100644
--- a/mcp-server/mcp_server/client/tag_operation.py
+++ b/mcp-server/mcp_server/client/tag_operation.py
@@ -78,7 +78,7 @@ class TagOperation(ABC):
         pass
 
     @abstractmethod
-    async def delete_tag(self, name: str) -> None:
+    async def delete_tag(self, name: str) -> str:
         """
         Delete a tag by its name.
 
@@ -86,7 +86,7 @@ class TagOperation(ABC):
             name: Name of the tag to delete
 
         Returns:
-            None
+            str: JSON-formatted string indicating whether the tag was deleted
         """
         pass
 
diff --git a/mcp-server/mcp_server/client/topic_operation.py 
b/mcp-server/mcp_server/client/topic_operation.py
index 6bc0bc94ab..7a90ae960c 100644
--- a/mcp-server/mcp_server/client/topic_operation.py
+++ b/mcp-server/mcp_server/client/topic_operation.py
@@ -53,3 +53,31 @@ class TopicOperation(ABC):
             str: JSON-formatted string containing full topic metadata
         """
         pass
+
+    @abstractmethod
+    # pylint: disable=too-many-positional-arguments
+    async def create_topic(
+        self,
+        catalog_name: str,
+        schema_name: str,
+        name: str,
+        comment: str,
+        properties: dict,
+    ) -> str:
+        pass
+
+    @abstractmethod
+    async def alter_topic(
+        self,
+        catalog_name: str,
+        schema_name: str,
+        topic_name: str,
+        updates: list,
+    ) -> str:
+        pass
+
+    @abstractmethod
+    async def delete_topic(
+        self, catalog_name: str, schema_name: str, topic_name: str
+    ) -> str:
+        pass
diff --git a/mcp-server/mcp_server/tools/catalog.py 
b/mcp-server/mcp_server/tools/catalog.py
index 50115610e8..b1b59f4015 100644
--- a/mcp-server/mcp_server/tools/catalog.py
+++ b/mcp-server/mcp_server/tools/catalog.py
@@ -108,3 +108,112 @@ def load_catalog_tools(mcp: FastMCP):
         """
         client = ctx.request_context.lifespan_context.rest_client()
         return await client.as_catalog_operation().get_list_of_catalogs()
+
+    @mcp.tool(tags={"catalog"})
+    # pylint: disable=too-many-positional-arguments
+    async def create_catalog(
+        ctx: Context,
+        name: str,
+        catalog_type: str,
+        provider: str,
+        comment: str,
+        properties: dict,
+    ) -> str:
+        """
+        Create a new catalog within the metalake.
+
+        Authorization is enforced by Gravitino: a principal without the
+        required grant receives an authorization denial.
+
+        Args:
+            ctx (Context): The request context object.
+            name (str): Name of the catalog to create.
+            catalog_type (str): Catalog type, one of "relational", "fileset",
+                "messaging", "model".
+            provider (str): Provider implementation, e.g. "hive",
+                "lakehouse-iceberg", "jdbc-postgresql", "kafka". May be empty
+                for model/fileset catalogs that have a single provider.
+            comment (str): Human-readable description.
+            properties (dict): Catalog configuration properties.
+
+        Returns:
+            str: JSON-formatted string containing the created catalog.
+        """
+        client = ctx.request_context.lifespan_context.rest_client()
+        return await client.as_catalog_operation().create_catalog(
+            name, catalog_type, provider, comment, properties
+        )
+
+    @mcp.tool(tags={"catalog"})
+    async def alter_catalog(
+        ctx: Context, catalog_name: str, updates: list
+    ) -> str:
+        """
+        Alter an existing catalog.
+
+        Args:
+            ctx (Context): The request context object.
+            catalog_name (str): Name of the catalog to alter.
+            updates (list): List of update operations. Example:
+                [
+                  {"@type": "rename", "newName": "new_catalog"},
+                  {"@type": "updateComment", "newComment": "updated"},
+                  {"@type": "setProperty", "property": "k", "value": "v"},
+                  {"@type": "removeProperty", "property": "k"}
+                ]
+
+        Returns:
+            str: JSON-formatted string containing the altered catalog.
+        """
+        client = ctx.request_context.lifespan_context.rest_client()
+        return await client.as_catalog_operation().alter_catalog(
+            catalog_name, updates
+        )
+
+    @mcp.tool(tags={"catalog"})
+    async def drop_catalog(
+        ctx: Context, catalog_name: str, force: bool = False
+    ) -> str:
+        """
+        Drop a catalog by its name.
+
+        A catalog that is in use (enabled) cannot be dropped unless `force`
+        is true; disable it first via `set_catalog_in_use` or pass
+        `force=True`.
+
+        Args:
+            ctx (Context): The request context object.
+            catalog_name (str): Name of the catalog to drop.
+            force (bool): Whether to drop the catalog even if it is in use
+                or non-empty. Defaults to False.
+
+        Returns:
+            str: JSON-formatted string indicating whether the catalog was 
dropped.
+        """
+        client = ctx.request_context.lifespan_context.rest_client()
+        return await client.as_catalog_operation().drop_catalog(
+            catalog_name, force
+        )
+
+    @mcp.tool(tags={"catalog"})
+    async def set_catalog_in_use(
+        ctx: Context, catalog_name: str, in_use: bool
+    ) -> str:
+        """
+        Enable or disable a catalog.
+
+        A disabled catalog rejects all operations except drop; a catalog must
+        be disabled (or force-dropped) before it can be dropped.
+
+        Args:
+            ctx (Context): The request context object.
+            catalog_name (str): Name of the catalog to enable or disable.
+            in_use (bool): True to enable the catalog, False to disable it.
+
+        Returns:
+            str: JSON-formatted string containing the base response.
+        """
+        client = ctx.request_context.lifespan_context.rest_client()
+        return await client.as_catalog_operation().set_catalog_in_use(
+            catalog_name, in_use
+        )
diff --git a/mcp-server/mcp_server/tools/fileset.py 
b/mcp-server/mcp_server/tools/fileset.py
index c3997ac16d..5aa7ec7cf6 100644
--- a/mcp-server/mcp_server/tools/fileset.py
+++ b/mcp-server/mcp_server/tools/fileset.py
@@ -96,7 +96,7 @@ def load_fileset_tools(mcp: FastMCP):
             catalog_name, schema_name, fileset_name
         )
 
-    # pylint:disable=too-many-positional-arguments
+    # pylint: disable=too-many-positional-arguments
     @mcp.tool(tags={"fileset"})
     async def list_files_in_fileset(
         ctx: Context,
@@ -143,3 +143,105 @@ def load_fileset_tools(mcp: FastMCP):
         return await client.as_fileset_operation().list_files_in_fileset(
             catalog_name, schema_name, fileset_name, location_name, sub_path
         )
+
+    # pylint: disable=too-many-positional-arguments
+    @mcp.tool(tags={"fileset"})
+    async def create_fileset(
+        ctx: Context,
+        catalog_name: str,
+        schema_name: str,
+        name: str,
+        fileset_type: str,
+        storage_location: str,
+        comment: str,
+        properties: dict,
+        storage_locations: dict = None,
+    ) -> str:
+        """
+        Create a new fileset within a schema.
+
+        Authorization is enforced by Gravitino: a principal without the
+        required grant receives an authorization denial.
+
+        Args:
+            ctx (Context): The request context object.
+            catalog_name (str): Name of the catalog.
+            schema_name (str): Name of the schema.
+            name (str): Name of the fileset to create.
+            fileset_type (str): Fileset type, one of "managed" or "external".
+            storage_location (str): Default storage location URI, e.g.
+                "file:/tmp/fileset1". May be empty for a managed fileset,
+                in which case the location is derived from catalog/schema
+                properties.
+            comment (str): Human-readable description.
+            properties (dict): Fileset configuration properties.
+            storage_locations (dict): Optional named storage locations for a
+                multiple-location fileset, mapping location name to URI.
+                Example: {"loc1": "s3a://bucket/fs", "loc2": "file:/tmp/fs"}
+
+        Returns:
+            str: JSON-formatted string containing the created fileset.
+        """
+        client = ctx.request_context.lifespan_context.rest_client()
+        return await client.as_fileset_operation().create_fileset(
+            catalog_name,
+            schema_name,
+            name,
+            fileset_type,
+            storage_location,
+            comment,
+            properties,
+            storage_locations,
+        )
+
+    @mcp.tool(tags={"fileset"})
+    async def alter_fileset(
+        ctx: Context,
+        catalog_name: str,
+        schema_name: str,
+        fileset_name: str,
+        updates: list,
+    ) -> str:
+        """
+        Alter an existing fileset.
+
+        Args:
+            ctx (Context): The request context object.
+            catalog_name (str): Name of the catalog.
+            schema_name (str): Name of the schema.
+            fileset_name (str): Name of the fileset to alter.
+            updates (list): List of update operations. Example:
+                [
+                  {"@type": "rename", "newName": "new_fileset"},
+                  {"@type": "updateComment", "newComment": "updated"},
+                  {"@type": "setProperty", "property": "k", "value": "v"}
+                ]
+
+        Returns:
+            str: JSON-formatted string containing the altered fileset.
+        """
+        client = ctx.request_context.lifespan_context.rest_client()
+        return await client.as_fileset_operation().alter_fileset(
+            catalog_name, schema_name, fileset_name, updates
+        )
+
+    @mcp.tool(tags={"fileset"})
+    async def drop_fileset(
+        ctx: Context, catalog_name: str, schema_name: str, fileset_name: str
+    ) -> str:
+        """
+        Drop a fileset by its name.
+
+        Args:
+            ctx (Context): The request context object.
+            catalog_name (str): Name of the catalog.
+            schema_name (str): Name of the schema.
+            fileset_name (str): Name of the fileset to drop.
+
+        Returns:
+            str: JSON-formatted string indicating whether the fileset was 
dropped.
+        """
+        client = ctx.request_context.lifespan_context.rest_client()
+        return await client.as_fileset_operation().drop_fileset(
+            catalog_name, schema_name, fileset_name
+        )
diff --git a/mcp-server/mcp_server/tools/model.py 
b/mcp-server/mcp_server/tools/model.py
index 4e5f21fdb5..064f16c70c 100644
--- a/mcp-server/mcp_server/tools/model.py
+++ b/mcp-server/mcp_server/tools/model.py
@@ -230,3 +230,246 @@ def load_model_tools(mcp: FastMCP):
         return await client.as_model_operation().load_model_version_by_alias(
             catalog_name, schema_name, model_name, alias
         )
+
+    @mcp.tool(tags={"model"})
+    # pylint: disable=too-many-positional-arguments
+    async def register_model(
+        ctx: Context,
+        catalog_name: str,
+        schema_name: str,
+        name: str,
+        comment: str,
+        properties: dict,
+    ) -> str:
+        """
+        Register a new model within a schema.
+
+        Authorization is enforced by Gravitino: a principal without the
+        required grant receives an authorization denial.
+
+        Args:
+            ctx (Context): The request context object.
+            catalog_name (str): Name of the catalog.
+            schema_name (str): Name of the schema.
+            name (str): Name of the model to register.
+            comment (str): Human-readable description.
+            properties (dict): Model configuration properties.
+
+        Returns:
+            str: JSON-formatted string containing the registered model.
+        """
+        client = ctx.request_context.lifespan_context.rest_client()
+        return await client.as_model_operation().register_model(
+            catalog_name, schema_name, name, comment, properties
+        )
+
+    @mcp.tool(tags={"model"})
+    async def delete_model(
+        ctx: Context, catalog_name: str, schema_name: str, model_name: str
+    ) -> str:
+        """
+        Delete a model by its name.
+
+        Args:
+            ctx (Context): The request context object.
+            catalog_name (str): Name of the catalog.
+            schema_name (str): Name of the schema.
+            model_name (str): Name of the model to delete.
+
+        Returns:
+            str: JSON-formatted string indicating whether the model was 
deleted.
+        """
+        client = ctx.request_context.lifespan_context.rest_client()
+        return await client.as_model_operation().delete_model(
+            catalog_name, schema_name, model_name
+        )
+
+    # pylint: disable=too-many-positional-arguments
+    @mcp.tool(tags={"model"})
+    async def link_model_version(
+        ctx: Context,
+        catalog_name: str,
+        schema_name: str,
+        model_name: str,
+        uri: str,
+        aliases: list,
+        comment: str,
+        properties: dict,
+    ) -> str:
+        """
+        Link a new version to an existing model.
+
+        Args:
+            ctx (Context): The request context object.
+            catalog_name (str): Name of the catalog.
+            schema_name (str): Name of the schema.
+            model_name (str): Name of the model.
+            uri (str): URI of the model version artifact.
+            aliases (list): List of string aliases for this version.
+            comment (str): Human-readable description.
+            properties (dict): Model version configuration properties.
+
+        Returns:
+            str: JSON-formatted string containing the success response.
+        """
+        client = ctx.request_context.lifespan_context.rest_client()
+        return await client.as_model_operation().link_model_version(
+            catalog_name,
+            schema_name,
+            model_name,
+            uri,
+            aliases,
+            comment,
+            properties,
+        )
+
+    @mcp.tool(tags={"model"})
+    async def delete_model_version(
+        ctx: Context,
+        catalog_name: str,
+        schema_name: str,
+        model_name: str,
+        version: int,
+    ) -> str:
+        """
+        Delete a specific version of a model.
+
+        Args:
+            ctx (Context): The request context object.
+            catalog_name (str): Name of the catalog.
+            schema_name (str): Name of the schema.
+            model_name (str): Name of the model.
+            version (int): Version number to delete.
+
+        Returns:
+            str: JSON-formatted string indicating whether the version was 
deleted.
+        """
+        client = ctx.request_context.lifespan_context.rest_client()
+        return await client.as_model_operation().delete_model_version(
+            catalog_name, schema_name, model_name, version
+        )
+
+    @mcp.tool(tags={"model"})
+    async def delete_model_version_by_alias(
+        ctx: Context,
+        catalog_name: str,
+        schema_name: str,
+        model_name: str,
+        alias: str,
+    ) -> str:
+        """
+        Delete a model version by one of its aliases.
+
+        Args:
+            ctx (Context): The request context object.
+            catalog_name (str): Name of the catalog.
+            schema_name (str): Name of the schema.
+            model_name (str): Name of the model.
+            alias (str): Alias of the version to delete, e.g. "latest".
+
+        Returns:
+            str: JSON-formatted string indicating whether the version was 
deleted.
+        """
+        client = ctx.request_context.lifespan_context.rest_client()
+        return await client.as_model_operation().delete_model_version_by_alias(
+            catalog_name, schema_name, model_name, alias
+        )
+
+    @mcp.tool(tags={"model"})
+    async def alter_model(
+        ctx: Context,
+        catalog_name: str,
+        schema_name: str,
+        model_name: str,
+        updates: list,
+    ) -> str:
+        """
+        Alter an existing model.
+
+        Args:
+            ctx (Context): The request context object.
+            catalog_name (str): Name of the catalog.
+            schema_name (str): Name of the schema.
+            model_name (str): Name of the model to alter.
+            updates (list): List of update operations. Example:
+                [
+                  {"@type": "rename", "newName": "new_model"},
+                  {"@type": "updateComment", "newComment": "updated"},
+                  {"@type": "setProperty", "property": "k", "value": "v"},
+                  {"@type": "removeProperty", "property": "k"}
+                ]
+
+        Returns:
+            str: JSON-formatted string containing the altered model.
+        """
+        client = ctx.request_context.lifespan_context.rest_client()
+        return await client.as_model_operation().alter_model(
+            catalog_name, schema_name, model_name, updates
+        )
+
+    # pylint: disable=too-many-positional-arguments
+    @mcp.tool(tags={"model"})
+    async def alter_model_version(
+        ctx: Context,
+        catalog_name: str,
+        schema_name: str,
+        model_name: str,
+        version: int,
+        updates: list,
+    ) -> str:
+        """
+        Alter a specific version of a model.
+
+        Args:
+            ctx (Context): The request context object.
+            catalog_name (str): Name of the catalog.
+            schema_name (str): Name of the schema.
+            model_name (str): Name of the model.
+            version (int): Version number to alter.
+            updates (list): List of update operations. Example:
+                [
+                  {"@type": "updateComment", "newComment": "updated"},
+                  {"@type": "setProperty", "property": "k", "value": "v"},
+                  {"@type": "removeProperty", "property": "k"},
+                  {"@type": "updateUri", "newUri": "s3://bucket/model-v2"},
+                  {"@type": "updateAliases", "aliasesToAdd": ["prod"],
+                   "aliasesToRemove": ["staging"]}
+                ]
+
+        Returns:
+            str: JSON-formatted string containing the altered model version.
+        """
+        client = ctx.request_context.lifespan_context.rest_client()
+        return await client.as_model_operation().alter_model_version(
+            catalog_name, schema_name, model_name, version, updates
+        )
+
+    # pylint: disable=too-many-positional-arguments
+    @mcp.tool(tags={"model"})
+    async def alter_model_version_by_alias(
+        ctx: Context,
+        catalog_name: str,
+        schema_name: str,
+        model_name: str,
+        alias: str,
+        updates: list,
+    ) -> str:
+        """
+        Alter a model version identified by one of its aliases.
+
+        Args:
+            ctx (Context): The request context object.
+            catalog_name (str): Name of the catalog.
+            schema_name (str): Name of the schema.
+            model_name (str): Name of the model.
+            alias (str): Alias of the version to alter, e.g. "latest".
+            updates (list): List of update operations; same format as
+                `alter_model_version`.
+
+        Returns:
+            str: JSON-formatted string containing the altered model version.
+        """
+        client = ctx.request_context.lifespan_context.rest_client()
+        return await client.as_model_operation().alter_model_version_by_alias(
+            catalog_name, schema_name, model_name, alias, updates
+        )
diff --git a/mcp-server/mcp_server/tools/schema.py 
b/mcp-server/mcp_server/tools/schema.py
index 3a37353a4b..41ec06d3ce 100644
--- a/mcp-server/mcp_server/tools/schema.py
+++ b/mcp-server/mcp_server/tools/schema.py
@@ -61,3 +61,82 @@ def load_schema_tools(mcp: FastMCP):
         return await client.as_schema_operation().get_list_of_schemas(
             catalog_name
         )
+
+    @mcp.tool(tags={"schema"})
+    async def create_schema(
+        ctx: Context,
+        catalog_name: str,
+        name: str,
+        comment: str,
+        properties: dict,
+    ) -> str:
+        """
+        Create a new schema within a catalog.
+
+        Authorization is enforced by Gravitino: a principal without the
+        required grant receives an authorization denial.
+
+        Args:
+            ctx (Context): The request context object.
+            catalog_name (str): Name of the catalog to create the schema in.
+            name (str): Name of the schema to create.
+            comment (str): Human-readable description.
+            properties (dict): Schema configuration properties.
+
+        Returns:
+            str: JSON-formatted string containing the created schema.
+        """
+        client = ctx.request_context.lifespan_context.rest_client()
+        return await client.as_schema_operation().create_schema(
+            catalog_name, name, comment, properties
+        )
+
+    @mcp.tool(tags={"schema"})
+    async def alter_schema(
+        ctx: Context, catalog_name: str, schema_name: str, updates: list
+    ) -> str:
+        """
+        Alter an existing schema.
+
+        Args:
+            ctx (Context): The request context object.
+            catalog_name (str): Name of the catalog containing the schema.
+            schema_name (str): Name of the schema to alter.
+            updates (list): List of update operations. Example:
+                [
+                  {"@type": "setProperty", "property": "k", "value": "v"},
+                  {"@type": "removeProperty", "property": "k"}
+                ]
+
+        Returns:
+            str: JSON-formatted string containing the altered schema.
+        """
+        client = ctx.request_context.lifespan_context.rest_client()
+        return await client.as_schema_operation().alter_schema(
+            catalog_name, schema_name, updates
+        )
+
+    @mcp.tool(tags={"schema"})
+    async def drop_schema(
+        ctx: Context, catalog_name: str, schema_name: str, cascade: bool = 
False
+    ) -> str:
+        """
+        Drop a schema by its name.
+
+        A non-empty schema cannot be dropped unless `cascade` is true.
+
+        Args:
+            ctx (Context): The request context object.
+            catalog_name (str): Name of the catalog containing the schema.
+            schema_name (str): Name of the schema to drop.
+            cascade (bool): Whether to also drop all entities under the
+                schema. Defaults to False, in which case dropping a
+                non-empty schema fails.
+
+        Returns:
+            str: JSON-formatted string indicating whether the schema was 
dropped.
+        """
+        client = ctx.request_context.lifespan_context.rest_client()
+        return await client.as_schema_operation().drop_schema(
+            catalog_name, schema_name, cascade
+        )
diff --git a/mcp-server/mcp_server/tools/table.py 
b/mcp-server/mcp_server/tools/table.py
index af6c62da6f..01b861a1bd 100644
--- a/mcp-server/mcp_server/tools/table.py
+++ b/mcp-server/mcp_server/tools/table.py
@@ -257,3 +257,127 @@ def load_table_tools(mcp: FastMCP):
         return await client.as_table_operation().load_table(
             catalog_name, schema_name, table_name
         )
+
+    @mcp.tool(tags={"table"})
+    # pylint: disable=too-many-positional-arguments
+    async def create_table(
+        ctx: Context,
+        catalog_name: str,
+        schema_name: str,
+        name: str,
+        comment: str,
+        columns: list,
+        properties: dict,
+        partitioning: list = None,
+        distribution: dict = None,
+        sort_orders: list = None,
+        indexes: list = None,
+    ) -> str:
+        """
+        Create a new table within a schema.
+
+        Authorization is enforced by Gravitino: a principal without the
+        required grant receives an authorization denial.
+
+        Args:
+            ctx (Context): The request context object.
+            catalog_name (str): Name of the catalog.
+            schema_name (str): Name of the schema.
+            name (str): Name of the table to create.
+            comment (str): Human-readable description.
+            columns (list): List of column definitions. Each column is a dict:
+                {
+                  "name": "id",
+                  "type": "integer",
+                  "comment": "primary id",
+                  "nullable": false,
+                  "autoIncrement": false
+                }
+            properties (dict): Table configuration properties.
+            partitioning (list): Optional partitioning strategies. Example:
+                [{"strategy": "identity", "fieldName": ["dt"]}]
+            distribution (dict): Optional distribution (bucketing). Example:
+                {"strategy": "hash", "number": 4,
+                 "funcArgs": [{"type": "field", "fieldName": ["id"]}]}
+            sort_orders (list): Optional sort orders. Example:
+                [{"sortTerm": {"type": "field", "fieldName": ["id"]},
+                  "direction": "asc", "nullOrdering": "nulls_first"}]
+            indexes (list): Optional indexes. Example:
+                [{"indexType": "primary_key", "name": "pk",
+                  "fieldNames": [["id"]]}]
+
+        Returns:
+            str: JSON-formatted string containing the created table.
+        """
+        client = ctx.request_context.lifespan_context.rest_client()
+        return await client.as_table_operation().create_table(
+            catalog_name,
+            schema_name,
+            name,
+            comment,
+            columns,
+            properties,
+            partitioning,
+            distribution,
+            sort_orders,
+            indexes,
+        )
+
+    @mcp.tool(tags={"table"})
+    async def alter_table(
+        ctx: Context,
+        catalog_name: str,
+        schema_name: str,
+        table_name: str,
+        updates: list,
+    ) -> str:
+        """
+        Alter an existing table.
+
+        Args:
+            ctx (Context): The request context object.
+            catalog_name (str): Name of the catalog.
+            schema_name (str): Name of the schema.
+            table_name (str): Name of the table to alter.
+            updates (list): List of update operations. Example:
+                [
+                  {"@type": "rename", "newName": "new_table"},
+                  {"@type": "updateComment", "newComment": "updated"},
+                  {"@type": "setProperty", "property": "k", "value": "v"}
+                ]
+
+        Returns:
+            str: JSON-formatted string containing the altered table.
+        """
+        client = ctx.request_context.lifespan_context.rest_client()
+        return await client.as_table_operation().alter_table(
+            catalog_name, schema_name, table_name, updates
+        )
+
+    @mcp.tool(tags={"table"})
+    async def drop_table(
+        ctx: Context,
+        catalog_name: str,
+        schema_name: str,
+        table_name: str,
+        purge: bool = False,
+    ) -> str:
+        """
+        Drop a table by its name.
+
+        Args:
+            ctx (Context): The request context object.
+            catalog_name (str): Name of the catalog.
+            schema_name (str): Name of the schema.
+            table_name (str): Name of the table to drop.
+            purge (bool): Whether to purge the table data in addition to
+                removing the metadata. Defaults to False. Not all catalogs
+                support purging.
+
+        Returns:
+            str: JSON-formatted string indicating whether the table was 
dropped.
+        """
+        client = ctx.request_context.lifespan_context.rest_client()
+        return await client.as_table_operation().drop_table(
+            catalog_name, schema_name, table_name, purge
+        )
diff --git a/mcp-server/mcp_server/tools/tag.py 
b/mcp-server/mcp_server/tools/tag.py
index f6daafa678..93eb89d62c 100644
--- a/mcp-server/mcp_server/tools/tag.py
+++ b/mcp-server/mcp_server/tools/tag.py
@@ -120,7 +120,8 @@ def load_tag_tool(mcp: FastMCP):
         client = ctx.request_context.lifespan_context.rest_client()
         return await client.as_tag_operation().list_of_tags()
 
-    # Disable the alter_tag tool by default as it can be destructive.
+    # alter_tag is destructive; it is exposed and access is enforced by 
Gravitino
+    # authorization rather than by hiding the tool.
     @mcp.tool(tags={"tag"})
     async def alter_tag(ctx: Context, name: str, updates: list) -> str:
         """
@@ -173,9 +174,10 @@ def load_tag_tool(mcp: FastMCP):
         client = ctx.request_context.lifespan_context.rest_client()
         return await client.as_tag_operation().alter_tag(name, updates)
 
-    # Disable the delete_tag tool by default as it can be destructive.
+    # delete_tag is destructive; it is exposed and access is enforced by 
Gravitino
+    # authorization rather than by hiding the tool.
     @mcp.tool(tags={"tag"})
-    async def delete_tag(ctx: Context, name: str) -> None:
+    async def delete_tag(ctx: Context, name: str) -> str:
         """
         Delete a tag by its name.
 
@@ -185,10 +187,7 @@ def load_tag_tool(mcp: FastMCP):
             name (str): Name of the tag to delete
 
         Returns:
-            None
-
-        Raises:
-            Exception: If the deletion fails, an exception is raised with an 
error message.
+            str: JSON-formatted string indicating whether the tag was deleted.
         """
         client = ctx.request_context.lifespan_context.rest_client()
         return await client.as_tag_operation().delete_tag(name)
@@ -364,8 +363,3 @@ def load_tag_tool(mcp: FastMCP):
         """
         client = ctx.request_context.lifespan_context.rest_client()
         return await client.as_tag_operation().list_metadata_by_tag(tag_name)
-
-    mcp.disable(
-        names={"create_tag", "alter_tag", "delete_tag"},
-        components={"tool"},
-    )
diff --git a/mcp-server/mcp_server/tools/topic.py 
b/mcp-server/mcp_server/tools/topic.py
index 93d2aa57d5..2455fa2fd9 100644
--- a/mcp-server/mcp_server/tools/topic.py
+++ b/mcp-server/mcp_server/tools/topic.py
@@ -132,3 +132,86 @@ def load_topic_tools(mcp: FastMCP):
         return await client.as_topic_operation().load_topic(
             catalog_name, schema_name, topic_name
         )
+
+    @mcp.tool(tags={"topic"})
+    # pylint: disable=too-many-positional-arguments
+    async def create_topic(
+        ctx: Context,
+        catalog_name: str,
+        schema_name: str,
+        name: str,
+        comment: str,
+        properties: dict,
+    ) -> str:
+        """
+        Create a new topic within a schema.
+
+        Authorization is enforced by Gravitino: a principal without the
+        required grant receives an authorization denial.
+
+        Args:
+            ctx (Context): The request context object.
+            catalog_name (str): Name of the catalog.
+            schema_name (str): Name of the schema.
+            name (str): Name of the topic to create.
+            comment (str): Human-readable description.
+            properties (dict): Topic configuration properties.
+
+        Returns:
+            str: JSON-formatted string containing the created topic.
+        """
+        client = ctx.request_context.lifespan_context.rest_client()
+        return await client.as_topic_operation().create_topic(
+            catalog_name, schema_name, name, comment, properties
+        )
+
+    @mcp.tool(tags={"topic"})
+    async def alter_topic(
+        ctx: Context,
+        catalog_name: str,
+        schema_name: str,
+        topic_name: str,
+        updates: list,
+    ) -> str:
+        """
+        Alter an existing topic.
+
+        Args:
+            ctx (Context): The request context object.
+            catalog_name (str): Name of the catalog.
+            schema_name (str): Name of the schema.
+            topic_name (str): Name of the topic to alter.
+            updates (list): List of update operations. Example:
+                [
+                  {"@type": "updateComment", "newComment": "updated"},
+                  {"@type": "setProperty", "property": "k", "value": "v"}
+                ]
+
+        Returns:
+            str: JSON-formatted string containing the altered topic.
+        """
+        client = ctx.request_context.lifespan_context.rest_client()
+        return await client.as_topic_operation().alter_topic(
+            catalog_name, schema_name, topic_name, updates
+        )
+
+    @mcp.tool(tags={"topic"})
+    async def delete_topic(
+        ctx: Context, catalog_name: str, schema_name: str, topic_name: str
+    ) -> str:
+        """
+        Delete a topic by its name.
+
+        Args:
+            ctx (Context): The request context object.
+            catalog_name (str): Name of the catalog.
+            schema_name (str): Name of the schema.
+            topic_name (str): Name of the topic to delete.
+
+        Returns:
+            str: JSON-formatted string indicating whether the topic was 
deleted.
+        """
+        client = ctx.request_context.lifespan_context.rest_client()
+        return await client.as_topic_operation().delete_topic(
+            catalog_name, schema_name, topic_name
+        )
diff --git a/mcp-server/tests/integration/test_authz_e2e.py 
b/mcp-server/tests/integration/test_authz_e2e.py
index 79f070fadc..488e6245db 100644
--- a/mcp-server/tests/integration/test_authz_e2e.py
+++ b/mcp-server/tests/integration/test_authz_e2e.py
@@ -83,15 +83,6 @@ def test_authorization_scoped_discovery(gravitino_fixture, 
integration_env):
     assert admin_catalogs != bob_catalogs
 
 
[email protected](
-    reason="Depends on the 'create_tag' write tool, which is disabled by 
default "
-    "in the MCP server (destructive-tool gating in tools/tag.py). With it 
disabled "
-    "the call fails as 'Unknown tool' rather than an authorization denial. No 
other "
-    "enabled write tool works as a drop-in here: 
associate_tag/policy_with_metadata "
-    "and run_job all require fixture entities that the setup does not create, 
so they "
-    "would fail with 'not found' instead of a denial. Re-enable this test once 
the PR "
-    "that enables 'create_tag' is merged."
-)
 def test_write_denied_for_readonly_principal(
     gravitino_fixture, integration_env
 ):
diff --git a/mcp-server/tests/unit/client/test_url_encoding.py 
b/mcp-server/tests/unit/client/test_url_encoding.py
index 36eb7cb8df..70de5dd831 100644
--- a/mcp-server/tests/unit/client/test_url_encoding.py
+++ b/mcp-server/tests/unit/client/test_url_encoding.py
@@ -65,6 +65,7 @@ def _make_mock_client(response_json: dict):
     client.post = AsyncMock(return_value=response)
     client.put = AsyncMock(return_value=response)
     client.delete = AsyncMock(return_value=response)
+    client.patch = AsyncMock(return_value=response)
     return client
 
 
@@ -98,6 +99,20 @@ class TestCatalogOperationUrlEncoding(unittest.TestCase):
         self.assertIn(_ENCODED_PATH_TRAVERSAL, url)
         self.assertNotIn("../../", url)
 
+    def test_drop_catalog_passes_force_as_query_param(self):
+        client = _make_mock_client({"code": 0, "dropped": True})
+        op = PlainRESTClientCatalogOperation(METALAKE, client)
+        result = asyncio.run(op.drop_catalog("catalog", True))
+        self.assertEqual("true", result)
+        self.assertTrue(_called_params(client.delete)["force"])
+
+    def test_set_catalog_in_use_sends_patch_with_in_use(self):
+        client = _make_mock_client({"code": 0})
+        op = PlainRESTClientCatalogOperation(METALAKE, client)
+        result = asyncio.run(op.set_catalog_in_use("catalog", False))
+        self.assertEqual('{"code": 0}', result)
+        self.assertEqual({"inUse": False}, client.patch.call_args[1]["json"])
+
 
 class TestSchemaOperationUrlEncoding(unittest.TestCase):
     def test_get_list_of_schemas_encodes_catalog_name(self):
@@ -116,6 +131,12 @@ class TestSchemaOperationUrlEncoding(unittest.TestCase):
         self.assertIn(_ENCODED_QUERY_INJECTION, url)
         self.assertNotIn("?admin=true", url)
 
+    def test_drop_schema_passes_cascade_as_query_param(self):
+        client = _make_mock_client({"code": 0, "dropped": True})
+        op = PlainRESTClientSchemaOperation(METALAKE, client)
+        asyncio.run(op.drop_schema("catalog", "schema", True))
+        self.assertTrue(_called_params(client.delete)["cascade"])
+
 
 class TestTableOperationUrlEncoding(unittest.TestCase):
     def test_get_list_of_tables_encodes_path_traversal(self):
@@ -135,6 +156,32 @@ class TestTableOperationUrlEncoding(unittest.TestCase):
         self.assertIn(_ENCODED_QUERY_INJECTION, url)
         self.assertNotIn("?admin=true", url)
 
+    def test_create_table_omits_unset_optional_fields(self):
+        client = _make_mock_client({"table": {}})
+        op = PlainRESTClientTableOperation(METALAKE, client)
+        asyncio.run(op.create_table("catalog", "schema", "t", "", [], {}))
+        request = client.post.call_args[1]["json"]
+        self.assertNotIn("partitioning", request)
+        self.assertNotIn("distribution", request)
+        self.assertNotIn("sortOrders", request)
+        self.assertNotIn("indexes", request)
+
+    def test_create_table_includes_partitioning_when_set(self):
+        client = _make_mock_client({"table": {}})
+        op = PlainRESTClientTableOperation(METALAKE, client)
+        partitioning = [{"strategy": "identity", "fieldName": ["dt"]}]
+        asyncio.run(
+            op.create_table("catalog", "schema", "t", "", [], {}, partitioning)
+        )
+        request = client.post.call_args[1]["json"]
+        self.assertEqual(partitioning, request["partitioning"])
+
+    def test_drop_table_passes_purge_as_query_param(self):
+        client = _make_mock_client({"code": 0, "dropped": True})
+        op = PlainRESTClientTableOperation(METALAKE, client)
+        asyncio.run(op.drop_table("catalog", "schema", "t", True))
+        self.assertTrue(_called_params(client.delete)["purge"])
+
 
 class TestModelOperationUrlEncoding(unittest.TestCase):
     def test_list_of_models_encodes_path_traversal(self):
@@ -165,6 +212,71 @@ class TestModelOperationUrlEncoding(unittest.TestCase):
         self.assertIn(_ENCODED_PATH_TRAVERSAL, url)
         self.assertNotIn("../../", url)
 
+    def test_delete_model_reads_dropped_response(self):
+        client = _make_mock_client({"code": 0, "dropped": True})
+        op = PlainRESTClientModelOperation(METALAKE, client)
+        result = asyncio.run(op.delete_model("catalog", "schema", "model"))
+        self.assertEqual("true", result)
+
+    def test_link_model_version_returns_base_response(self):
+        client = _make_mock_client({"code": 0})
+        op = PlainRESTClientModelOperation(METALAKE, client)
+        result = asyncio.run(
+            op.link_model_version(
+                "catalog", "schema", "model", "s3://bucket/model", [], "", {}
+            )
+        )
+        self.assertEqual('{"code": 0}', result)
+
+    def test_delete_model_version_reads_dropped_response(self):
+        client = _make_mock_client({"code": 0, "dropped": True})
+        op = PlainRESTClientModelOperation(METALAKE, client)
+        result = asyncio.run(
+            op.delete_model_version("catalog", "schema", "model", 1)
+        )
+        self.assertEqual("true", result)
+
+    def test_delete_model_version_by_alias_encodes_alias(self):
+        client = _make_mock_client({"code": 0, "dropped": True})
+        op = PlainRESTClientModelOperation(METALAKE, client)
+        result = asyncio.run(
+            op.delete_model_version_by_alias(
+                "catalog", "schema", "model", _PATH_TRAVERSAL
+            )
+        )
+        self.assertEqual("true", result)
+        url = _called_url(client.delete)
+        self.assertIn(_ENCODED_PATH_TRAVERSAL, url)
+        self.assertNotIn("../../", url)
+
+    def test_alter_model_encodes_model_name(self):
+        client = _make_mock_client({"model": {}})
+        op = PlainRESTClientModelOperation(METALAKE, client)
+        asyncio.run(op.alter_model("catalog", "schema", _QUERY_INJECTION, []))
+        url = _called_url(client.put)
+        self.assertIn(_ENCODED_QUERY_INJECTION, url)
+        self.assertNotIn("?admin=true", url)
+
+    def test_alter_model_version_reads_model_version_response(self):
+        client = _make_mock_client({"modelVersion": {"version": 1}})
+        op = PlainRESTClientModelOperation(METALAKE, client)
+        result = asyncio.run(
+            op.alter_model_version("catalog", "schema", "model", 1, [])
+        )
+        self.assertEqual('{"version": 1}', result)
+
+    def test_alter_model_version_by_alias_encodes_alias(self):
+        client = _make_mock_client({"modelVersion": {}})
+        op = PlainRESTClientModelOperation(METALAKE, client)
+        asyncio.run(
+            op.alter_model_version_by_alias(
+                "catalog", "schema", "model", _PATH_TRAVERSAL, []
+            )
+        )
+        url = _called_url(client.put)
+        self.assertIn(_ENCODED_PATH_TRAVERSAL, url)
+        self.assertNotIn("../../", url)
+
 
 class TestTopicOperationUrlEncoding(unittest.TestCase):
     def test_list_of_topics_encodes_path_traversal(self):
@@ -185,6 +297,28 @@ class TestTopicOperationUrlEncoding(unittest.TestCase):
 
 
 class TestFilesetOperationUrlEncoding(unittest.TestCase):
+    def test_create_fileset_omits_empty_storage_location(self):
+        client = _make_mock_client({"fileset": {}})
+        op = PlainRESTClientFilesetOperation(METALAKE, client)
+        asyncio.run(
+            op.create_fileset("catalog", "schema", "fs", "managed", "", "", {})
+        )
+        request = client.post.call_args[1]["json"]
+        self.assertNotIn("storageLocation", request)
+        self.assertNotIn("storageLocations", request)
+
+    def test_create_fileset_includes_storage_locations_when_set(self):
+        client = _make_mock_client({"fileset": {}})
+        op = PlainRESTClientFilesetOperation(METALAKE, client)
+        locations = {"loc1": "file:/tmp/fs1", "loc2": "file:/tmp/fs2"}
+        asyncio.run(
+            op.create_fileset(
+                "catalog", "schema", "fs", "managed", "", "", {}, locations
+            )
+        )
+        request = client.post.call_args[1]["json"]
+        self.assertEqual(locations, request["storageLocations"])
+
     def test_list_of_filesets_encodes_path_traversal(self):
         client = _make_mock_client({"identifiers": []})
         op = PlainRESTClientFilesetOperation(METALAKE, client)
@@ -242,9 +376,10 @@ class TestTagOperationUrlEncoding(unittest.TestCase):
         self.assertNotIn("?admin=true", url)
 
     def test_delete_tag_encodes_tag_name(self):
-        client = _make_mock_client({})
+        client = _make_mock_client({"code": 0, "dropped": True})
         op = PlainRESTClientTagOperation(METALAKE, client)
-        asyncio.run(op.delete_tag(_PATH_TRAVERSAL))
+        result = asyncio.run(op.delete_tag(_PATH_TRAVERSAL))
+        self.assertEqual("true", result)
         url = _called_url(client.delete)
         self.assertIn(_ENCODED_PATH_TRAVERSAL, url)
         self.assertNotIn("../../", url)
diff --git a/mcp-server/tests/unit/tools/mock_operation.py 
b/mcp-server/tests/unit/tools/mock_operation.py
index e1fc540442..e1953bc474 100644
--- a/mcp-server/tests/unit/tools/mock_operation.py
+++ b/mcp-server/tests/unit/tools/mock_operation.py
@@ -69,11 +69,40 @@ class MockCatalogOperation(CatalogOperation):
     async def get_list_of_catalogs(self) -> str:
         return "mock_catalogs"
 
+    # pylint: disable=too-many-positional-arguments
+    async def create_catalog(
+        self, name, catalog_type, provider, comment, properties
+    ) -> str:
+        return f"mock_catalog_created: {name}, {catalog_type}, {provider}"
+
+    async def alter_catalog(self, catalog_name, updates) -> str:
+        return f"mock_catalog_altered: {catalog_name} with updates {updates}"
+
+    async def drop_catalog(self, catalog_name, force) -> str:
+        return f"mock_catalog_dropped: {catalog_name}, force={force}"
+
+    async def set_catalog_in_use(self, catalog_name, in_use) -> str:
+        return f"mock_catalog_set_in_use: {catalog_name}, in_use={in_use}"
+
 
 class MockSchemaOperation(SchemaOperation):
     async def get_list_of_schemas(self, catalog_name: str) -> str:
         return "mock_schemas"
 
+    async def create_schema(
+        self, catalog_name, name, comment, properties
+    ) -> str:
+        return f"mock_schema_created: {catalog_name}.{name}"
+
+    async def alter_schema(self, catalog_name, schema_name, updates) -> str:
+        return f"mock_schema_altered: {catalog_name}.{schema_name} with 
updates {updates}"
+
+    async def drop_schema(self, catalog_name, schema_name, cascade) -> str:
+        return (
+            f"mock_schema_dropped: {catalog_name}.{schema_name}, "
+            f"cascade={cascade}"
+        )
+
 
 class MockTableOperation(TableOperation):
     async def get_list_of_tables(
@@ -86,6 +115,41 @@ class MockTableOperation(TableOperation):
     ) -> str:
         return "mock_table"
 
+    # pylint: disable=R0917
+    async def create_table(
+        self,
+        catalog_name,
+        schema_name,
+        name,
+        comment,
+        columns,
+        properties,
+        partitioning=None,
+        distribution=None,
+        sort_orders=None,
+        indexes=None,
+    ) -> str:
+        return (
+            f"mock_table_created: {catalog_name}.{schema_name}.{name}, "
+            f"partitioning={partitioning}"
+        )
+
+    async def alter_table(
+        self, catalog_name, schema_name, table_name, updates
+    ) -> str:
+        return (
+            f"mock_table_altered: {catalog_name}.{schema_name}.{table_name} "
+            f"with updates {updates}"
+        )
+
+    async def drop_table(
+        self, catalog_name, schema_name, table_name, purge
+    ) -> str:
+        return (
+            f"mock_table_dropped: {catalog_name}.{schema_name}.{table_name}, "
+            f"purge={purge}"
+        )
+
 
 class MockFilesetOperation(FilesetOperation):
     async def list_of_filesets(
@@ -110,6 +174,38 @@ class MockFilesetOperation(FilesetOperation):
     ) -> str:
         return "mock_files_in_fileset"
 
+    # pylint: disable=R0917
+    async def create_fileset(
+        self,
+        catalog_name,
+        schema_name,
+        name,
+        fileset_type,
+        storage_location,
+        comment,
+        properties,
+        storage_locations=None,
+    ) -> str:
+        return (
+            f"mock_fileset_created: {catalog_name}.{schema_name}.{name}, "
+            f"storage_locations={storage_locations}"
+        )
+
+    async def alter_fileset(
+        self, catalog_name, schema_name, fileset_name, updates
+    ) -> str:
+        return (
+            f"mock_fileset_altered: 
{catalog_name}.{schema_name}.{fileset_name} "
+            f"with updates {updates}"
+        )
+
+    async def drop_fileset(
+        self, catalog_name, schema_name, fileset_name
+    ) -> str:
+        return (
+            f"mock_fileset_dropped: 
{catalog_name}.{schema_name}.{fileset_name}"
+        )
+
 
 class MockPolicyOperation(PolicyOperation):
     async def associate_policy_with_metadata(
@@ -170,6 +266,75 @@ class MockModelOperation(ModelOperation):
     ) -> str:
         return "mock_model_version_by_alias"
 
+    # pylint: disable=too-many-positional-arguments
+    async def register_model(
+        self, catalog_name, schema_name, name, comment, properties
+    ) -> str:
+        return f"mock_model_registered: {catalog_name}.{schema_name}.{name}"
+
+    async def delete_model(self, catalog_name, schema_name, model_name) -> str:
+        return f"mock_model_deleted: {catalog_name}.{schema_name}.{model_name}"
+
+    # pylint: disable=R0917
+    async def link_model_version(
+        self,
+        catalog_name,
+        schema_name,
+        model_name,
+        uri,
+        aliases,
+        comment,
+        properties,
+    ) -> str:
+        return (
+            f"mock_model_version_linked: 
{catalog_name}.{schema_name}.{model_name} "
+            f"uri={uri} aliases={aliases}"
+        )
+
+    async def delete_model_version(
+        self, catalog_name, schema_name, model_name, version
+    ) -> str:
+        return (
+            f"mock_model_version_deleted: 
{catalog_name}.{schema_name}.{model_name} "
+            f"version={version}"
+        )
+
+    async def delete_model_version_by_alias(
+        self, catalog_name, schema_name, model_name, alias
+    ) -> str:
+        return (
+            f"mock_model_version_deleted_by_alias: "
+            f"{catalog_name}.{schema_name}.{model_name} alias={alias}"
+        )
+
+    async def alter_model(
+        self, catalog_name, schema_name, model_name, updates
+    ) -> str:
+        return (
+            f"mock_model_altered: {catalog_name}.{schema_name}.{model_name} "
+            f"with updates {updates}"
+        )
+
+    # pylint: disable=R0917
+    async def alter_model_version(
+        self, catalog_name, schema_name, model_name, version, updates
+    ) -> str:
+        return (
+            f"mock_model_version_altered: "
+            f"{catalog_name}.{schema_name}.{model_name} version={version} "
+            f"with updates {updates}"
+        )
+
+    # pylint: disable=R0917
+    async def alter_model_version_by_alias(
+        self, catalog_name, schema_name, model_name, alias, updates
+    ) -> str:
+        return (
+            f"mock_model_version_altered_by_alias: "
+            f"{catalog_name}.{schema_name}.{model_name} alias={alias} "
+            f"with updates {updates}"
+        )
+
 
 class MockTopicOperation(TopicOperation):
     async def list_of_topics(self, catalog_name: str, schema_name: str) -> str:
@@ -180,6 +345,23 @@ class MockTopicOperation(TopicOperation):
     ) -> str:
         return "mock_topic"
 
+    # pylint: disable=too-many-positional-arguments
+    async def create_topic(
+        self, catalog_name, schema_name, name, comment, properties
+    ) -> str:
+        return f"mock_topic_created: {catalog_name}.{schema_name}.{name}"
+
+    async def alter_topic(
+        self, catalog_name, schema_name, topic_name, updates
+    ) -> str:
+        return (
+            f"mock_topic_altered: {catalog_name}.{schema_name}.{topic_name} "
+            f"with updates {updates}"
+        )
+
+    async def delete_topic(self, catalog_name, schema_name, topic_name) -> str:
+        return f"mock_topic_deleted: {catalog_name}.{schema_name}.{topic_name}"
+
 
 class MockTagOperation(TagOperation):
     async def list_of_tags(self) -> str:
diff --git a/mcp-server/tests/unit/tools/test_catalog.py 
b/mcp-server/tests/unit/tools/test_catalog.py
index ca6c03f735..6b626b9015 100644
--- a/mcp-server/tests/unit/tools/test_catalog.py
+++ b/mcp-server/tests/unit/tools/test_catalog.py
@@ -45,3 +45,90 @@ class TestCatalogTool(unittest.TestCase):
                 self.assertEqual("mock_catalogs", result.content[0].text)
 
         asyncio.run(_test_list_catalogs(self.mcp))
+
+    def test_create_catalog(self):
+        async def _test(mcp_server):
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "create_catalog",
+                    {
+                        "name": "cat1",
+                        "catalog_type": "relational",
+                        "provider": "hive",
+                        "comment": "c",
+                        "properties": {"k": "v"},
+                    },
+                )
+                self.assertEqual(
+                    "mock_catalog_created: cat1, relational, hive",
+                    result.content[0].text,
+                )
+
+        asyncio.run(_test(self.mcp))
+
+    def test_alter_catalog(self):
+        async def _test(mcp_server):
+            updates = [{"@type": "rename", "newName": "cat2"}]
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "alter_catalog",
+                    {"catalog_name": "cat1", "updates": updates},
+                )
+                self.assertEqual(
+                    f"mock_catalog_altered: cat1 with updates {updates}",
+                    result.content[0].text,
+                )
+
+        asyncio.run(_test(self.mcp))
+
+    def test_drop_catalog(self):
+        async def _test(mcp_server):
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "drop_catalog", {"catalog_name": "cat1"}
+                )
+                self.assertEqual(
+                    "mock_catalog_dropped: cat1, force=False",
+                    result.content[0].text,
+                )
+
+        asyncio.run(_test(self.mcp))
+
+    def test_drop_catalog_force(self):
+        async def _test(mcp_server):
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "drop_catalog", {"catalog_name": "cat1", "force": True}
+                )
+                self.assertEqual(
+                    "mock_catalog_dropped: cat1, force=True",
+                    result.content[0].text,
+                )
+
+        asyncio.run(_test(self.mcp))
+
+    def test_set_catalog_in_use(self):
+        async def _test(mcp_server):
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "set_catalog_in_use",
+                    {"catalog_name": "cat1", "in_use": False},
+                )
+                self.assertEqual(
+                    "mock_catalog_set_in_use: cat1, in_use=False",
+                    result.content[0].text,
+                )
+
+        asyncio.run(_test(self.mcp))
+
+    def test_write_tools_exposed(self):
+        """Write tools must be present (authorization enforced by 
Gravitino)."""
+
+        async def _test(mcp_server):
+            names = {t.name for t in await mcp_server.list_tools()}
+            self.assertIn("create_catalog", names)
+            self.assertIn("alter_catalog", names)
+            self.assertIn("drop_catalog", names)
+            self.assertIn("set_catalog_in_use", names)
+
+        asyncio.run(_test(self.mcp))
diff --git a/mcp-server/tests/unit/tools/test_fileset.py 
b/mcp-server/tests/unit/tools/test_fileset.py
index a0c0371491..5b341f0b7a 100644
--- a/mcp-server/tests/unit/tools/test_fileset.py
+++ b/mcp-server/tests/unit/tools/test_fileset.py
@@ -75,3 +75,87 @@ class TestFilesetTool(unittest.TestCase):
                 )
 
         asyncio.run(_test_list_files_in_fileset(self.mcp))
+
+    def test_create_fileset(self):
+        async def _test(mcp_server):
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "create_fileset",
+                    {
+                        "catalog_name": "cat",
+                        "schema_name": "sch",
+                        "name": "fs",
+                        "fileset_type": "managed",
+                        "storage_location": "file:/tmp/fs",
+                        "comment": "c",
+                        "properties": {"k": "v"},
+                    },
+                )
+                self.assertEqual(
+                    "mock_fileset_created: cat.sch.fs, storage_locations=None",
+                    result.content[0].text,
+                )
+
+        asyncio.run(_test(self.mcp))
+
+    def test_create_fileset_with_multiple_locations(self):
+        async def _test(mcp_server):
+            locations = {"loc1": "file:/tmp/fs1", "loc2": "file:/tmp/fs2"}
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "create_fileset",
+                    {
+                        "catalog_name": "cat",
+                        "schema_name": "sch",
+                        "name": "fs",
+                        "fileset_type": "managed",
+                        "storage_location": "",
+                        "comment": "c",
+                        "properties": {"k": "v"},
+                        "storage_locations": locations,
+                    },
+                )
+                self.assertEqual(
+                    "mock_fileset_created: cat.sch.fs, "
+                    f"storage_locations={locations}",
+                    result.content[0].text,
+                )
+
+        asyncio.run(_test(self.mcp))
+
+    def test_alter_fileset(self):
+        async def _test(mcp_server):
+            updates = [{"@type": "rename", "newName": "fs2"}]
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "alter_fileset",
+                    {
+                        "catalog_name": "cat",
+                        "schema_name": "sch",
+                        "fileset_name": "fs",
+                        "updates": updates,
+                    },
+                )
+                self.assertEqual(
+                    f"mock_fileset_altered: cat.sch.fs with updates {updates}",
+                    result.content[0].text,
+                )
+
+        asyncio.run(_test(self.mcp))
+
+    def test_drop_fileset(self):
+        async def _test(mcp_server):
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "drop_fileset",
+                    {
+                        "catalog_name": "cat",
+                        "schema_name": "sch",
+                        "fileset_name": "fs",
+                    },
+                )
+                self.assertEqual(
+                    "mock_fileset_dropped: cat.sch.fs", result.content[0].text
+                )
+
+        asyncio.run(_test(self.mcp))
diff --git a/mcp-server/tests/unit/tools/test_model.py 
b/mcp-server/tests/unit/tools/test_model.py
index ff27e64d3b..d550c8ba44 100644
--- a/mcp-server/tests/unit/tools/test_model.py
+++ b/mcp-server/tests/unit/tools/test_model.py
@@ -106,3 +106,165 @@ class TestModelTool(unittest.TestCase):
                 )
 
         asyncio.run(_test_load_model_version_by_alias(self.mcp))
+
+    def test_register_model(self):
+        async def _test(mcp_server):
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "register_model",
+                    {
+                        "catalog_name": "cat",
+                        "schema_name": "sch",
+                        "name": "m",
+                        "comment": "c",
+                        "properties": {"k": "v"},
+                    },
+                )
+                self.assertEqual(
+                    "mock_model_registered: cat.sch.m", result.content[0].text
+                )
+
+        asyncio.run(_test(self.mcp))
+
+    def test_delete_model(self):
+        async def _test(mcp_server):
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "delete_model",
+                    {
+                        "catalog_name": "cat",
+                        "schema_name": "sch",
+                        "model_name": "m",
+                    },
+                )
+                self.assertEqual(
+                    "mock_model_deleted: cat.sch.m", result.content[0].text
+                )
+
+        asyncio.run(_test(self.mcp))
+
+    def test_link_model_version(self):
+        async def _test(mcp_server):
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "link_model_version",
+                    {
+                        "catalog_name": "cat",
+                        "schema_name": "sch",
+                        "model_name": "m",
+                        "uri": "s3://bucket/model",
+                        "aliases": ["latest"],
+                        "comment": "c",
+                        "properties": {"k": "v"},
+                    },
+                )
+                self.assertEqual(
+                    "mock_model_version_linked: cat.sch.m "
+                    "uri=s3://bucket/model aliases=['latest']",
+                    result.content[0].text,
+                )
+
+        asyncio.run(_test(self.mcp))
+
+    def test_delete_model_version(self):
+        async def _test(mcp_server):
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "delete_model_version",
+                    {
+                        "catalog_name": "cat",
+                        "schema_name": "sch",
+                        "model_name": "m",
+                        "version": 1,
+                    },
+                )
+                self.assertEqual(
+                    "mock_model_version_deleted: cat.sch.m version=1",
+                    result.content[0].text,
+                )
+
+        asyncio.run(_test(self.mcp))
+
+    def test_delete_model_version_by_alias(self):
+        async def _test(mcp_server):
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "delete_model_version_by_alias",
+                    {
+                        "catalog_name": "cat",
+                        "schema_name": "sch",
+                        "model_name": "m",
+                        "alias": "latest",
+                    },
+                )
+                self.assertEqual(
+                    "mock_model_version_deleted_by_alias: cat.sch.m "
+                    "alias=latest",
+                    result.content[0].text,
+                )
+
+        asyncio.run(_test(self.mcp))
+
+    def test_alter_model(self):
+        async def _test(mcp_server):
+            updates = [{"@type": "rename", "newName": "m2"}]
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "alter_model",
+                    {
+                        "catalog_name": "cat",
+                        "schema_name": "sch",
+                        "model_name": "m",
+                        "updates": updates,
+                    },
+                )
+                self.assertEqual(
+                    f"mock_model_altered: cat.sch.m with updates {updates}",
+                    result.content[0].text,
+                )
+
+        asyncio.run(_test(self.mcp))
+
+    def test_alter_model_version(self):
+        async def _test(mcp_server):
+            updates = [{"@type": "updateComment", "newComment": "c2"}]
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "alter_model_version",
+                    {
+                        "catalog_name": "cat",
+                        "schema_name": "sch",
+                        "model_name": "m",
+                        "version": 1,
+                        "updates": updates,
+                    },
+                )
+                self.assertEqual(
+                    "mock_model_version_altered: cat.sch.m version=1 "
+                    f"with updates {updates}",
+                    result.content[0].text,
+                )
+
+        asyncio.run(_test(self.mcp))
+
+    def test_alter_model_version_by_alias(self):
+        async def _test(mcp_server):
+            updates = [{"@type": "updateUri", "newUri": "s3://bucket/m2"}]
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "alter_model_version_by_alias",
+                    {
+                        "catalog_name": "cat",
+                        "schema_name": "sch",
+                        "model_name": "m",
+                        "alias": "latest",
+                        "updates": updates,
+                    },
+                )
+                self.assertEqual(
+                    "mock_model_version_altered_by_alias: cat.sch.m "
+                    f"alias=latest with updates {updates}",
+                    result.content[0].text,
+                )
+
+        asyncio.run(_test(self.mcp))
diff --git a/mcp-server/tests/unit/tools/test_schema.py 
b/mcp-server/tests/unit/tools/test_schema.py
index 443c3968dc..172b8d70f5 100644
--- a/mcp-server/tests/unit/tools/test_schema.py
+++ b/mcp-server/tests/unit/tools/test_schema.py
@@ -42,3 +42,72 @@ class TestSchemaTool(unittest.TestCase):
                 self.assertEqual("mock_schemas", result.content[0].text)
 
         asyncio.run(_test_list_schemas(self.mcp))
+
+    def test_create_schema(self):
+        async def _test(mcp_server):
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "create_schema",
+                    {
+                        "catalog_name": "cat",
+                        "name": "sch",
+                        "comment": "c",
+                        "properties": {"k": "v"},
+                    },
+                )
+                self.assertEqual(
+                    "mock_schema_created: cat.sch", result.content[0].text
+                )
+
+        asyncio.run(_test(self.mcp))
+
+    def test_alter_schema(self):
+        async def _test(mcp_server):
+            updates = [{"@type": "setProperty", "property": "k", "value": "v"}]
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "alter_schema",
+                    {
+                        "catalog_name": "cat",
+                        "schema_name": "sch",
+                        "updates": updates,
+                    },
+                )
+                self.assertEqual(
+                    f"mock_schema_altered: cat.sch with updates {updates}",
+                    result.content[0].text,
+                )
+
+        asyncio.run(_test(self.mcp))
+
+    def test_drop_schema(self):
+        async def _test(mcp_server):
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "drop_schema",
+                    {"catalog_name": "cat", "schema_name": "sch"},
+                )
+                self.assertEqual(
+                    "mock_schema_dropped: cat.sch, cascade=False",
+                    result.content[0].text,
+                )
+
+        asyncio.run(_test(self.mcp))
+
+    def test_drop_schema_cascade(self):
+        async def _test(mcp_server):
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "drop_schema",
+                    {
+                        "catalog_name": "cat",
+                        "schema_name": "sch",
+                        "cascade": True,
+                    },
+                )
+                self.assertEqual(
+                    "mock_schema_dropped: cat.sch, cascade=True",
+                    result.content[0].text,
+                )
+
+        asyncio.run(_test(self.mcp))
diff --git a/mcp-server/tests/unit/tools/test_table.py 
b/mcp-server/tests/unit/tools/test_table.py
index 1b426e5512..208cfc928e 100644
--- a/mcp-server/tests/unit/tools/test_table.py
+++ b/mcp-server/tests/unit/tools/test_table.py
@@ -57,3 +57,107 @@ class TestTableTool(unittest.TestCase):
                 self.assertEqual("mock_table", result.content[0].text)
 
         asyncio.run(_test_load_table(self.mcp))
+
+    def test_create_table(self):
+        async def _test(mcp_server):
+            columns = [{"name": "id", "type": "integer", "nullable": False}]
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "create_table",
+                    {
+                        "catalog_name": "cat",
+                        "schema_name": "sch",
+                        "name": "tbl",
+                        "comment": "c",
+                        "columns": columns,
+                        "properties": {"k": "v"},
+                    },
+                )
+                self.assertEqual(
+                    "mock_table_created: cat.sch.tbl, partitioning=None",
+                    result.content[0].text,
+                )
+
+        asyncio.run(_test(self.mcp))
+
+    def test_create_table_with_partitioning(self):
+        async def _test(mcp_server):
+            columns = [{"name": "id", "type": "integer", "nullable": False}]
+            partitioning = [{"strategy": "identity", "fieldName": ["dt"]}]
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "create_table",
+                    {
+                        "catalog_name": "cat",
+                        "schema_name": "sch",
+                        "name": "tbl",
+                        "comment": "c",
+                        "columns": columns,
+                        "properties": {"k": "v"},
+                        "partitioning": partitioning,
+                    },
+                )
+                self.assertEqual(
+                    "mock_table_created: cat.sch.tbl, "
+                    f"partitioning={partitioning}",
+                    result.content[0].text,
+                )
+
+        asyncio.run(_test(self.mcp))
+
+    def test_alter_table(self):
+        async def _test(mcp_server):
+            updates = [{"@type": "rename", "newName": "tbl2"}]
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "alter_table",
+                    {
+                        "catalog_name": "cat",
+                        "schema_name": "sch",
+                        "table_name": "tbl",
+                        "updates": updates,
+                    },
+                )
+                self.assertEqual(
+                    f"mock_table_altered: cat.sch.tbl with updates {updates}",
+                    result.content[0].text,
+                )
+
+        asyncio.run(_test(self.mcp))
+
+    def test_drop_table(self):
+        async def _test(mcp_server):
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "drop_table",
+                    {
+                        "catalog_name": "cat",
+                        "schema_name": "sch",
+                        "table_name": "tbl",
+                    },
+                )
+                self.assertEqual(
+                    "mock_table_dropped: cat.sch.tbl, purge=False",
+                    result.content[0].text,
+                )
+
+        asyncio.run(_test(self.mcp))
+
+    def test_drop_table_purge(self):
+        async def _test(mcp_server):
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "drop_table",
+                    {
+                        "catalog_name": "cat",
+                        "schema_name": "sch",
+                        "table_name": "tbl",
+                        "purge": True,
+                    },
+                )
+                self.assertEqual(
+                    "mock_table_dropped: cat.sch.tbl, purge=True",
+                    result.content[0].text,
+                )
+
+        asyncio.run(_test(self.mcp))
diff --git a/mcp-server/tests/unit/tools/test_tag.py 
b/mcp-server/tests/unit/tools/test_tag.py
index 1bb7046f4c..daff0df295 100644
--- a/mcp-server/tests/unit/tools/test_tag.py
+++ b/mcp-server/tests/unit/tools/test_tag.py
@@ -99,14 +99,50 @@ class TestTagTool(unittest.TestCase):
 
         asyncio.run(_test_disassociate_tag_from_metadata(self.mcp))
 
-    def test_destructive_tag_tools_disabled_by_default(self):
-        async def _test_destructive_tag_tools_disabled_by_default(mcp_server):
+    def test_write_tag_tools_are_exposed(self):
+        """Tag write tools are registered/exposed (not hidden).
+
+        Authorization enforcement is delegated to Gravitino and covered by the
+        live integration test, not asserted here; this only checks exposure.
+        """
+
+        async def _test(mcp_server):
             tool_names = {tool.name for tool in await mcp_server.list_tools()}
+            self.assertIn("create_tag", tool_names)
+            self.assertIn("alter_tag", tool_names)
+            self.assertIn("delete_tag", tool_names)
+
+        asyncio.run(_test(self.mcp))
 
-            self.assertIn("get_tag_by_name", tool_names)
-            self.assertIn("list_of_tags", tool_names)
-            self.assertNotIn("create_tag", tool_names)
-            self.assertNotIn("alter_tag", tool_names)
-            self.assertNotIn("delete_tag", tool_names)
+    def test_create_tag(self):
+        async def _test(mcp_server):
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "create_tag",
+                    {"name": "t1", "comment": "c", "properties": {"k": "v"}},
+                )
+                self.assertEqual("mock_tag_created: t1", 
result.content[0].text)
+
+        asyncio.run(_test(self.mcp))
+
+    def test_alter_tag(self):
+        async def _test(mcp_server):
+            updates = [{"@type": "rename", "newName": "t2"}]
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "alter_tag", {"name": "t1", "updates": updates}
+                )
+                self.assertEqual(
+                    f"mock_tag_altered: t1 with updates {updates}",
+                    result.content[0].text,
+                )
+
+        asyncio.run(_test(self.mcp))
+
+    def test_delete_tag(self):
+        async def _test(mcp_server):
+            async with Client(mcp_server) as client:
+                result = await client.call_tool("delete_tag", {"name": "t1"})
+                self.assertEqual("mock_tag_deleted: t1", 
result.content[0].text)
 
-        asyncio.run(_test_destructive_tag_tools_disabled_by_default(self.mcp))
+        asyncio.run(_test(self.mcp))
diff --git a/mcp-server/tests/unit/tools/test_topic.py 
b/mcp-server/tests/unit/tools/test_topic.py
index 28422d0634..6f3f915bb8 100644
--- a/mcp-server/tests/unit/tools/test_topic.py
+++ b/mcp-server/tests/unit/tools/test_topic.py
@@ -57,3 +57,59 @@ class TestTopicTool(unittest.TestCase):
                 self.assertEqual("mock_topic", result.content[0].text)
 
         asyncio.run(_test_load_topic(self.mcp))
+
+    def test_create_topic(self):
+        async def _test(mcp_server):
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "create_topic",
+                    {
+                        "catalog_name": "cat",
+                        "schema_name": "sch",
+                        "name": "tp",
+                        "comment": "c",
+                        "properties": {"k": "v"},
+                    },
+                )
+                self.assertEqual(
+                    "mock_topic_created: cat.sch.tp", result.content[0].text
+                )
+
+        asyncio.run(_test(self.mcp))
+
+    def test_alter_topic(self):
+        async def _test(mcp_server):
+            updates = [{"@type": "setProperty", "property": "k", "value": "v"}]
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "alter_topic",
+                    {
+                        "catalog_name": "cat",
+                        "schema_name": "sch",
+                        "topic_name": "tp",
+                        "updates": updates,
+                    },
+                )
+                self.assertEqual(
+                    f"mock_topic_altered: cat.sch.tp with updates {updates}",
+                    result.content[0].text,
+                )
+
+        asyncio.run(_test(self.mcp))
+
+    def test_delete_topic(self):
+        async def _test(mcp_server):
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "delete_topic",
+                    {
+                        "catalog_name": "cat",
+                        "schema_name": "sch",
+                        "topic_name": "tp",
+                    },
+                )
+                self.assertEqual(
+                    "mock_topic_deleted: cat.sch.tp", result.content[0].text
+                )
+
+        asyncio.run(_test(self.mcp))

Reply via email to