Support plural names/keys in enable/disable API

Change enable() and disable() to accept sets: names, keys, tags.
Use key-based disable for decorator enabled=False to scope exactly.
This commit is contained in:
Jeremiah Lowin 2026-01-18 14:58:29 -05:00
commit 84d7d3b281
21 changed files with 167 additions and 136 deletions

View file

@ -149,7 +149,7 @@ mcp.add_transform(ToolTransform({
```
</CodeGroup>
`remove_tool_transformation()` is deprecated with no replacement - transforms are immutable once added. Use `server.disable(name=..., components=["tool"])` to hide tools dynamically.
`remove_tool_transformation()` is deprecated with no replacement - transforms are immutable once added. Use `server.disable(names={...}, components=["tool"])` to hide tools dynamically.
### FastMCP.as_proxy() Deprecated
@ -195,8 +195,8 @@ tool.enable()
```
```python After
server.disable(name="my_tool", components=["tool"])
server.enable(name="my_tool", components=["tool"])
server.disable(names={"my_tool"}, components=["tool"])
server.enable(names={"my_tool"}, components=["tool"])
```
</CodeGroup>
@ -208,7 +208,7 @@ Multiple `enable()`/`disable()` calls are additive. Later calls override earlier
```python
server.disable(tags={"internal"}) # Hide all internal
server.enable(name="safe_tool") # Show safe_tool (overrides the disable)
server.enable(names={"safe_tool"}) # Show safe_tool (overrides the disable)
# Result: safe_tool is visible, other internal tools are hidden
```

View file

@ -190,20 +190,20 @@ Components can be enabled/disabled using the enabled system. Each `enable()` or
mcp = FastMCP("Server")
# Disable by name and component type
mcp.disable(name="dangerous_tool", components=["tool"])
mcp.disable(names={"dangerous_tool"}, components=["tool"])
# Disable by tag
mcp.disable(tags={"admin"})
# Disable by version
mcp.disable(name="old_tool", version="1.0", components=["tool"])
mcp.disable(names={"old_tool"}, version="1.0", components=["tool"])
# Allowlist mode - only show components with these tags
mcp.enable(tags={"public"}, only=True)
# Enable overrides earlier disable (later transform wins)
mcp.disable(tags={"internal"})
mcp.enable(name="safe_tool") # safe_tool is visible despite internal tag
mcp.enable(names={"safe_tool"}) # safe_tool is visible despite internal tag
```
Works at both server and provider level. Supports:
@ -823,7 +823,7 @@ tool = await server.get_tool("my_tool")
tool.disable()
# v3.0
server.disable(name="my_tool", components=["tool"])
server.disable(names={"my_tool"}, components=["tool"])
```
### Component Lookup Methods

View file

@ -38,7 +38,7 @@ from fastmcp.server.transforms import Enabled
Enabled(False, tags=frozenset({"internal"}))
# Re-enable specific tool (override earlier disable)
Enabled(True, name="safe_tool")
Enabled(True, names={"safe_tool"})
# Allowlist via composition:
Enabled(False, match_all=True) # disable everything

View file

@ -75,10 +75,10 @@ Use keys to target specific components.
```python
# Disable a specific tool
mcp.disable(keys=["tool:delete_everything"])
mcp.disable(keys={"tool:delete_everything"})
# Disable multiple specific components
mcp.disable(keys=["tool:reset_system", "resource:data://secrets"])
mcp.disable(keys={"tool:reset_system", "resource:data://secrets"})
```
### Tags
@ -117,7 +117,7 @@ You can specify both keys and tags in a single call. The filters combine additiv
```python
# Disable specific tools AND all dangerous-tagged components
mcp.disable(keys=["tool:debug_info"], tags={"dangerous"})
mcp.disable(keys={"tool:debug_info"}, tags={"dangerous"})
```
## Allowlist Mode
@ -164,7 +164,7 @@ When you call `enable(only=True)`:
```python
# Start fresh - only enable these specific tools
mcp.enable(keys=["tool:safe_read", "tool:safe_write"], only=True)
mcp.enable(keys={"tool:safe_read", "tool:safe_write"}, only=True)
# Later, switch to a different allowlist
mcp.enable(tags={"production"}, only=True)
@ -176,7 +176,7 @@ Even in allowlist mode, the blocklist takes precedence. A component that's both
```python
mcp.enable(tags={"api"}, only=True) # Allow all api-tagged
mcp.disable(keys=["tool:api_admin"]) # But block this specific one
mcp.disable(keys={"tool:api_admin"}) # But block this specific one
# api_admin is disabled despite having the "api" tag
```
@ -325,10 +325,10 @@ from fastmcp.server.transforms import Enabled
mcp = FastMCP("Server")
# Using the convenience method (recommended)
mcp.disable(name="secret_tool")
mcp.disable(names={"secret_tool"})
# Equivalent to:
mcp.add_transform(Enabled(False, name="secret_tool"))
mcp.add_transform(Enabled(False, names={"secret_tool"}))
```
Server-level transforms override provider-level transforms. If a component is disabled at the provider level but enabled at the server level, the server-level `enable()` can re-enable it.

View file

@ -345,7 +345,7 @@ def internal_prompt() -> str:
return "Internal system prompt"
# Disable specific prompts by key
mcp.disable(keys=["prompt:internal_prompt"])
mcp.disable(keys={"prompt:internal_prompt"})
# Disable prompts by tag
mcp.disable(tags={"internal"})
@ -412,8 +412,8 @@ def example_prompt() -> str:
# These operations trigger notifications:
mcp.add_prompt(example_prompt) # Sends prompts/list_changed notification
mcp.disable(keys=["prompt:example_prompt"]) # Sends prompts/list_changed notification
mcp.enable(keys=["prompt:example_prompt"]) # Sends prompts/list_changed notification
mcp.disable(keys={"prompt:example_prompt"}) # Sends prompts/list_changed notification
mcp.enable(keys={"prompt:example_prompt"}) # Sends prompts/list_changed notification
```
Notifications are only sent when these operations occur within an active MCP request context (e.g., when called from within a tool or other MCP operation). Operations performed during server initialization do not trigger notifications.

View file

@ -123,7 +123,7 @@ def get_status() -> str:
mcp.disable(tags={"admin"})
# Or only enable specific tools
mcp.enable(keys=["tool:get_status"], only=True)
mcp.enable(keys={"tool:get_status"}, only=True)
```
See [Enabled](/servers/enabled) for the full documentation on keys, tags, allowlist mode, and provider-level control.

View file

@ -244,7 +244,7 @@ my_server = FastMCP("MyServer")
my_server.add_tool(local_tool)
# Now you can control enabled state
my_server.disable(keys=[local_tool.key])
my_server.disable(keys={local_tool.key})
```
## Performance Considerations

View file

@ -231,7 +231,7 @@ def get_public(): return "public"
def get_secret(): return "secret"
# Disable specific resources by key
mcp.disable(keys=["resource:data://secret"])
mcp.disable(keys={"resource:data://secret"})
# Disable resources by tag
mcp.disable(tags={"internal"})
@ -395,8 +395,8 @@ def example_resource() -> str:
# These operations trigger notifications:
mcp.add_resource(example_resource) # Sends resources/list_changed notification
mcp.disable(keys=["resource:data://example"]) # Sends resources/list_changed notification
mcp.enable(keys=["resource:data://example"]) # Sends resources/list_changed notification
mcp.disable(keys={"resource:data://example"}) # Sends resources/list_changed notification
mcp.enable(keys={"resource:data://example"}) # Sends resources/list_changed notification
```
Notifications are only sent when these operations occur within an active MCP request context (e.g., when called from within a tool or other MCP operation). Operations performed during server initialization do not trigger notifications.

View file

@ -853,7 +853,7 @@ def public_action() -> str:
return "Done"
# Disable specific tools by key
mcp.disable(keys=["tool:admin_action"])
mcp.disable(keys={"tool:admin_action"})
# Disable tools by tag
mcp.disable(tags={"admin"})
@ -967,8 +967,8 @@ def example_tool() -> str:
# These operations trigger notifications:
mcp.add_tool(example_tool) # Sends tools/list_changed notification
mcp.disable(keys=["tool:example_tool"]) # Sends tools/list_changed notification
mcp.enable(keys=["tool:example_tool"]) # Sends tools/list_changed notification
mcp.disable(keys={"tool:example_tool"}) # Sends tools/list_changed notification
mcp.enable(keys={"tool:example_tool"}) # Sends tools/list_changed notification
mcp.remove_tool("example_tool") # Sends tools/list_changed notification
```

View file

@ -112,7 +112,7 @@ def _make_endpoint(server: FastMCP, component_type: str, action: str):
# Call server.enable() or server.disable()
method = getattr(server, action)
method(name=name, version=version, components=components)
method(names={name} if name else None, version=version, components=components)
return JSONResponse(
{"message": f"{action.capitalize()}d {component_type}: {name}"}

View file

@ -509,7 +509,8 @@ class Provider:
def enable(
self,
*,
name: str | None = None,
names: set[str] | None = None,
keys: set[str] | None = None,
version: str | None = None,
tags: set[str] | None = None,
components: list[Literal["tool", "resource", "template", "prompt"]]
@ -526,7 +527,8 @@ class Provider:
then enables matching components.
Args:
name: Component name to enable.
names: Component names or URIs to enable.
keys: Component keys to enable (e.g., {"tool:my_tool@v1"}).
version: Component version to enable.
tags: Enable components with these tags.
components: Component types to include (e.g., ["tool", "prompt"]).
@ -542,7 +544,8 @@ class Provider:
self._transforms.append(
Enabled(
True,
name=name,
names=names,
keys=keys,
version=version,
components=frozenset(components) if components else None,
tags=frozenset(tags) if tags else None,
@ -554,7 +557,8 @@ class Provider:
def disable(
self,
*,
name: str | None = None,
names: set[str] | None = None,
keys: set[str] | None = None,
version: str | None = None,
tags: set[str] | None = None,
components: list[Literal["tool", "resource", "template", "prompt"]]
@ -567,7 +571,8 @@ class Provider:
(the later transform wins).
Args:
name: Component name to disable.
names: Component names or URIs to disable.
keys: Component keys to disable (e.g., {"tool:my_tool@v1"}).
version: Component version to disable.
tags: Disable components with these tags.
components: Component types to include (e.g., ["tool", "prompt"]).
@ -578,7 +583,8 @@ class Provider:
self._transforms.append(
Enabled(
False,
name=name,
names=names,
keys=keys,
version=version,
components=frozenset(components) if components else None,
tags=frozenset(tags) if tags else None,

View file

@ -283,7 +283,7 @@ class LocalProvider(Provider):
tool = Tool.from_function(tool)
self._add_component(tool)
if not enabled:
self.disable(name=tool.name)
self.disable(keys={tool.key}, components=["tool"], version=tool.version)
return tool
def remove_tool(self, name: str, version: str | None = None) -> None:
@ -375,9 +375,17 @@ class LocalProvider(Provider):
self._add_component(resource)
if not enabled:
if isinstance(resource, ResourceTemplate):
self.disable(name=resource.uri_template)
self.disable(
keys={resource.key},
components=["template"],
version=resource.version,
)
else:
self.disable(name=str(resource.uri))
self.disable(
keys={resource.key},
components=["resource"],
version=resource.version,
)
return resource
def remove_resource(self, uri: str, version: str | None = None) -> None:
@ -475,7 +483,9 @@ class LocalProvider(Provider):
)
self._add_component(prompt)
if not enabled:
self.disable(name=prompt.name)
self.disable(
keys={prompt.key}, components=["prompt"], version=prompt.version
)
return prompt
def remove_prompt(self, name: str, version: str | None = None) -> None:
@ -799,7 +809,11 @@ class LocalProvider(Provider):
)
self._add_component(tool_obj)
if not enabled:
self.disable(name=tool_name)
self.disable(
keys={tool_obj.key},
components=["tool"],
version=tool_obj.version,
)
return tool_obj
else:
from fastmcp.tools.function_tool import ToolMeta
@ -971,11 +985,11 @@ class LocalProvider(Provider):
if isinstance(obj, ResourceTemplate):
self.add_template(obj)
if not enabled:
self.disable(name=obj.uri_template)
self.disable(keys={obj.key})
else:
self.add_resource(obj)
if not enabled:
self.disable(name=str(obj.uri))
self.disable(keys={obj.key})
return obj
else:
from fastmcp.resources.function_resource import ResourceMeta
@ -1138,7 +1152,7 @@ class LocalProvider(Provider):
)
self._add_component(prompt_obj)
if not enabled:
self.disable(name=prompt_name)
self.disable(keys={prompt_obj.key})
return prompt_obj
else:
from fastmcp.prompts.function_prompt import PromptMeta

View file

@ -50,7 +50,7 @@ class Enabled(Transform):
Enabled(False, tags=frozenset({"internal"}))
# Re-enable specific tool (override earlier disable)
Enabled(True, name="safe_tool")
Enabled(True, names={"safe_tool"})
# Allowlist via composition:
Enabled(False, match_all=True) # disable everything
@ -62,7 +62,8 @@ class Enabled(Transform):
self,
enabled: bool,
*,
name: str | None = None,
names: set[str] | None = None,
keys: set[str] | None = None,
version: str | None = None,
tags: frozenset[str] | None = None,
components: frozenset[str] | None = None,
@ -72,14 +73,16 @@ class Enabled(Transform):
Args:
enabled: If True, mark matching as enabled; if False, mark as disabled.
name: Component name to match.
names: Component names or URIs to match.
keys: Component keys to match (e.g., {"tool:my_tool@v1"}).
version: Component version to match.
tags: Tags to match (component must have at least one).
components: Component types to match (e.g., frozenset({"tool", "prompt"})).
match_all: If True, matches all components regardless of other criteria.
"""
self._enabled = enabled
self.name = name
self.names = names
self.keys = keys
self.version = version
self.tags = tags # e.g., frozenset({"internal", "deprecated"})
self.components = components # e.g., frozenset({"tool", "prompt"})
@ -90,8 +93,10 @@ class Enabled(Transform):
if self.match_all:
return f"Enabled({self._enabled}, match_all=True)"
parts = []
if self.name:
parts.append(f"name={self.name!r}")
if self.names:
parts.append(f"names={set(self.names)}")
if self.keys:
parts.append(f"keys={set(self.keys)}")
if self.version:
parts.append(f"version={self.version!r}")
if self.components:
@ -121,7 +126,8 @@ class Enabled(Transform):
# Empty criteria matches nothing (safe default)
if (
self.name is None
self.names is None
and self.keys is None
and self.version is None
and self.components is None
and self.tags is None
@ -136,15 +142,20 @@ class Enabled(Transform):
if component_type not in self.components:
return False
# Check name if specified
if self.name is not None:
# Check keys if specified (exact match only)
if self.keys is not None:
if component.key not in self.keys:
return False
# Check names if specified
if self.names is not None:
# For resources, also check URI; for templates, check uri_template
matches_name = component.name == self.name
matches_name = component.name in self.names
matches_uri = False
if isinstance(component, Resource):
matches_uri = str(component.uri) == self.name
matches_uri = str(component.uri) in self.names
elif isinstance(component, ResourceTemplate):
matches_uri = component.uri_template == self.name
matches_uri = component.uri_template in self.names
if not (matches_name or matches_uri):
return False

View file

@ -50,7 +50,7 @@ class TestComponentManagementRoutes:
async def test_enable_tool_route(self, client, mcp):
"""Test enabling a tool via the HTTP route."""
# First disable the tool
mcp.disable(name="test_tool", components=["tool"])
mcp.disable(names={"test_tool"}, components=["tool"])
tools = await mcp.get_tools()
assert not any(t.name == "test_tool" for t in tools)
@ -83,7 +83,7 @@ class TestComponentManagementRoutes:
async def test_enable_resource_route(self, client, mcp):
"""Test enabling a resource via the HTTP route."""
# First disable the resource (can use URI as name for resources)
mcp.disable(name="data://test_resource", components=["resource"])
mcp.disable(names={"data://test_resource"}, components=["resource"])
resources = await mcp.get_resources()
assert not any(str(r.uri) == "data://test_resource" for r in resources)
@ -116,7 +116,7 @@ class TestComponentManagementRoutes:
async def test_enable_template_route(self, client, mcp):
"""Test enabling a resource template via the HTTP route."""
key = "data://test_resource/{id}"
mcp.disable(name="data://test_resource/{id}", components=["template"])
mcp.disable(names={"data://test_resource/{id}"}, components=["template"])
templates = await mcp.get_resource_templates()
assert not any(t.uri_template == key for t in templates)
response = client.post("/resources/data://test_resource/{id}/enable")
@ -143,7 +143,7 @@ class TestComponentManagementRoutes:
async def test_enable_prompt_route(self, client, mcp):
"""Test enabling a prompt via the HTTP route."""
# First disable the prompt
mcp.disable(name="test_prompt", components=["prompt"])
mcp.disable(names={"test_prompt"}, components=["prompt"])
prompts = await mcp.get_prompts()
assert not any(p.name == "test_prompt" for p in prompts)
@ -224,7 +224,7 @@ class TestAuthComponentManagementRoutes:
async def test_unauthorized_enable_tool(self):
"""Test that unauthenticated requests to enable a tool are rejected."""
self.mcp.disable(name="test_tool", components=["tool"])
self.mcp.disable(names={"test_tool"}, components=["tool"])
tools = await self.mcp.get_tools()
assert not any(t.name == "test_tool" for t in tools)
@ -235,7 +235,7 @@ class TestAuthComponentManagementRoutes:
async def test_authorized_enable_tool(self):
"""Test that authenticated requests to enable a tool are allowed."""
self.mcp.disable(name="test_tool", components=["tool"])
self.mcp.disable(names={"test_tool"}, components=["tool"])
tools = await self.mcp.get_tools()
assert not any(t.name == "test_tool" for t in tools)
@ -273,7 +273,7 @@ class TestAuthComponentManagementRoutes:
async def test_forbidden_enable_tool(self):
"""Test that requests with insufficient scopes are rejected."""
self.mcp.disable(name="test_tool", components=["tool"])
self.mcp.disable(names={"test_tool"}, components=["tool"])
tools = await self.mcp.get_tools()
assert not any(t.name == "test_tool" for t in tools)
@ -287,7 +287,7 @@ class TestAuthComponentManagementRoutes:
async def test_authorized_enable_resource(self):
"""Test that authenticated requests to enable a resource are allowed."""
self.mcp.disable(name="data://test_resource", components=["resource"])
self.mcp.disable(names={"data://test_resource"}, components=["resource"])
resources = await self.mcp.get_resources()
assert not any(str(r.uri) == "data://test_resource" for r in resources)
@ -312,7 +312,7 @@ class TestAuthComponentManagementRoutes:
async def test_forbidden_enable_resource(self):
"""Test that requests with insufficient scopes are rejected."""
self.mcp.disable(name="data://test_resource", components=["resource"])
self.mcp.disable(names={"data://test_resource"}, components=["resource"])
resources = await self.mcp.get_resources()
assert not any(str(r.uri) == "data://test_resource" for r in resources)
@ -340,7 +340,7 @@ class TestAuthComponentManagementRoutes:
async def test_unauthorized_enable_prompt(self):
"""Test that unauthenticated requests to enable a prompt are rejected."""
self.mcp.disable(name="test_prompt", components=["prompt"])
self.mcp.disable(names={"test_prompt"}, components=["prompt"])
prompts = await self.mcp.get_prompts()
assert not any(p.name == "test_prompt" for p in prompts)
@ -351,7 +351,7 @@ class TestAuthComponentManagementRoutes:
async def test_authorized_enable_prompt(self):
"""Test that authenticated requests to enable a prompt are allowed."""
self.mcp.disable(name="test_prompt", components=["prompt"])
self.mcp.disable(names={"test_prompt"}, components=["prompt"])
prompts = await self.mcp.get_prompts()
assert not any(p.name == "test_prompt" for p in prompts)
@ -429,7 +429,7 @@ class TestComponentManagerWithPath:
return TestClient(mcp_with_path.http_app())
async def test_enable_tool_route_with_path(self, client_with_path, mcp_with_path):
mcp_with_path.disable(name="test_tool", components=["tool"])
mcp_with_path.disable(names={"test_tool"}, components=["tool"])
tools = await mcp_with_path.get_tools()
assert not any(t.name == "test_tool" for t in tools)
response = client_with_path.post("/test/tools/test_tool/enable")
@ -450,7 +450,7 @@ class TestComponentManagerWithPath:
assert not any(str(r.uri) == "data://test_resource" for r in resources)
async def test_enable_prompt_route_with_path(self, client_with_path, mcp_with_path):
mcp_with_path.disable(name="test_prompt", components=["prompt"])
mcp_with_path.disable(names={"test_prompt"}, components=["prompt"])
prompts = await mcp_with_path.get_prompts()
assert not any(p.name == "test_prompt" for p in prompts)
response = client_with_path.post("/test/prompts/test_prompt/enable")
@ -503,7 +503,7 @@ class TestComponentManagerWithPathAuth:
self.client = TestClient(self.mcp.http_app())
async def test_unauthorized_enable_tool(self):
self.mcp.disable(name="test_tool", components=["tool"])
self.mcp.disable(names={"test_tool"}, components=["tool"])
tools = await self.mcp.get_tools()
assert not any(t.name == "test_tool" for t in tools)
response = self.client.post("/test/tools/test_tool/enable")
@ -512,7 +512,7 @@ class TestComponentManagerWithPathAuth:
assert not any(t.name == "test_tool" for t in tools)
async def test_forbidden_enable_tool(self):
self.mcp.disable(name="test_tool", components=["tool"])
self.mcp.disable(names={"test_tool"}, components=["tool"])
tools = await self.mcp.get_tools()
assert not any(t.name == "test_tool" for t in tools)
response = self.client.post(
@ -524,7 +524,7 @@ class TestComponentManagerWithPathAuth:
assert not any(t.name == "test_tool" for t in tools)
async def test_authorized_enable_tool(self):
self.mcp.disable(name="test_tool", components=["tool"])
self.mcp.disable(names={"test_tool"}, components=["tool"])
tools = await self.mcp.get_tools()
assert not any(t.name == "test_tool" for t in tools)
response = self.client.post(
@ -568,7 +568,7 @@ class TestComponentManagerWithPathAuth:
assert not any(str(r.uri) == "data://test_resource" for r in resources)
async def test_unauthorized_enable_prompt(self):
self.mcp.disable(name="test_prompt", components=["prompt"])
self.mcp.disable(names={"test_prompt"}, components=["prompt"])
prompts = await self.mcp.get_prompts()
assert not any(p.name == "test_prompt" for p in prompts)
response = self.client.post("/test/prompts/test_prompt/enable")
@ -577,7 +577,7 @@ class TestComponentManagerWithPathAuth:
assert not any(p.name == "test_prompt" for p in prompts)
async def test_forbidden_enable_prompt(self):
self.mcp.disable(name="test_prompt", components=["prompt"])
self.mcp.disable(names={"test_prompt"}, components=["prompt"])
prompts = await self.mcp.get_prompts()
assert not any(p.name == "test_prompt" for p in prompts)
response = self.client.post(
@ -589,7 +589,7 @@ class TestComponentManagerWithPathAuth:
assert not any(p.name == "test_prompt" for p in prompts)
async def test_authorized_enable_prompt(self):
self.mcp.disable(name="test_prompt", components=["prompt"])
self.mcp.disable(names={"test_prompt"}, components=["prompt"])
prompts = await self.mcp.get_prompts()
assert not any(p.name == "test_prompt" for p in prompts)
response = self.client.post(

View file

@ -350,7 +350,7 @@ class TestLocalProviderDecorators:
enabled_transforms = [t for t in provider.transforms if isinstance(t, Enabled)]
assert len(enabled_transforms) == 1
assert enabled_transforms[0]._enabled is False
assert enabled_transforms[0].name == "disabled_tool"
assert enabled_transforms[0].names == {"disabled_tool"}
async def test_tool_enabled_false_not_listed(self):
"""Disabled tool should not appear in get_tools (filtering happens at server level)."""
@ -385,7 +385,7 @@ class TestLocalProviderDecorators:
assert await server.get_tool("my_tool") is None
# Server-level enable overrides it
server.enable(name="my_tool")
server.enable(names={"my_tool"})
tool = await server.get_tool("my_tool")
assert tool is not None
assert tool.name == "my_tool"
@ -439,7 +439,7 @@ class TestLocalProviderDecorators:
enabled_transforms = [t for t in provider.transforms if isinstance(t, Enabled)]
assert len(enabled_transforms) == 1
assert enabled_transforms[0]._enabled is False
assert enabled_transforms[0].name == "resource://test"
assert enabled_transforms[0].names == {"resource://test"}
async def test_resource_enabled_false_not_listed(self):
"""Disabled resource should not appear in get_resources (filtering at server level)."""
@ -475,7 +475,7 @@ class TestLocalProviderDecorators:
enabled_transforms = [t for t in provider.transforms if isinstance(t, Enabled)]
assert len(enabled_transforms) == 1
assert enabled_transforms[0]._enabled is False
assert enabled_transforms[0].name == "data://{id}"
assert enabled_transforms[0].names == {"data://{id}"}
async def test_template_enabled_false_not_listed(self):
"""Disabled template should not appear in get_resource_templates (filtering at server level)."""
@ -546,7 +546,7 @@ class TestLocalProviderDecorators:
enabled_transforms = [t for t in provider.transforms if isinstance(t, Enabled)]
assert len(enabled_transforms) == 1
assert enabled_transforms[0]._enabled is False
assert enabled_transforms[0].name == "disabled_prompt"
assert enabled_transforms[0].names == {"disabled_prompt"}
async def test_prompt_enabled_false_not_listed(self):
"""Disabled prompt should not appear in get_prompts (filtering at server level)."""

View file

@ -330,12 +330,12 @@ class TestPromptEnabled:
prompts = await mcp.get_prompts()
assert any(p.name == "sample_prompt" for p in prompts)
mcp.disable(name="sample_prompt", components=["prompt"])
mcp.disable(names={"sample_prompt"}, components=["prompt"])
prompts = await mcp.get_prompts()
assert not any(p.name == "sample_prompt" for p in prompts)
mcp.enable(name="sample_prompt", components=["prompt"])
mcp.enable(names={"sample_prompt"}, components=["prompt"])
prompts = await mcp.get_prompts()
assert any(p.name == "sample_prompt" for p in prompts)
@ -347,7 +347,7 @@ class TestPromptEnabled:
def sample_prompt() -> str:
return "Hello, world!"
mcp.disable(name="sample_prompt", components=["prompt"])
mcp.disable(names={"sample_prompt"}, components=["prompt"])
prompts = await mcp.get_prompts()
assert len(prompts) == 0
@ -358,11 +358,11 @@ class TestPromptEnabled:
def sample_prompt() -> str:
return "Hello, world!"
mcp.disable(name="sample_prompt", components=["prompt"])
mcp.disable(names={"sample_prompt"}, components=["prompt"])
prompts = await mcp.get_prompts()
assert not any(p.name == "sample_prompt" for p in prompts)
mcp.enable(name="sample_prompt", components=["prompt"])
mcp.enable(names={"sample_prompt"}, components=["prompt"])
prompts = await mcp.get_prompts()
assert len(prompts) == 1
@ -373,7 +373,7 @@ class TestPromptEnabled:
def sample_prompt() -> str:
return "Hello, world!"
mcp.disable(name="sample_prompt", components=["prompt"])
mcp.disable(names={"sample_prompt"}, components=["prompt"])
prompts = await mcp.get_prompts()
assert len(prompts) == 0
@ -391,7 +391,7 @@ class TestPromptEnabled:
prompt = await mcp.get_prompt("sample_prompt")
assert prompt is not None
mcp.disable(name="sample_prompt", components=["prompt"])
mcp.disable(names={"sample_prompt"}, components=["prompt"])
prompts = await mcp.get_prompts()
assert len(prompts) == 0
@ -406,7 +406,7 @@ class TestPromptEnabled:
def sample_prompt() -> str:
return "Hello, world!"
mcp.disable(name="sample_prompt", components=["prompt"])
mcp.disable(names={"sample_prompt"}, components=["prompt"])
# get_prompt() applies enabled transform, returns None for disabled
prompt = await mcp.get_prompt("sample_prompt")

View file

@ -738,12 +738,12 @@ class TestResourceEnabled:
resources = await mcp.get_resources()
assert any(str(r.uri) == "resource://data" for r in resources)
mcp.disable(name="resource://data", components=["resource"])
mcp.disable(names={"resource://data"}, components=["resource"])
resources = await mcp.get_resources()
assert not any(str(r.uri) == "resource://data" for r in resources)
mcp.enable(name="resource://data", components=["resource"])
mcp.enable(names={"resource://data"}, components=["resource"])
resources = await mcp.get_resources()
assert any(str(r.uri) == "resource://data" for r in resources)
@ -755,7 +755,7 @@ class TestResourceEnabled:
def sample_resource() -> str:
return "Hello, world!"
mcp.disable(name="resource://data", components=["resource"])
mcp.disable(names={"resource://data"}, components=["resource"])
resources = await mcp.get_resources()
assert len(resources) == 0
@ -769,11 +769,11 @@ class TestResourceEnabled:
def sample_resource() -> str:
return "Hello, world!"
mcp.disable(name="resource://data", components=["resource"])
mcp.disable(names={"resource://data"}, components=["resource"])
resources = await mcp.get_resources()
assert not any(str(r.uri) == "resource://data" for r in resources)
mcp.enable(name="resource://data", components=["resource"])
mcp.enable(names={"resource://data"}, components=["resource"])
resources = await mcp.get_resources()
assert len(resources) == 1
@ -784,7 +784,7 @@ class TestResourceEnabled:
def sample_resource() -> str:
return "Hello, world!"
mcp.disable(name="resource://data", components=["resource"])
mcp.disable(names={"resource://data"}, components=["resource"])
resources = await mcp.get_resources()
assert len(resources) == 0
@ -801,7 +801,7 @@ class TestResourceEnabled:
resource = await mcp.get_resource("resource://data")
assert resource is not None
mcp.disable(name="resource://data", components=["resource"])
mcp.disable(names={"resource://data"}, components=["resource"])
resources = await mcp.get_resources()
assert len(resources) == 0
@ -815,7 +815,7 @@ class TestResourceEnabled:
def sample_resource() -> str:
return "Hello, world!"
mcp.disable(name="resource://data", components=["resource"])
mcp.disable(names={"resource://data"}, components=["resource"])
with pytest.raises(NotFoundError, match="Unknown resource"):
await mcp.read_resource("resource://data")
@ -891,12 +891,12 @@ class TestResourceTemplateEnabled:
templates = await mcp.get_resource_templates()
assert any(t.uri_template == "resource://{param}" for t in templates)
mcp.disable(name="resource://{param}", components=["template"])
mcp.disable(names={"resource://{param}"}, components=["template"])
templates = await mcp.get_resource_templates()
assert not any(t.uri_template == "resource://{param}" for t in templates)
mcp.enable(name="resource://{param}", components=["template"])
mcp.enable(names={"resource://{param}"}, components=["template"])
templates = await mcp.get_resource_templates()
assert any(t.uri_template == "resource://{param}" for t in templates)
@ -908,7 +908,7 @@ class TestResourceTemplateEnabled:
def sample_template(param: str) -> str:
return f"Template: {param}"
mcp.disable(name="resource://{param}", components=["template"])
mcp.disable(names={"resource://{param}"}, components=["template"])
templates = await mcp.get_resource_templates()
assert len(templates) == 0
@ -922,11 +922,11 @@ class TestResourceTemplateEnabled:
def sample_template(param: str) -> str:
return f"Template: {param}"
mcp.disable(name="resource://{param}", components=["template"])
mcp.disable(names={"resource://{param}"}, components=["template"])
templates = await mcp.get_resource_templates()
assert not any(t.uri_template == "resource://{param}" for t in templates)
mcp.enable(name="resource://{param}", components=["template"])
mcp.enable(names={"resource://{param}"}, components=["template"])
templates = await mcp.get_resource_templates()
assert len(templates) == 1
@ -937,7 +937,7 @@ class TestResourceTemplateEnabled:
def sample_template(param: str) -> str:
return f"Template: {param}"
mcp.disable(name="resource://{param}", components=["template"])
mcp.disable(names={"resource://{param}"}, components=["template"])
templates = await mcp.get_resource_templates()
assert len(templates) == 0
@ -954,7 +954,7 @@ class TestResourceTemplateEnabled:
template = await mcp.get_resource_template("resource://{param}")
assert template is not None
mcp.disable(name="resource://{param}", components=["template"])
mcp.disable(names={"resource://{param}"}, components=["template"])
templates = await mcp.get_resource_templates()
assert len(templates) == 0
@ -968,7 +968,7 @@ class TestResourceTemplateEnabled:
def sample_template(param: str) -> str:
return f"Template: {param}"
mcp.disable(name="resource://{param}", components=["template"])
mcp.disable(names={"resource://{param}"}, components=["template"])
with pytest.raises(NotFoundError, match="Unknown resource"):
await mcp.read_resource("resource://test")

View file

@ -1473,14 +1473,14 @@ class TestToolEnabled:
assert any(t.name == "sample_tool" for t in tools)
# Disable via server
mcp.disable(name="sample_tool", components=["tool"])
mcp.disable(names={"sample_tool"}, components=["tool"])
# Tool should not be in list when disabled
tools = await mcp.get_tools()
assert not any(t.name == "sample_tool" for t in tools)
# Re-enable via server
mcp.enable(name="sample_tool", components=["tool"])
mcp.enable(names={"sample_tool"}, components=["tool"])
tools = await mcp.get_tools()
assert any(t.name == "sample_tool" for t in tools)
@ -1491,7 +1491,7 @@ class TestToolEnabled:
def sample_tool(x: int) -> int:
return x * 2
mcp.disable(name="sample_tool", components=["tool"])
mcp.disable(names={"sample_tool"}, components=["tool"])
tools = await mcp.get_tools()
assert len(tools) == 0
@ -1505,8 +1505,8 @@ class TestToolEnabled:
def sample_tool(x: int) -> int:
return x * 2
mcp.disable(name="sample_tool", components=["tool"])
mcp.enable(name="sample_tool", components=["tool"])
mcp.disable(names={"sample_tool"}, components=["tool"])
mcp.enable(names={"sample_tool"}, components=["tool"])
tools = await mcp.get_tools()
assert len(tools) == 1
@ -1517,7 +1517,7 @@ class TestToolEnabled:
def sample_tool(x: int) -> int:
return x * 2
mcp.disable(name="sample_tool", components=["tool"])
mcp.disable(names={"sample_tool"}, components=["tool"])
tools = await mcp.get_tools()
assert len(tools) == 0
@ -1531,10 +1531,10 @@ class TestToolEnabled:
def sample_tool(x: int) -> int:
return x * 2
tool = await mcp.get_tool(name="sample_tool")
tool = await mcp.get_tool("sample_tool")
assert tool is not None
mcp.disable(name="sample_tool", components=["tool"])
mcp.disable(names={"sample_tool"}, components=["tool"])
tools = await mcp.get_tools()
assert len(tools) == 0
@ -1548,7 +1548,7 @@ class TestToolEnabled:
def sample_tool(x: int) -> int:
return x * 2
mcp.disable(name="sample_tool", components=["tool"])
mcp.disable(names={"sample_tool"}, components=["tool"])
with pytest.raises(NotFoundError, match="Unknown tool"):
await mcp.call_tool("sample_tool", {"x": 5})

View file

@ -1488,12 +1488,12 @@ class TestComponentServicePrefixLess:
assert any(t.name == "my_tool" for t in tools)
# Disable and re-enable
main_app.disable(name="my_tool", components=["tool"])
main_app.disable(names={"my_tool"}, components=["tool"])
# Verify tool is now disabled
tools = await main_app.get_tools()
assert not any(t.name == "my_tool" for t in tools)
main_app.enable(name="my_tool", components=["tool"])
main_app.enable(names={"my_tool"}, components=["tool"])
# Verify tool is now enabled
tools = await main_app.get_tools()
assert any(t.name == "my_tool" for t in tools)
@ -1511,12 +1511,12 @@ class TestComponentServicePrefixLess:
main_app.mount(sub_app)
# Disable and re-enable
main_app.disable(name="data://test", components=["resource"])
main_app.disable(names={"data://test"}, components=["resource"])
# Verify resource is now disabled
resources = await main_app.get_resources()
assert not any(str(r.uri) == "data://test" for r in resources)
main_app.enable(name="data://test", components=["resource"])
main_app.enable(names={"data://test"}, components=["resource"])
# Verify resource is now enabled
resources = await main_app.get_resources()
assert any(str(r.uri) == "data://test" for r in resources)
@ -1534,12 +1534,12 @@ class TestComponentServicePrefixLess:
main_app.mount(sub_app)
# Disable and re-enable
main_app.disable(name="my_prompt", components=["prompt"])
main_app.disable(names={"my_prompt"}, components=["prompt"])
# Verify prompt is now disabled
prompts = await main_app.get_prompts()
assert not any(p.name == "my_prompt" for p in prompts)
main_app.enable(name="my_prompt", components=["prompt"])
main_app.enable(names={"my_prompt"}, components=["prompt"])
# Verify prompt is now enabled
prompts = await main_app.get_prompts()
assert any(p.name == "my_prompt" for p in prompts)

View file

@ -21,7 +21,7 @@ class TestMatching:
def test_match_by_name(self):
"""Matches component by name."""
t = Enabled(False, name="foo")
t = Enabled(False, names={"foo"})
assert t._matches(Tool(name="foo", parameters={})) is True
assert t._matches(Tool(name="bar", parameters={})) is False
@ -39,7 +39,7 @@ class TestMatching:
def test_match_by_component_type(self):
"""Only matches specified component types."""
t = Enabled(False, name="foo", components=frozenset({"prompt"}))
t = Enabled(False, names={"foo"}, components=frozenset({"prompt"}))
# Tool has key "tool:foo@", not "prompt:foo@"
assert t._matches(Tool(name="foo", parameters={})) is False
@ -47,7 +47,7 @@ class TestMatching:
"""Multiple criteria use AND logic - all must match."""
t = Enabled(
False,
name="foo",
names={"foo"},
version="v1",
tags=frozenset({"internal"}),
)
@ -69,13 +69,13 @@ class TestMarking:
def test_disable_marks_as_disabled(self):
"""Enabled(False, ...) marks matching components as disabled."""
tool = Tool(name="foo", parameters={})
Enabled(False, name="foo")._mark_component(tool)
Enabled(False, names={"foo"})._mark_component(tool)
assert is_enabled(tool) is False
def test_enable_marks_as_enabled(self):
"""Enabled(True, ...) marks matching components as enabled."""
tool = Tool(name="foo", parameters={})
Enabled(True, name="foo")._mark_component(tool)
Enabled(True, names={"foo"})._mark_component(tool)
assert is_enabled(tool) is True
assert tool.meta is not None
assert tool.meta["fastmcp"]["_internal"]["enabled"] is True
@ -83,7 +83,7 @@ class TestMarking:
def test_non_matching_unchanged(self):
"""Non-matching components are not modified."""
tool = Tool(name="bar", parameters={})
Enabled(False, name="foo")._mark_component(tool)
Enabled(False, names={"foo"})._mark_component(tool)
# No _internal key added
assert tool.meta is None or "_internal" not in tool.meta.get("fastmcp", {})
assert is_enabled(tool) is True
@ -91,7 +91,7 @@ class TestMarking:
def test_mutates_in_place(self):
"""Marking mutates the component in place."""
tool = Tool(name="foo", parameters={})
result = Enabled(False, name="foo")._mark_component(tool)
result = Enabled(False, names={"foo"})._mark_component(tool)
assert result is tool
def test_disable_all(self):
@ -107,19 +107,19 @@ class TestOverride:
def test_enable_overrides_disable(self):
"""An enable after disable results in enabled."""
tool = Tool(name="foo", parameters={})
Enabled(False, name="foo")._mark_component(tool)
Enabled(False, names={"foo"})._mark_component(tool)
assert is_enabled(tool) is False
Enabled(True, name="foo")._mark_component(tool)
Enabled(True, names={"foo"})._mark_component(tool)
assert is_enabled(tool) is True
def test_disable_overrides_enable(self):
"""A disable after enable results in disabled."""
tool = Tool(name="foo", parameters={})
Enabled(True, name="foo")._mark_component(tool)
Enabled(True, names={"foo"})._mark_component(tool)
assert is_enabled(tool) is True
Enabled(False, name="foo")._mark_component(tool)
Enabled(False, names={"foo"})._mark_component(tool)
assert is_enabled(tool) is False
@ -137,7 +137,7 @@ class TestHelperFunctions:
Tool(name="enabled", parameters={}),
Tool(name="disabled", parameters={}),
]
Enabled(False, name="disabled")._mark_component(tools[1])
Enabled(False, names={"disabled"})._mark_component(tools[1])
visible = [t for t in tools if is_enabled(t)]
assert [t.name for t in visible] == ["enabled"]
@ -149,7 +149,7 @@ class TestMetadata:
def test_internal_metadata_stripped_by_get_meta(self):
"""Internal metadata is stripped when calling get_meta()."""
tool = Tool(name="foo", parameters={})
Enabled(True, name="foo")._mark_component(tool)
Enabled(True, names={"foo"})._mark_component(tool)
# Raw meta has _internal
assert tool.meta is not None
@ -162,7 +162,7 @@ class TestMetadata:
def test_user_metadata_preserved(self):
"""User-provided metadata is not affected."""
tool = Tool(name="foo", parameters={}, meta={"custom": "value"})
marked = Enabled(False, name="foo")._mark_component(tool)
marked = Enabled(False, names={"foo"})._mark_component(tool)
assert marked.meta is not None
assert marked.meta["custom"] == "value"
@ -173,14 +173,14 @@ class TestRepr:
def test_repr_disable(self):
"""Repr shows disable action and criteria."""
t = Enabled(False, name="foo")
t = Enabled(False, names={"foo"})
r = repr(t)
assert "disable" in r
assert "foo" in r
def test_repr_enable(self):
"""Repr shows enable action."""
t = Enabled(True, name="foo")
t = Enabled(True, names={"foo"})
assert "enable" in repr(t)
def test_repr_match_all(self):

View file

@ -1061,7 +1061,7 @@ class TestEnableDisable:
mcp.add_tool(new_add)
# Disable original tool, but new_add should still work
mcp.disable(name="add", components=["tool"])
mcp.disable(names={"add"}, components=["tool"])
async with Client(mcp) as client:
tools = await client.list_tools()
@ -1088,8 +1088,8 @@ class TestEnableDisable:
mcp.add_tool(new_add)
# Disable both tools via server
mcp.disable(name="add", components=["tool"]).disable(
name="new_add", components=["tool"]
mcp.disable(names={"add"}, components=["tool"]).disable(
names={"new_add"}, components=["tool"]
)
async with Client(mcp) as client: