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
+ )