Remove tuple tags

This commit is contained in:
Jeremiah Lowin 2025-06-10 19:50:43 -04:00
commit c5fb5f7fd2
4 changed files with 15 additions and 189 deletions

View file

@ -131,14 +131,8 @@ class FastMCP(Generic[LifespanResultT]):
mask_error_details: bool | None = None,
tools: list[Tool | Callable[..., Any]] | None = None,
dependencies: list[str] | None = None,
include_tags: set[str]
| set[tuple[str, ...]]
| set[str | tuple[str, ...]]
| None = None,
exclude_tags: set[str]
| set[tuple[str, ...]]
| set[str | tuple[str, ...]]
| None = None,
include_tags: set[str] | None = None,
exclude_tags: set[str] | None = None,
# ---
# ---
# --- The following arguments are DEPRECATED ---
@ -1681,10 +1675,8 @@ class FastMCP(Generic[LifespanResultT]):
If the component's enabled property is False, always return False.
If both include_tags and exclude_tags are None, return True.
If exclude_tags is provided, check each exclude tag:
- If the exclude tag is a tuple, all tags in the tuple must be present in the input tags to exclude.
- If the exclude tag is a string, it must be present in the input tags to exclude.
If include_tags is provided, check each include tag:
- If the include tag is a tuple, all tags in the tuple must be present in the input tags to include.
- If the include tag is a string, it must be present in the input tags to include.
If include_tags is provided and none of the include tags match, return False.
If include_tags is not provided, return True.
@ -1696,26 +1688,16 @@ class FastMCP(Generic[LifespanResultT]):
return True
if self.exclude_tags is not None:
for etag in self.exclude_tags:
if isinstance(etag, tuple):
if all(et in component.tags for et in etag):
return False
else:
if etag in component.tags:
return False
if any(etag in component.tags for etag in self.exclude_tags):
return False
if self.include_tags is not None:
for itag in self.include_tags:
if isinstance(itag, tuple):
if all(it in component.tags for it in itag):
return True
else:
if itag in component.tags:
return True
if any(itag in component.tags for itag in self.include_tags):
return True
else:
return False
return False
else:
return True
return True
class MountedServer:

View file

