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