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

jerryshao 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 a880b41eef [#13120] fix(mcp): return tag details when listing metadata 
tags (#13121)
a880b41eef is described below

commit a880b41eef8a1ffc33e1db41ce102e99b8842612
Author: Qi Yu <[email protected]>
AuthorDate: Mon Sep 14 14:40:53 2026 +0800

    [#13120] fix(mcp): return tag details when listing metadata tags (#13121)
    
    ### What changes were proposed in this pull request?
    
    Extract `tags` from the detailed metadata tag list response instead of
    `names`. Add regression tests for complete tag details, empty results,
    server errors, and the MCP tool response.
    
    ### Why are the changes needed?
    
    The request uses `details=true`, so the REST API returns `tags`. Reading
    the absent `names` field silently returns `[]` for objects with
    associated tags.
    
    Fix: #13120
    
    ### Does this PR introduce _any_ user-facing change?
    
    `list_tags_for_metadata` returns the documented tag objects, including
    comments, properties, audit information, and inheritance flags.
    
    ### How was this patch tested?
    
    - The REST client and MCP tool regression tests fail before the fix and
    pass afterward.
    - MCP unit suite: 272 tests and 9 subtests passed.
    - `./gradlew :mcp-server:spotlessApply`, targeted Pylint (10/10), and
    `git diff --check` passed.
    - HTTP responses are simulated with `httpx.MockTransport`; no live
    Gravitino server is required for these tests.
---
 .../plain/plain_rest_client_tag_operation.py       |   2 +-
 mcp-server/tests/unit/client/test_tag_operation.py | 130 +++++++++++++++++++++
 2 files changed, 131 insertions(+), 1 deletion(-)

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 ab2b20c04b..7260ef66b1 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
@@ -93,7 +93,7 @@ class PlainRESTClientTagOperation(TagOperation):
             f"/objects/{encode_path_segment(metadata_type)}"
             f"/{encode_path_segment(metadata_full_name)}/tags?details=true"
         )
-        return extract_content_from_response(response, "names", [])
+        return extract_content_from_response(response, "tags", [])
 
     async def list_metadata_by_tag(self, tag_name: str) -> str:
         response = await self.rest_client.get(
diff --git a/mcp-server/tests/unit/client/test_tag_operation.py 
b/mcp-server/tests/unit/client/test_tag_operation.py
new file mode 100644
index 0000000000..0298b35f17
--- /dev/null
+++ b/mcp-server/tests/unit/client/test_tag_operation.py
@@ -0,0 +1,130 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Regression tests for the metadata tag list REST response contract."""
+
+import json
+import unittest
+from unittest.mock import patch
+
+import httpx
+from fastmcp import Client
+
+from mcp_server.client.factory import RESTClientFactory
+from mcp_server.client.plain.exception import GravitinoException
+from mcp_server.client.plain.plain_rest_client_tag_operation import (
+    PlainRESTClientTagOperation,
+)
+from mcp_server.core import Setting
+from mcp_server.server import GravitinoMCPServer
+from tests.unit.tools import MockOperation
+
+
+class TestTagOperation(unittest.IsolatedAsyncioTestCase):
+    def setUp(self):
+        self.tags = [
+            {
+                "name": "firstTag",
+                "comment": "Inherited governance tag",
+                "properties": {"classification": "internal"},
+                "audit": {
+                    "creator": "admin",
+                    "createTime": "2026-09-13T10:00:00Z",
+                },
+                "inherited": True,
+            },
+            {
+                "name": "tableTag",
+                "comment": "Direct table tag",
+                "properties": {},
+                "audit": {
+                    "creator": "admin",
+                    "createTime": "2026-09-13T11:00:00Z",
+                },
+                "inherited": False,
+            },
+        ]
+
+    def _client(self, body):
+        def respond(request):
+            self.assertEqual(request.method, "GET")
+            self.assertEqual(
+                request.url.path,
+                
"/api/metalakes/acme/objects/table/iceberg_s3.sales.orders/tags",
+            )
+            self.assertEqual(dict(request.url.params), {"details": "true"})
+            return httpx.Response(200, json=body)
+
+        return httpx.AsyncClient(
+            base_url="http://localhost:8090";,
+            transport=httpx.MockTransport(respond),
+        )
+
+    async def test_list_tags_preserves_complete_tag_details(self):
+        async with self._client({"code": 0, "tags": self.tags}) as client:
+            operation = PlainRESTClientTagOperation("acme", client)
+            result = await operation.list_tags_for_metadata(
+                "iceberg_s3.sales.orders", "table"
+            )
+            self.assertEqual(json.loads(result), self.tags)
+
+    async def test_list_tags_without_associations(self):
+        async with self._client({"code": 0, "tags": []}) as client:
+            operation = PlainRESTClientTagOperation("acme", client)
+            result = await operation.list_tags_for_metadata(
+                "iceberg_s3.sales.orders", "table"
+            )
+            self.assertEqual(json.loads(result), [])
+
+    async def test_list_tags_propagates_server_error(self):
+        body = {
+            "code": 1003,
+            "type": "NoSuchMetadataObjectException",
+            "message": "Metadata object does not exist",
+        }
+        async with self._client(body) as client:
+            operation = PlainRESTClientTagOperation("acme", client)
+            with self.assertRaisesRegex(
+                GravitinoException, "Metadata object does not exist"
+            ):
+                await operation.list_tags_for_metadata(
+                    "iceberg_s3.sales.orders", "table"
+                )
+
+    async def test_mcp_tool_returns_rest_tag_details(self):
+        async with self._client({"code": 0, "tags": self.tags}) as rest_client:
+            operation = PlainRESTClientTagOperation("acme", rest_client)
+            with (
+                patch.object(
+                    RESTClientFactory, "_rest_client_class", MockOperation
+                ),
+                patch.object(
+                    MockOperation, "as_tag_operation", return_value=operation
+                ),
+            ):
+                server = GravitinoMCPServer(Setting("acme"))
+                async with Client(server.mcp) as client:
+                    result = await client.call_tool(
+                        "list_tags_for_metadata",
+                        {
+                            "metadata_full_name": "iceberg_s3.sales.orders",
+                            "metadata_type": "table",
+                        },
+                    )
+                    self.assertEqual(
+                        json.loads(result.content[0].text), self.tags
+                    )

Reply via email to