@ -235,44 +235,27 @@ class Settings(BaseSettings):
] = None
include_tags: Annotated[
set[str] | set[tuple[str, ...]] | set[str | tuple[str, ...]] | None,
set[str] | None,
Field(
default=None,
description=inspect.cleandoc(
"""
If provided, only components that match these tags will be
exposed to clients. This can be a set of tags or tuples of tags.
A component is considered to match if ANY of its tags match ANY
of the tags in the set, or if any combination of its tags match
ALL of the tags in any tuple in the set.
For example, if include_tags is set to {"tag1", ("tag2",
"tag3")}, then a component with tags {"tag1", "tag4"} or
{"tag2", "tag3", "tag4"} will be included, but a component with
tags {"tag2", "tag4"} will not be included.
exposed to clients. A component is considered to match if ANY of
its tags match ANY of the tags in the set.
"""
),
),
] = None
exclude_tags: Annotated[
set[str] | set[tuple[str, ...]] | set[str | tuple[str, ...]] | None,
set[str] | None,
Field(
default=None,
description=inspect.cleandoc(
"""
If provided, components that match these tags will be excluded
from the server. This can be a set of tags or tuples of tags.
This is applied after include_tags, so if a component matches
both include_tags and exclude_tags, it will be excluded.
A component is considered to match if ANY of its tags match ANY
of the tags in the set, or if any combination of its tags match
ALL of the tags in any tuple in the set.
For example, if exclude_tags is set to {"tag1", ("tag2",
"tag3")}, then a component with tags {"tag1", "tag4"} or
{"tag2", "tag3", "tag4"} will be excluded, but a component with
tags {"tag2", "tag4"} will not be excluded.
from the server. A component is considered to match if ANY of
its tags match ANY of the tags in the set.
"""
),
),

View file

@ -1261,20 +1261,6 @@ class TestShouldIncludeComponent:
result = mcp._should_enable_component(tool)
assert result is True
def test_exclude_tuple_all_present_returns_false(self):
"""Test that when all tags in exclude tuple are present, returns False."""
tool = Tool(name="test_tool", tags={"tag1", "tag2", "tag3"}, parameters={})
mcp = FastMCP(tools=[tool], exclude_tags={("tag1", "tag2")})
result = mcp._should_enable_component(tool)
assert result is False
def test_exclude_tuple_partial_present_returns_true(self):
"""Test that when only some tags in exclude tuple are present, returns True."""
tool = Tool(name="test_tool", tags={"tag1", "tag3"}, parameters={})
mcp = FastMCP(tools=[tool], exclude_tags={("tag1", "tag2")})
result = mcp._should_enable_component(tool)
assert result is True
def test_multiple_exclude_tags_any_match_returns_false(self):
"""Test that when any exclude tag matches, returns False."""
tool = Tool(name="test_tool", tags={"tag1", "tag2", "tag3"}, parameters={})
@ -1300,20 +1286,6 @@ class TestShouldIncludeComponent:
result = mcp._should_enable_component(tool)
assert result is False
def test_include_tuple_all_present_returns_true(self):
"""Test that when all tags in include tuple are present, returns True."""
tool = Tool(name="test_tool", tags={"tag1", "tag2", "tag3"}, parameters={})
mcp = FastMCP(tools=[tool], include_tags={("tag1", "tag2")})
result = mcp._should_enable_component(tool)
assert result is True
def test_include_tuple_partial_present_returns_false(self):
"""Test that when only some tags in include tuple are present, returns False."""
tool = Tool(name="test_tool", tags={"tag1", "tag3"}, parameters={})
mcp = FastMCP(tools=[tool], include_tags={("tag1", "tag2")})
result = mcp._should_enable_component(tool)
assert result is False
def test_multiple_include_tags_any_match_returns_true(self):
"""Test that when any include tag matches, returns True."""
tool = Tool(name="test_tool", tags={"tag1", "tag2", "tag3"}, parameters={})
@ -1339,79 +1311,6 @@ class TestShouldIncludeComponent:
result = mcp._should_enable_component(tool)
assert result is False
def test_mixed_string_and_tuple_exclude_tags(self):
"""Test exclude tags with both string and tuple formats."""
# Should be excluded because "tag1" is present
tool1 = Tool(
name="test_tool", tags={"tag1", "tag2", "tag3", "tag4"}, parameters={}
)
mcp1 = FastMCP(tools=[tool1], exclude_tags={"tag1", ("tag2", "tag3")})
result = mcp1._should_enable_component(tool1)
assert result is False
# Remove tag1, should still be excluded because both tag2 and tag3 are present
tool2 = Tool(name="test_tool", tags={"tag2", "tag3", "tag4"}, parameters={})
mcp2 = FastMCP(tools=[tool2], exclude_tags={"tag1", ("tag2", "tag3")})
result = mcp2._should_enable_component(tool2)
assert result is False
# Remove tag2, should not be excluded
tool3 = Tool(
name="test_tool", tags={"tag1_removed", "tag3", "tag4"}, parameters={}
)
mcp3 = FastMCP(tools=[tool3], exclude_tags={("tag2", "tag3")})
result = mcp3._should_enable_component(tool3)
assert result is True
def test_mixed_string_and_tuple_include_tags(self):
"""Test include tags with both string and tuple formats."""
# Should be included because both tag1 and tag2 are present (tuple match)
tool1 = Tool(name="test_tool", tags={"tag1", "tag2"}, parameters={})
mcp1 = FastMCP(tools=[tool1], include_tags={"not_present", ("tag1", "tag2")})
result = mcp1._should_enable_component(tool1)
assert result is True
# Should be included because tag1 is present (string match)
tool2 = Tool(name="test_tool", tags={"tag1", "tag2"}, parameters={})
mcp2 = FastMCP(
tools=[tool2], include_tags={"tag1", ("not_present1", "not_present2")}
)
result = mcp2._should_enable_component(tool2)
assert result is True
# Should not be included because no conditions are met
tool3 = Tool(name="test_tool", tags={"tag1", "tag2"}, parameters={})
mcp3 = FastMCP(
tools=[tool3],
include_tags={"not_present", ("not_present1", "not_present2")},
)
result = mcp3._should_enable_component(tool3)
assert result is False
def test_complex_scenario_with_both_filters(self):
"""Test complex scenario with both include and exclude filters."""
# Should be excluded despite matching include conditions
tool1 = Tool(
name="test_tool", tags={"api", "read", "admin", "sensitive"}, parameters={}
)
mcp1 = FastMCP(
tools=[tool1],
include_tags={"api", ("read", "admin")},
exclude_tags={"sensitive"},
)
result = mcp1._should_enable_component(tool1)
assert result is False
# Remove sensitive tag, should now be included
tool2 = Tool(name="test_tool", tags={"api", "read", "admin"}, parameters={})
mcp2 = FastMCP(
tools=[tool2],
include_tags={"api", ("read", "admin")},
exclude_tags={"sensitive"},
)
result = mcp2._should_enable_component(tool2)
assert result is True
def test_empty_include_exclude_sets(self):
"""Test behavior with empty include/exclude sets."""
# Empty include set means nothing matches
@ -1439,27 +1338,3 @@ class TestShouldIncludeComponent:
mcp2 = FastMCP(tools=[tool2], exclude_tags={"bad_tag"})
result = mcp2._should_enable_component(tool2)
assert result is True
# Tuple filters with empty tags
tool3 = Tool(name="test_tool", tags=set(), parameters={})
mcp3 = FastMCP(tools=[tool3], include_tags={("tag1", "tag2")})
result = mcp3._should_enable_component(tool3)
assert result is False
tool4 = Tool(name="test_tool", tags=set(), parameters={})
mcp4 = FastMCP(tools=[tool4], exclude_tags={("tag1", "tag2")})
result = mcp4._should_enable_component(tool4)
assert result is True
def test_single_element_tuples(self):
"""Test behavior with single-element tuples."""
# Single-element tuple should behave like a string
tool1 = Tool(name="test_tool", tags={"tag1", "tag2"}, parameters={})
mcp1 = FastMCP(tools=[tool1], include_tags={("tag1",)})
result = mcp1._should_enable_component(tool1)
assert result is True
tool2 = Tool(name="test_tool", tags={"tag1", "tag2"}, parameters={})
mcp2 = FastMCP(tools=[tool2], exclude_tags={("tag1",)})
result = mcp2._should_enable_component(tool2)
assert result is False

View file

@ -143,13 +143,6 @@ class TestToolTags:
tools = await client.list_tools()
assert {t.name for t in tools} == {"tool_1"}
async def test_include_tags_tuple(self):
mcp = self.create_server(include_tags={("a", "b")})
async with Client(mcp) as client:
tools = await client.list_tools()
assert {t.name for t in tools} == {"tool_1"}
async def test_exclude_tags_all_tools(self):
mcp = self.create_server(exclude_tags={"a", "b"})
@ -164,13 +157,6 @@ class TestToolTags:
tools = await client.list_tools()
assert {t.name for t in tools} == {"tool_2"}
async def test_exclude_tags_tuple(self):
mcp = self.create_server(exclude_tags={("a", "b")})
async with Client(mcp) as client:
tools = await client.list_tools()
assert {t.name for t in tools} == {"tool_2"}
async def test_exclude_precedence(self):
mcp = self.create_server(exclude_tags={"a"}, include_tags={"b"})