Spaces:
Running
Running
Jeremiah Lowin commited on
Commit ·
23dba76
1
Parent(s): 4351aa1
Remove tuple tags
Browse files- src/fastmcp/server/server.py +9 -27
- src/fastmcp/settings.py +6 -23
- tests/server/test_server.py +0 -125
- tests/server/test_server_interactions.py +0 -14
src/fastmcp/server/server.py
CHANGED
|
@@ -131,14 +131,8 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 131 |
mask_error_details: bool | None = None,
|
| 132 |
tools: list[Tool | Callable[..., Any]] | None = None,
|
| 133 |
dependencies: list[str] | None = None,
|
| 134 |
-
include_tags: set[str]
|
| 135 |
-
|
| 136 |
-
| set[str | tuple[str, ...]]
|
| 137 |
-
| None = None,
|
| 138 |
-
exclude_tags: set[str]
|
| 139 |
-
| set[tuple[str, ...]]
|
| 140 |
-
| set[str | tuple[str, ...]]
|
| 141 |
-
| None = None,
|
| 142 |
# ---
|
| 143 |
# ---
|
| 144 |
# --- The following arguments are DEPRECATED ---
|
|
@@ -1681,10 +1675,8 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 1681 |
• If the component's enabled property is False, always return False.
|
| 1682 |
• If both include_tags and exclude_tags are None, return True.
|
| 1683 |
• If exclude_tags is provided, check each exclude tag:
|
| 1684 |
-
- If the exclude tag is a tuple, all tags in the tuple must be present in the input tags to exclude.
|
| 1685 |
- If the exclude tag is a string, it must be present in the input tags to exclude.
|
| 1686 |
• If include_tags is provided, check each include tag:
|
| 1687 |
-
- If the include tag is a tuple, all tags in the tuple must be present in the input tags to include.
|
| 1688 |
- If the include tag is a string, it must be present in the input tags to include.
|
| 1689 |
• If include_tags is provided and none of the include tags match, return False.
|
| 1690 |
• If include_tags is not provided, return True.
|
|
@@ -1696,26 +1688,16 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 1696 |
return True
|
| 1697 |
|
| 1698 |
if self.exclude_tags is not None:
|
| 1699 |
-
for etag in self.exclude_tags:
|
| 1700 |
-
|
| 1701 |
-
if all(et in component.tags for et in etag):
|
| 1702 |
-
return False
|
| 1703 |
-
else:
|
| 1704 |
-
if etag in component.tags:
|
| 1705 |
-
return False
|
| 1706 |
|
| 1707 |
if self.include_tags is not None:
|
| 1708 |
-
for itag in self.include_tags:
|
| 1709 |
-
|
| 1710 |
-
|
| 1711 |
-
|
| 1712 |
-
else:
|
| 1713 |
-
if itag in component.tags:
|
| 1714 |
-
return True
|
| 1715 |
|
| 1716 |
-
|
| 1717 |
-
else:
|
| 1718 |
-
return True
|
| 1719 |
|
| 1720 |
|
| 1721 |
class MountedServer:
|
|
|
|
| 131 |
mask_error_details: bool | None = None,
|
| 132 |
tools: list[Tool | Callable[..., Any]] | None = None,
|
| 133 |
dependencies: list[str] | None = None,
|
| 134 |
+
include_tags: set[str] | None = None,
|
| 135 |
+
exclude_tags: set[str] | None = None,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
# ---
|
| 137 |
# ---
|
| 138 |
# --- The following arguments are DEPRECATED ---
|
|
|
|
| 1675 |
• If the component's enabled property is False, always return False.
|
| 1676 |
• If both include_tags and exclude_tags are None, return True.
|
| 1677 |
• If exclude_tags is provided, check each exclude tag:
|
|
|
|
| 1678 |
- If the exclude tag is a string, it must be present in the input tags to exclude.
|
| 1679 |
• If include_tags is provided, check each include tag:
|
|
|
|
| 1680 |
- If the include tag is a string, it must be present in the input tags to include.
|
| 1681 |
• If include_tags is provided and none of the include tags match, return False.
|
| 1682 |
• If include_tags is not provided, return True.
|
|
|
|
| 1688 |
return True
|
| 1689 |
|
| 1690 |
if self.exclude_tags is not None:
|
| 1691 |
+
if any(etag in component.tags for etag in self.exclude_tags):
|
| 1692 |
+
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1693 |
|
| 1694 |
if self.include_tags is not None:
|
| 1695 |
+
if any(itag in component.tags for itag in self.include_tags):
|
| 1696 |
+
return True
|
| 1697 |
+
else:
|
| 1698 |
+
return False
|
|
|
|
|
|
|
|
|
|
| 1699 |
|
| 1700 |
+
return True
|
|
|
|
|
|
|
| 1701 |
|
| 1702 |
|
| 1703 |
class MountedServer:
|
src/fastmcp/settings.py
CHANGED
|
@@ -235,44 +235,27 @@ class Settings(BaseSettings):
|
|
| 235 |
] = None
|
| 236 |
|
| 237 |
include_tags: Annotated[
|
| 238 |
-
set[str] |
|
| 239 |
Field(
|
| 240 |
default=None,
|
| 241 |
description=inspect.cleandoc(
|
| 242 |
"""
|
| 243 |
If provided, only components that match these tags will be
|
| 244 |
-
exposed to clients.
|
| 245 |
-
|
| 246 |
-
of the tags in the set, or if any combination of its tags match
|
| 247 |
-
ALL of the tags in any tuple in the set.
|
| 248 |
-
|
| 249 |
-
For example, if include_tags is set to {"tag1", ("tag2",
|
| 250 |
-
"tag3")}, then a component with tags {"tag1", "tag4"} or
|
| 251 |
-
{"tag2", "tag3", "tag4"} will be included, but a component with
|
| 252 |
-
tags {"tag2", "tag4"} will not be included.
|
| 253 |
"""
|
| 254 |
),
|
| 255 |
),
|
| 256 |
] = None
|
| 257 |
exclude_tags: Annotated[
|
| 258 |
-
set[str] |
|
| 259 |
Field(
|
| 260 |
default=None,
|
| 261 |
description=inspect.cleandoc(
|
| 262 |
"""
|
| 263 |
If provided, components that match these tags will be excluded
|
| 264 |
-
from the server.
|
| 265 |
-
|
| 266 |
-
both include_tags and exclude_tags, it will be excluded.
|
| 267 |
-
|
| 268 |
-
A component is considered to match if ANY of its tags match ANY
|
| 269 |
-
of the tags in the set, or if any combination of its tags match
|
| 270 |
-
ALL of the tags in any tuple in the set.
|
| 271 |
-
|
| 272 |
-
For example, if exclude_tags is set to {"tag1", ("tag2",
|
| 273 |
-
"tag3")}, then a component with tags {"tag1", "tag4"} or
|
| 274 |
-
{"tag2", "tag3", "tag4"} will be excluded, but a component with
|
| 275 |
-
tags {"tag2", "tag4"} will not be excluded.
|
| 276 |
"""
|
| 277 |
),
|
| 278 |
),
|
|
|
|
| 235 |
] = None
|
| 236 |
|
| 237 |
include_tags: Annotated[
|
| 238 |
+
set[str] | None,
|
| 239 |
Field(
|
| 240 |
default=None,
|
| 241 |
description=inspect.cleandoc(
|
| 242 |
"""
|
| 243 |
If provided, only components that match these tags will be
|
| 244 |
+
exposed to clients. A component is considered to match if ANY of
|
| 245 |
+
its tags match ANY of the tags in the set.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 246 |
"""
|
| 247 |
),
|
| 248 |
),
|
| 249 |
] = None
|
| 250 |
exclude_tags: Annotated[
|
| 251 |
+
set[str] | None,
|
| 252 |
Field(
|
| 253 |
default=None,
|
| 254 |
description=inspect.cleandoc(
|
| 255 |
"""
|
| 256 |
If provided, components that match these tags will be excluded
|
| 257 |
+
from the server. A component is considered to match if ANY of
|
| 258 |
+
its tags match ANY of the tags in the set.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 259 |
"""
|
| 260 |
),
|
| 261 |
),
|
tests/server/test_server.py
CHANGED
|
@@ -1261,20 +1261,6 @@ class TestShouldIncludeComponent:
|
|
| 1261 |
result = mcp._should_enable_component(tool)
|
| 1262 |
assert result is True
|
| 1263 |
|
| 1264 |
-
def test_exclude_tuple_all_present_returns_false(self):
|
| 1265 |
-
"""Test that when all tags in exclude tuple are present, returns False."""
|
| 1266 |
-
tool = Tool(name="test_tool", tags={"tag1", "tag2", "tag3"}, parameters={})
|
| 1267 |
-
mcp = FastMCP(tools=[tool], exclude_tags={("tag1", "tag2")})
|
| 1268 |
-
result = mcp._should_enable_component(tool)
|
| 1269 |
-
assert result is False
|
| 1270 |
-
|
| 1271 |
-
def test_exclude_tuple_partial_present_returns_true(self):
|
| 1272 |
-
"""Test that when only some tags in exclude tuple are present, returns True."""
|
| 1273 |
-
tool = Tool(name="test_tool", tags={"tag1", "tag3"}, parameters={})
|
| 1274 |
-
mcp = FastMCP(tools=[tool], exclude_tags={("tag1", "tag2")})
|
| 1275 |
-
result = mcp._should_enable_component(tool)
|
| 1276 |
-
assert result is True
|
| 1277 |
-
|
| 1278 |
def test_multiple_exclude_tags_any_match_returns_false(self):
|
| 1279 |
"""Test that when any exclude tag matches, returns False."""
|
| 1280 |
tool = Tool(name="test_tool", tags={"tag1", "tag2", "tag3"}, parameters={})
|
|
@@ -1300,20 +1286,6 @@ class TestShouldIncludeComponent:
|
|
| 1300 |
result = mcp._should_enable_component(tool)
|
| 1301 |
assert result is False
|
| 1302 |
|
| 1303 |
-
def test_include_tuple_all_present_returns_true(self):
|
| 1304 |
-
"""Test that when all tags in include tuple are present, returns True."""
|
| 1305 |
-
tool = Tool(name="test_tool", tags={"tag1", "tag2", "tag3"}, parameters={})
|
| 1306 |
-
mcp = FastMCP(tools=[tool], include_tags={("tag1", "tag2")})
|
| 1307 |
-
result = mcp._should_enable_component(tool)
|
| 1308 |
-
assert result is True
|
| 1309 |
-
|
| 1310 |
-
def test_include_tuple_partial_present_returns_false(self):
|
| 1311 |
-
"""Test that when only some tags in include tuple are present, returns False."""
|
| 1312 |
-
tool = Tool(name="test_tool", tags={"tag1", "tag3"}, parameters={})
|
| 1313 |
-
mcp = FastMCP(tools=[tool], include_tags={("tag1", "tag2")})
|
| 1314 |
-
result = mcp._should_enable_component(tool)
|
| 1315 |
-
assert result is False
|
| 1316 |
-
|
| 1317 |
def test_multiple_include_tags_any_match_returns_true(self):
|
| 1318 |
"""Test that when any include tag matches, returns True."""
|
| 1319 |
tool = Tool(name="test_tool", tags={"tag1", "tag2", "tag3"}, parameters={})
|
|
@@ -1339,79 +1311,6 @@ class TestShouldIncludeComponent:
|
|
| 1339 |
result = mcp._should_enable_component(tool)
|
| 1340 |
assert result is False
|
| 1341 |
|
| 1342 |
-
def test_mixed_string_and_tuple_exclude_tags(self):
|
| 1343 |
-
"""Test exclude tags with both string and tuple formats."""
|
| 1344 |
-
# Should be excluded because "tag1" is present
|
| 1345 |
-
tool1 = Tool(
|
| 1346 |
-
name="test_tool", tags={"tag1", "tag2", "tag3", "tag4"}, parameters={}
|
| 1347 |
-
)
|
| 1348 |
-
mcp1 = FastMCP(tools=[tool1], exclude_tags={"tag1", ("tag2", "tag3")})
|
| 1349 |
-
result = mcp1._should_enable_component(tool1)
|
| 1350 |
-
assert result is False
|
| 1351 |
-
|
| 1352 |
-
# Remove tag1, should still be excluded because both tag2 and tag3 are present
|
| 1353 |
-
tool2 = Tool(name="test_tool", tags={"tag2", "tag3", "tag4"}, parameters={})
|
| 1354 |
-
mcp2 = FastMCP(tools=[tool2], exclude_tags={"tag1", ("tag2", "tag3")})
|
| 1355 |
-
result = mcp2._should_enable_component(tool2)
|
| 1356 |
-
assert result is False
|
| 1357 |
-
|
| 1358 |
-
# Remove tag2, should not be excluded
|
| 1359 |
-
tool3 = Tool(
|
| 1360 |
-
name="test_tool", tags={"tag1_removed", "tag3", "tag4"}, parameters={}
|
| 1361 |
-
)
|
| 1362 |
-
mcp3 = FastMCP(tools=[tool3], exclude_tags={("tag2", "tag3")})
|
| 1363 |
-
result = mcp3._should_enable_component(tool3)
|
| 1364 |
-
assert result is True
|
| 1365 |
-
|
| 1366 |
-
def test_mixed_string_and_tuple_include_tags(self):
|
| 1367 |
-
"""Test include tags with both string and tuple formats."""
|
| 1368 |
-
# Should be included because both tag1 and tag2 are present (tuple match)
|
| 1369 |
-
tool1 = Tool(name="test_tool", tags={"tag1", "tag2"}, parameters={})
|
| 1370 |
-
mcp1 = FastMCP(tools=[tool1], include_tags={"not_present", ("tag1", "tag2")})
|
| 1371 |
-
result = mcp1._should_enable_component(tool1)
|
| 1372 |
-
assert result is True
|
| 1373 |
-
|
| 1374 |
-
# Should be included because tag1 is present (string match)
|
| 1375 |
-
tool2 = Tool(name="test_tool", tags={"tag1", "tag2"}, parameters={})
|
| 1376 |
-
mcp2 = FastMCP(
|
| 1377 |
-
tools=[tool2], include_tags={"tag1", ("not_present1", "not_present2")}
|
| 1378 |
-
)
|
| 1379 |
-
result = mcp2._should_enable_component(tool2)
|
| 1380 |
-
assert result is True
|
| 1381 |
-
|
| 1382 |
-
# Should not be included because no conditions are met
|
| 1383 |
-
tool3 = Tool(name="test_tool", tags={"tag1", "tag2"}, parameters={})
|
| 1384 |
-
mcp3 = FastMCP(
|
| 1385 |
-
tools=[tool3],
|
| 1386 |
-
include_tags={"not_present", ("not_present1", "not_present2")},
|
| 1387 |
-
)
|
| 1388 |
-
result = mcp3._should_enable_component(tool3)
|
| 1389 |
-
assert result is False
|
| 1390 |
-
|
| 1391 |
-
def test_complex_scenario_with_both_filters(self):
|
| 1392 |
-
"""Test complex scenario with both include and exclude filters."""
|
| 1393 |
-
# Should be excluded despite matching include conditions
|
| 1394 |
-
tool1 = Tool(
|
| 1395 |
-
name="test_tool", tags={"api", "read", "admin", "sensitive"}, parameters={}
|
| 1396 |
-
)
|
| 1397 |
-
mcp1 = FastMCP(
|
| 1398 |
-
tools=[tool1],
|
| 1399 |
-
include_tags={"api", ("read", "admin")},
|
| 1400 |
-
exclude_tags={"sensitive"},
|
| 1401 |
-
)
|
| 1402 |
-
result = mcp1._should_enable_component(tool1)
|
| 1403 |
-
assert result is False
|
| 1404 |
-
|
| 1405 |
-
# Remove sensitive tag, should now be included
|
| 1406 |
-
tool2 = Tool(name="test_tool", tags={"api", "read", "admin"}, parameters={})
|
| 1407 |
-
mcp2 = FastMCP(
|
| 1408 |
-
tools=[tool2],
|
| 1409 |
-
include_tags={"api", ("read", "admin")},
|
| 1410 |
-
exclude_tags={"sensitive"},
|
| 1411 |
-
)
|
| 1412 |
-
result = mcp2._should_enable_component(tool2)
|
| 1413 |
-
assert result is True
|
| 1414 |
-
|
| 1415 |
def test_empty_include_exclude_sets(self):
|
| 1416 |
"""Test behavior with empty include/exclude sets."""
|
| 1417 |
# Empty include set means nothing matches
|
|
@@ -1439,27 +1338,3 @@ class TestShouldIncludeComponent:
|
|
| 1439 |
mcp2 = FastMCP(tools=[tool2], exclude_tags={"bad_tag"})
|
| 1440 |
result = mcp2._should_enable_component(tool2)
|
| 1441 |
assert result is True
|
| 1442 |
-
|
| 1443 |
-
# Tuple filters with empty tags
|
| 1444 |
-
tool3 = Tool(name="test_tool", tags=set(), parameters={})
|
| 1445 |
-
mcp3 = FastMCP(tools=[tool3], include_tags={("tag1", "tag2")})
|
| 1446 |
-
result = mcp3._should_enable_component(tool3)
|
| 1447 |
-
assert result is False
|
| 1448 |
-
|
| 1449 |
-
tool4 = Tool(name="test_tool", tags=set(), parameters={})
|
| 1450 |
-
mcp4 = FastMCP(tools=[tool4], exclude_tags={("tag1", "tag2")})
|
| 1451 |
-
result = mcp4._should_enable_component(tool4)
|
| 1452 |
-
assert result is True
|
| 1453 |
-
|
| 1454 |
-
def test_single_element_tuples(self):
|
| 1455 |
-
"""Test behavior with single-element tuples."""
|
| 1456 |
-
# Single-element tuple should behave like a string
|
| 1457 |
-
tool1 = Tool(name="test_tool", tags={"tag1", "tag2"}, parameters={})
|
| 1458 |
-
mcp1 = FastMCP(tools=[tool1], include_tags={("tag1",)})
|
| 1459 |
-
result = mcp1._should_enable_component(tool1)
|
| 1460 |
-
assert result is True
|
| 1461 |
-
|
| 1462 |
-
tool2 = Tool(name="test_tool", tags={"tag1", "tag2"}, parameters={})
|
| 1463 |
-
mcp2 = FastMCP(tools=[tool2], exclude_tags={("tag1",)})
|
| 1464 |
-
result = mcp2._should_enable_component(tool2)
|
| 1465 |
-
assert result is False
|
|
|
|
| 1261 |
result = mcp._should_enable_component(tool)
|
| 1262 |
assert result is True
|
| 1263 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1264 |
def test_multiple_exclude_tags_any_match_returns_false(self):
|
| 1265 |
"""Test that when any exclude tag matches, returns False."""
|
| 1266 |
tool = Tool(name="test_tool", tags={"tag1", "tag2", "tag3"}, parameters={})
|
|
|
|
| 1286 |
result = mcp._should_enable_component(tool)
|
| 1287 |
assert result is False
|
| 1288 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1289 |
def test_multiple_include_tags_any_match_returns_true(self):
|
| 1290 |
"""Test that when any include tag matches, returns True."""
|
| 1291 |
tool = Tool(name="test_tool", tags={"tag1", "tag2", "tag3"}, parameters={})
|
|
|
|
| 1311 |
result = mcp._should_enable_component(tool)
|
| 1312 |
assert result is False
|
| 1313 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1314 |
def test_empty_include_exclude_sets(self):
|
| 1315 |
"""Test behavior with empty include/exclude sets."""
|
| 1316 |
# Empty include set means nothing matches
|
|
|
|
| 1338 |
mcp2 = FastMCP(tools=[tool2], exclude_tags={"bad_tag"})
|
| 1339 |
result = mcp2._should_enable_component(tool2)
|
| 1340 |
assert result is True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/server/test_server_interactions.py
CHANGED
|
@@ -143,13 +143,6 @@ class TestToolTags:
|
|
| 143 |
tools = await client.list_tools()
|
| 144 |
assert {t.name for t in tools} == {"tool_1"}
|
| 145 |
|
| 146 |
-
async def test_include_tags_tuple(self):
|
| 147 |
-
mcp = self.create_server(include_tags={("a", "b")})
|
| 148 |
-
|
| 149 |
-
async with Client(mcp) as client:
|
| 150 |
-
tools = await client.list_tools()
|
| 151 |
-
assert {t.name for t in tools} == {"tool_1"}
|
| 152 |
-
|
| 153 |
async def test_exclude_tags_all_tools(self):
|
| 154 |
mcp = self.create_server(exclude_tags={"a", "b"})
|
| 155 |
|
|
@@ -164,13 +157,6 @@ class TestToolTags:
|
|
| 164 |
tools = await client.list_tools()
|
| 165 |
assert {t.name for t in tools} == {"tool_2"}
|
| 166 |
|
| 167 |
-
async def test_exclude_tags_tuple(self):
|
| 168 |
-
mcp = self.create_server(exclude_tags={("a", "b")})
|
| 169 |
-
|
| 170 |
-
async with Client(mcp) as client:
|
| 171 |
-
tools = await client.list_tools()
|
| 172 |
-
assert {t.name for t in tools} == {"tool_2"}
|
| 173 |
-
|
| 174 |
async def test_exclude_precedence(self):
|
| 175 |
mcp = self.create_server(exclude_tags={"a"}, include_tags={"b"})
|
| 176 |
|
|
|
|
| 143 |
tools = await client.list_tools()
|
| 144 |
assert {t.name for t in tools} == {"tool_1"}
|
| 145 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
async def test_exclude_tags_all_tools(self):
|
| 147 |
mcp = self.create_server(exclude_tags={"a", "b"})
|
| 148 |
|
|
|
|
| 157 |
tools = await client.list_tools()
|
| 158 |
assert {t.name for t in tools} == {"tool_2"}
|
| 159 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
async def test_exclude_precedence(self):
|
| 161 |
mcp = self.create_server(exclude_tags={"a"}, include_tags={"b"})
|
| 162 |
|