mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-24 06:24:18 +02:00
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:
parent
50ba6ea5a4
commit
84d7d3b281
21 changed files with 167 additions and 136 deletions
|
|
@ -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
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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}"}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)."""
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue