From 84d7d3b2813ac0afc03d73d203acf9e7eac17837 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sun, 18 Jan 2026 14:58:29 -0500 Subject: [PATCH] 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. --- docs/development/upgrade-guide.mdx | 8 ++-- docs/development/v3-notes/v3-features.mdx | 8 ++-- .../fastmcp-server-transforms-enabled.mdx | 2 +- docs/servers/enabled.mdx | 14 +++---- docs/servers/prompts.mdx | 6 +-- docs/servers/providers/local.mdx | 2 +- docs/servers/providers/proxy.mdx | 2 +- docs/servers/resources.mdx | 6 +-- docs/servers/tools.mdx | 6 +-- .../component_manager/component_manager.py | 2 +- src/fastmcp/server/providers/base.py | 18 ++++++--- .../server/providers/local_provider.py | 30 +++++++++++---- src/fastmcp/server/transforms/enabled.py | 35 +++++++++++------ tests/contrib/test_component_manager.py | 38 +++++++++---------- tests/server/providers/test_local_provider.py | 10 ++--- .../providers/test_local_provider_prompts.py | 16 ++++---- .../test_local_provider_resources.py | 32 ++++++++-------- .../providers/test_local_provider_tools.py | 18 ++++----- tests/server/test_mount.py | 12 +++--- tests/server/transforms/test_enabled.py | 32 ++++++++-------- tests/tools/test_tool_transform.py | 6 +-- 21 files changed, 167 insertions(+), 136 deletions(-) diff --git a/docs/development/upgrade-guide.mdx b/docs/development/upgrade-guide.mdx index 3ab91b09b..0fcf5ab47 100644 --- a/docs/development/upgrade-guide.mdx +++ b/docs/development/upgrade-guide.mdx @@ -149,7 +149,7 @@ mcp.add_transform(ToolTransform({ ``` -`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"]) ``` @@ -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 ``` diff --git a/docs/development/v3-notes/v3-features.mdx b/docs/development/v3-notes/v3-features.mdx index 4eb033282..280d50a36 100644 --- a/docs/development/v3-notes/v3-features.mdx +++ b/docs/development/v3-notes/v3-features.mdx @@ -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 diff --git a/docs/python-sdk/fastmcp-server-transforms-enabled.mdx b/docs/python-sdk/fastmcp-server-transforms-enabled.mdx index 90a5d58d3..149e777e2 100644 --- a/docs/python-sdk/fastmcp-server-transforms-enabled.mdx +++ b/docs/python-sdk/fastmcp-server-transforms-enabled.mdx @@ -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 diff --git a/docs/servers/enabled.mdx b/docs/servers/enabled.mdx index 830079270..fe4836a26 100644 --- a/docs/servers/enabled.mdx +++ b/docs/servers/enabled.mdx @@ -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. diff --git a/docs/servers/prompts.mdx b/docs/servers/prompts.mdx index 98b937b91..47e3c4872 100644 --- a/docs/servers/prompts.mdx +++ b/docs/servers/prompts.mdx @@ -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. diff --git a/docs/servers/providers/local.mdx b/docs/servers/providers/local.mdx index 8d93ea0ec..9765c4111 100644 --- a/docs/servers/providers/local.mdx +++ b/docs/servers/providers/local.mdx @@ -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. diff --git a/docs/servers/providers/proxy.mdx b/docs/servers/providers/proxy.mdx index 63754783a..b9b04bbdc 100644 --- a/docs/servers/providers/proxy.mdx +++ b/docs/servers/providers/proxy.mdx @@ -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 diff --git a/docs/servers/resources.mdx b/docs/servers/resources.mdx index f3e442008..b78b7568a 100644 --- a/docs/servers/resources.mdx +++ b/docs/servers/resources.mdx @@ -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. diff --git a/docs/servers/tools.mdx b/docs/servers/tools.mdx index 31fa556c8..483d0825b 100644 --- a/docs/servers/tools.mdx +++ b/docs/servers/tools.mdx @@ -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 ``` diff --git a/src/fastmcp/contrib/component_manager/component_manager.py b/src/fastmcp/contrib/component_manager/component_manager.py index f439c78c3..099b9052f 100644 --- a/src/fastmcp/contrib/component_manager/component_manager.py +++ b/src/fastmcp/contrib/component_manager/component_manager.py @@ -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}"} diff --git a/src/fastmcp/server/providers/base.py b/src/fastmcp/server/providers/base.py index 4f14c59e9..107339ca8 100644 --- a/src/fastmcp/server/providers/base.py +++ b/src/fastmcp/server/providers/base.py @@ -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, diff --git a/src/fastmcp/server/providers/local_provider.py b/src/fastmcp/server/providers/local_provider.py index 1105ffbf1..abdcdbd65 100644 --- a/src/fastmcp/server/providers/local_provider.py +++ b/src/fastmcp/server/providers/local_provider.py @@ -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 diff --git a/src/fastmcp/server/transforms/enabled.py b/src/fastmcp/server/transforms/enabled.py index 39a14db77..1a61c48ac 100644 --- a/src/fastmcp/server/transforms/enabled.py +++ b/src/fastmcp/server/transforms/enabled.py @@ -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 diff --git a/tests/contrib/test_component_manager.py b/tests/contrib/test_component_manager.py index 0cd0d4637..22d2ce65f 100644 --- a/tests/contrib/test_component_manager.py +++ b/tests/contrib/test_component_manager.py @@ -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( diff --git a/tests/server/providers/test_local_provider.py b/tests/server/providers/test_local_provider.py index 920fe3585..363b3f44f 100644 --- a/tests/server/providers/test_local_provider.py +++ b/tests/server/providers/test_local_provider.py @@ -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).""" diff --git a/tests/server/providers/test_local_provider_prompts.py b/tests/server/providers/test_local_provider_prompts.py index 9ebd41fee..89246ea95 100644 --- a/tests/server/providers/test_local_provider_prompts.py +++ b/tests/server/providers/test_local_provider_prompts.py @@ -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") diff --git a/tests/server/providers/test_local_provider_resources.py b/tests/server/providers/test_local_provider_resources.py index e68cf7335..6933e8b4e 100644 --- a/tests/server/providers/test_local_provider_resources.py +++ b/tests/server/providers/test_local_provider_resources.py @@ -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") diff --git a/tests/server/providers/test_local_provider_tools.py b/tests/server/providers/test_local_provider_tools.py index d11154e87..fa5965bd1 100644 --- a/tests/server/providers/test_local_provider_tools.py +++ b/tests/server/providers/test_local_provider_tools.py @@ -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}) diff --git a/tests/server/test_mount.py b/tests/server/test_mount.py index 5e928a6e8..c310c57ee 100644 --- a/tests/server/test_mount.py +++ b/tests/server/test_mount.py @@ -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) diff --git a/tests/server/transforms/test_enabled.py b/tests/server/transforms/test_enabled.py index 7be8ab3c6..88df14d93 100644 --- a/tests/server/transforms/test_enabled.py +++ b/tests/server/transforms/test_enabled.py @@ -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): diff --git a/tests/tools/test_tool_transform.py b/tests/tools/test_tool_transform.py index ca90b6a44..369475615 100644 --- a/tests/tools/test_tool_transform.py +++ b/tests/tools/test_tool_transform.py @@ -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: