mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-22 13:34:17 +02:00
Remove tuple tags
This commit is contained in:
parent
ec9fab070c
commit
c5fb5f7fd2
4 changed files with 15 additions and 189 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
"""
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"})
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue