Jeremiah Lowin commited on
Commit
23dba76
·
1 Parent(s): 4351aa1

Remove tuple tags

Browse files
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
- | set[tuple[str, ...]]
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
- if isinstance(etag, tuple):
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
- if isinstance(itag, tuple):
1710
- if all(it in component.tags for it in itag):
1711
- return True
1712
- else:
1713
- if itag in component.tags:
1714
- return True
1715
 
1716
- return False
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] | set[tuple[str, ...]] | set[str | tuple[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. This can be a set of tags or tuples of tags.
245
- A component is considered to match if ANY of its tags match ANY
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] | set[tuple[str, ...]] | set[str | tuple[str, ...]] | None,
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. This can be a set of tags or tuples of tags.
265
- This is applied after include_tags, so if a component matches
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