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

bharos 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 92999dab47 [#12369] fix(mcp-server): use FastMCP.enable() for 
--include-tool-tags (#12368)
92999dab47 is described below

commit 92999dab473650c1234a82d08b8d65e64603e8b5
Author: Bharath Krishna <[email protected]>
AuthorDate: Wed Aug 5 18:50:45 2026 -0700

    [#12369] fix(mcp-server): use FastMCP.enable() for --include-tool-tags 
(#12368)
    
    ### What changes were proposed in this pull request?
    
    Two fixes to `--include-tool-tags`:
    
    1. `_create_gravitino_mcp` passed `include_tags=` to the `FastMCP`
    constructor, which fastmcp 3.4.2 no longer accepts. Replaced with
    `mcp.enable(tags=..., only=True)`.
    2. The flag's help text listed 8 tags while the tools register 12
    (`job`, `partition`, `statistic` and `view` were missing). Rather than
    just correct the list, it is hoisted into `SUPPORTED_TOOL_TAGS` in
    `mcp_server/tools/__init__.py` and the help string is now built from it,
    so the two cannot drift apart again.
    
    Also adds `mcp-server/tests/unit/test_tool_tags.py`. No existing test
    constructed the server with tags, which is why the regression shipped
    unnoticed.
    
    ### Why are the changes needed?
    
    Since #11869 bumped fastmcp 3.2.0 -> 3.4.2, any use of
    `--include-tool-tags` exits immediately:
    
    ```
    TypeError: FastMCP() no longer accepts `include_tags`.
    Use `server.enable(tags=..., only=True)` after creating the server.
    ```
    
    Fix: #12369
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. `--include-tool-tags` starts the server and filters tools again,
    and `--help` now lists all 12 supported tags.
    
    One behaviour worth calling out: a tag matching no tool now yields a
    server exposing no tools, instead of the `TypeError` above. That is the
    allowlist semantics of `enable(..., only=True)`, but it does mean a typo
    fails silently. I kept the existing behaviour rather than widen this PR;
    happy to add explicit tag validation if reviewers would prefer an error,
    which `SUPPORTED_TOOL_TAGS` now makes straightforward.
    
    ### How was this patch tested?
    
    Five new unit tests in `tests/unit/test_tool_tags.py` cover: no tags
    disables filtering, single-tag filtering, multiple tags being unioned,
    an unknown tag, and that `SUPPORTED_TOOL_TAGS` matches the tags the
    tools actually register.
    
    Reverting `server.py` to `main` makes the four filtering tests error on
    the `TypeError`; restoring it makes them pass. The fifth test fails if
    the constant and the registered tags diverge — dropping a single entry
    from `SUPPORTED_TOOL_TAGS` reproduces that. The first test compares the
    unfiltered listing against the union of all 12 tag slices and requires
    every slice to narrow it. I checked both halves are load-bearing by
    injecting the corresponding bug: a default that drops one tag group
    breaks the equality, and a filter that no-ops for a single tag breaks
    the narrowing check while the equality still holds.
    
    Gates, matching `mcp-server/build.gradle.kts`:
    
    - `isort --check mcp_server tests` and `black --check mcp_server tests`
    -> clean
    - `pylint ./tests ./mcp_server` -> 10.00/10
    - `python -m unittest discover -s tests` -> `Ran 172 tests ... OK`
    
    Also started the server manually with `--include-tool-tags catalog`,
    `view` and `job,view,statistic`; all start normally with no traceback,
    where previously each exited with the `TypeError` above.
---
 mcp-server/mcp_server/main.py           |  7 +--
 mcp-server/mcp_server/server.py         | 18 +++----
 mcp-server/mcp_server/tools/__init__.py | 18 +++++++
 mcp-server/tests/unit/test_tool_tags.py | 83 +++++++++++++++++++++++++++++++++
 4 files changed, 112 insertions(+), 14 deletions(-)

diff --git a/mcp-server/mcp_server/main.py b/mcp-server/mcp_server/main.py
index 903d9d30b0..81fabfab2e 100644
--- a/mcp-server/mcp_server/main.py
+++ b/mcp-server/mcp_server/main.py
@@ -21,6 +21,7 @@ import os
 
 from mcp_server.core.setting import DefaultSetting, Setting
 from mcp_server.server import GravitinoMCPServer
+from mcp_server.tools import SUPPORTED_TOOL_TAGS
 
 
 def do_main():
@@ -85,9 +86,9 @@ def _parse_args():
         "--include-tool-tags",
         type=_comma_separated_set,
         default=set(),
-        help="The tool tags to include, separated by commas, support 
tags:[catalog, "
-        "schema, table, topic, model, fileset, tag, policy]. default: empty, "
-        "all tools will be included).",
+        help="The tool tags to include, separated by commas, support tags:"
+        f"[{', '.join(sorted(SUPPORTED_TOOL_TAGS))}]. default: empty, "
+        "all tools will be included.",
     )
 
     parser.add_argument(
diff --git a/mcp-server/mcp_server/server.py b/mcp-server/mcp_server/server.py
index b8909120da..fecfdac828 100644
--- a/mcp-server/mcp_server/server.py
+++ b/mcp-server/mcp_server/server.py
@@ -97,17 +97,13 @@ def _create_lifespan_manager(gravitino_context: 
GravitinoContext):
 
 
 def _create_gravitino_mcp(setting: Setting) -> FastMCP:
-    if setting.tags is not None and len(setting.tags) > 0:
-        mcp = FastMCP(
-            "Gravitino MCP Server",
-            lifespan=_create_lifespan_manager(GravitinoContext(setting)),
-            include_tags=setting.tags,
-        )
-    else:
-        mcp = FastMCP(
-            "Gravitino MCP Server",
-            lifespan=_create_lifespan_manager(GravitinoContext(setting)),
-        )
+    mcp = FastMCP(
+        "Gravitino MCP Server",
+        lifespan=_create_lifespan_manager(GravitinoContext(setting)),
+    )
+    if setting.tags:
+        # Allowlist mode: disable everything, then re-enable the wanted tags.
+        mcp.enable(tags=setting.tags, only=True)
 
     mcp.add_middleware(AuditMiddleware(startup_authorization(setting)))
     mcp.add_middleware(
diff --git a/mcp-server/mcp_server/tools/__init__.py 
b/mcp-server/mcp_server/tools/__init__.py
index 1bbf302271..e15f6ad8d1 100644
--- a/mcp-server/mcp_server/tools/__init__.py
+++ b/mcp-server/mcp_server/tools/__init__.py
@@ -31,6 +31,24 @@ from mcp_server.tools.tag import load_tag_tool
 from mcp_server.tools.topic import load_topic_tools
 from mcp_server.tools.view import load_view_tools
 
+# Mirrors the tags the tools declare; enforced by tests/unit/test_tool_tags.py.
+SUPPORTED_TOOL_TAGS = frozenset(
+    {
+        "catalog",
+        "fileset",
+        "job",
+        "model",
+        "partition",
+        "policy",
+        "schema",
+        "statistic",
+        "table",
+        "tag",
+        "topic",
+        "view",
+    }
+)
+
 
 def load_tools(mcp: FastMCP):
     load_job_tool(mcp)
diff --git a/mcp-server/tests/unit/test_tool_tags.py 
b/mcp-server/tests/unit/test_tool_tags.py
new file mode 100644
index 0000000000..ea4c044e33
--- /dev/null
+++ b/mcp-server/tests/unit/test_tool_tags.py
@@ -0,0 +1,83 @@
+# 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.
+
+"""Tests for the tool filtering driven by --include-tool-tags."""
+
+import asyncio
+import unittest
+from typing import Optional, Set
+
+from mcp_server.client.factory import RESTClientFactory
+from mcp_server.client.plain.plain_rest_client_operation import (
+    PlainRESTClientOperation,
+)
+from mcp_server.core.setting import Setting
+from mcp_server.server import GravitinoMCPServer
+from mcp_server.tools import SUPPORTED_TOOL_TAGS
+from tests.unit.tools import MockOperation
+
+
+class TestIncludeToolTags(unittest.TestCase):
+    """--include-tool-tags acts as an allowlist over the registered tools."""
+
+    def setUp(self):
+        RESTClientFactory.set_rest_client(MockOperation)
+
+    def tearDown(self):
+        RESTClientFactory.set_rest_client(PlainRESTClientOperation)
+
+    def _tools(self, tags: Optional[Set[str]] = None) -> list:
+        setting = Setting(metalake="ml", tags=tags or set())
+        server = GravitinoMCPServer(setting)
+        return asyncio.run(server.mcp.list_tools())
+
+    def _names(self, tags: Optional[Set[str]] = None) -> Set[str]:
+        return {tool.name for tool in self._tools(tags)}
+
+    def _registered_tags(self) -> Set[str]:
+        tags = set()
+        for tool in self._tools():
+            tags.update(tool.tags)
+        return tags
+
+    def test_no_tags_disables_filtering(self):
+        unfiltered = self._names()
+        by_tag = {tag: self._names({tag}) for tag in SUPPORTED_TOOL_TAGS}
+        self.assertEqual(set().union(*by_tag.values()), unfiltered)
+        # Equality alone holds if one tag no-ops, so require each to narrow.
+        for tag, subset in by_tag.items():
+            self.assertLess(subset, unfiltered, tag)
+
+    def test_single_tag_exposes_only_matching_tools(self):
+        tools = self._tools({"view"})
+        self.assertTrue(tools)
+        for tool in tools:
+            self.assertIn("view", tool.tags)
+        self.assertLess(len(tools), len(self._tools()))
+
+    def test_multiple_tags_are_unioned(self):
+        combined = self._names({"view", "schema"})
+        self.assertEqual(
+            combined, self._names({"view"}) | self._names({"schema"})
+        )
+
+    def test_unknown_tag_exposes_no_tools(self):
+        self.assertEqual(self._names({"no_such_tag"}), set())
+
+    def test_supported_tags_constant_matches_registered_tags(self):
+        # --help renders this constant, so this keeps the docs from drifting.
+        self.assertEqual(SUPPORTED_TOOL_TAGS, self._registered_tags())

Reply via email to