Summary
Visibility(False, tags=['internal']) crashes with TypeError when applied to components. The constructor accepts any iterable for tags (type hint is set[str] | None), but _matches() performs set intersection (&) which fails on lists.
Reproduction
from fastmcp import FastMCP
from fastmcp.server.transforms.visibility import Visibility
mcp = FastMCP('test')
@mcp.tool(tags={'internal'})
def my_tool() -> str:
"""A test tool"""
return 'hello'
vis = Visibility(False, tags=['internal'])
mcp.add_transform(vis)
# Crashes:
tools = await mcp.list_tools()
# TypeError: unsupported operand type(s) for &: 'set' and 'list'
Root Cause
visibility.py:88 stores tags without coercion:
self.tags = tags # stored as-is (could be list)
visibility.py:171 uses set intersection:
return self.tags is None or bool(component.tags & self.tags)
component.tags is always a set (enforced by Pydantic BeforeValidator), but self.tags can be a list, and set & list raises TypeError.
Note
The safe API paths (provider.disable(), ctx.disable_components()) already coerce to set. Only direct Visibility() construction is affected.
Fix
Coerce tags to set in the constructor: self.tags = set(tags) if tags is not None else None
Summary
Visibility(False, tags=['internal'])crashes withTypeErrorwhen applied to components. The constructor accepts any iterable fortags(type hint isset[str] | None), but_matches()performs set intersection (&) which fails on lists.Reproduction
Root Cause
visibility.py:88stores tags without coercion:visibility.py:171uses set intersection:component.tagsis always aset(enforced by PydanticBeforeValidator), butself.tagscan be a list, andset & listraisesTypeError.Note
The safe API paths (
provider.disable(),ctx.disable_components()) already coerce to set. Only directVisibility()construction is affected.Fix
Coerce
tagsto set in the constructor:self.tags = set(tags) if tags is not None else None