Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/mcp/server/mcpserver/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ class ResourceError(MCPServerError):
"""Error in resource operations."""


class PromptError(MCPServerError):
"""Error in prompt operations."""


class ToolError(MCPServerError):
"""Error in tool operations."""

Expand Down
14 changes: 14 additions & 0 deletions src/mcp/server/mcpserver/prompts/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from typing import TYPE_CHECKING, Any

from mcp.server.mcpserver.exceptions import PromptError
from mcp.server.mcpserver.prompts.base import Message, Prompt
from mcp.server.mcpserver.utilities.logging import get_logger

Expand Down Expand Up @@ -45,6 +46,19 @@ def add_prompt(
self._prompts[prompt.name] = prompt
return prompt

def remove_prompt(self, name: str) -> None:
"""Remove a prompt by name.

Args:
name: The name of the prompt to remove

Raises:
PromptError: If the prompt does not exist
"""
if name not in self._prompts:
raise PromptError(f"Unknown prompt: {name}")
del self._prompts[name]

async def render_prompt(
self,
name: str,
Expand Down
28 changes: 28 additions & 0 deletions src/mcp/server/mcpserver/resources/resource_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from pydantic import AnyUrl

from mcp.server.mcpserver.exceptions import ResourceError
from mcp.server.mcpserver.resources.base import Resource
from mcp.server.mcpserver.resources.templates import ResourceTemplate
from mcp.server.mcpserver.utilities.logging import get_logger
Expand Down Expand Up @@ -108,3 +109,30 @@ def list_templates(self) -> list[ResourceTemplate]:
"""List all registered templates."""
logger.debug("Listing templates", extra={"count": len(self._templates)})
return list(self._templates.values())

def remove_resource(self, uri: AnyUrl | str) -> None:
"""Remove a resource by URI.

Args:
uri: The URI of the resource to remove

Raises:
ResourceError: If the resource does not exist
"""
uri_str = str(uri)
if uri_str not in self._resources:
raise ResourceError(f"Unknown resource: {uri}")
del self._resources[uri_str]

def remove_template(self, uri_template: str) -> None:
"""Remove a resource template by URI template.

Args:
uri_template: The URI template of the template to remove

Raises:
ResourceError: If the template does not exist
"""
if uri_template not in self._templates:
raise ResourceError(f"Unknown template: {uri_template}")
del self._templates[uri_template]
33 changes: 33 additions & 0 deletions src/mcp/server/mcpserver/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,39 @@ def remove_tool(self, name: str) -> None:
"""
self._tool_manager.remove_tool(name)

def remove_prompt(self, name: str) -> None:
"""Remove a prompt from the server by name.

Args:
name: The name of the prompt to remove

Raises:
PromptError: If the prompt does not exist
"""
self._prompt_manager.remove_prompt(name)

def remove_resource(self, uri: str) -> None:
"""Remove a resource from the server by URI.

Args:
uri: The URI of the resource to remove

Raises:
ResourceError: If the resource does not exist
"""
self._resource_manager.remove_resource(uri)

def remove_resource_template(self, uri_template: str) -> None:
"""Remove a resource template from the server by URI template.

Args:
uri_template: The URI template of the template to remove

Raises:
ResourceError: If the template does not exist
"""
self._resource_manager.remove_template(uri_template)

def tool(
self,
name: str | None = None,
Expand Down
67 changes: 67 additions & 0 deletions tests/server/mcpserver/prompts/test_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,70 @@ def fn(name: str) -> str: # pragma: no cover
manager.add_prompt(prompt)
with pytest.raises(ValueError, match="Missing required arguments"):
await manager.render_prompt("fn", None, Context())


class TestRemovePrompt:
"""Test PromptManager.remove_prompt() functionality."""

def test_remove_existing_prompt(self):
"""Test removing an existing prompt."""

def fn() -> str: # pragma: no cover
return "Hello, world!"

manager = PromptManager()
prompt = Prompt.from_function(fn)
manager.add_prompt(prompt)

# Verify prompt exists
assert manager.get_prompt("fn") is not None
assert len(manager.list_prompts()) == 1

# Remove the prompt - should not raise any exception
manager.remove_prompt("fn")

# Verify prompt is removed
assert manager.get_prompt("fn") is None
assert len(manager.list_prompts()) == 0

def test_remove_nonexistent_prompt(self):
"""Test removing a non-existent prompt raises error."""
manager = PromptManager()

with pytest.raises(Exception, match="Unknown prompt: nonexistent"):
manager.remove_prompt("nonexistent")

def test_remove_one_prompt_from_multiple(self):
"""Test removing one prompt when multiple prompts exist."""

def fn1() -> str: # pragma: no cover
return "Hello, world!"

def fn2() -> str: # pragma: no cover
return "Goodbye, world!"

def fn3() -> str: # pragma: no cover
return "How are you?"

manager = PromptManager()
prompt1 = Prompt.from_function(fn1)
prompt2 = Prompt.from_function(fn2)
prompt3 = Prompt.from_function(fn3)
manager.add_prompt(prompt1)
manager.add_prompt(prompt2)
manager.add_prompt(prompt3)

# Verify all prompts exist
assert len(manager.list_prompts()) == 3
assert manager.get_prompt("fn1") is not None
assert manager.get_prompt("fn2") is not None
assert manager.get_prompt("fn3") is not None

# Remove middle prompt
manager.remove_prompt("fn2")

# Verify only fn2 is removed
assert len(manager.list_prompts()) == 2
assert manager.get_prompt("fn1") is not None
assert manager.get_prompt("fn2") is None
assert manager.get_prompt("fn3") is not None
163 changes: 163 additions & 0 deletions tests/server/mcpserver/resources/test_resource_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from pydantic import AnyUrl

from mcp.server.mcpserver import Context
from mcp.server.mcpserver.exceptions import ResourceError
from mcp.server.mcpserver.resources import FileResource, FunctionResource, ResourceManager, ResourceTemplate


Expand Down Expand Up @@ -175,3 +176,165 @@ def get_item(id: str) -> str: # pragma: no cover
)

assert template.meta is None


class TestRemoveResource:
"""Test ResourceManager.remove_resource() functionality."""

def test_remove_existing_resource(self, temp_file: Path):
"""Test removing an existing resource."""
manager = ResourceManager()
resource = FileResource(
uri=f"file://{temp_file}",
name="test",
path=temp_file,
)
manager.add_resource(resource)

# Verify resource exists
assert len(manager.list_resources()) == 1

# Remove the resource - should not raise any exception
manager.remove_resource(f"file://{temp_file}")

# Verify resource is removed
assert len(manager.list_resources()) == 0

def test_remove_nonexistent_resource(self):
"""Test removing a non-existent resource raises ResourceError."""
manager = ResourceManager()

with pytest.raises(ResourceError, match="Unknown resource: nonexistent"):
manager.remove_resource("nonexistent")

def test_remove_resource_with_anyurl(self, temp_file: Path):
"""Test removing a resource using AnyUrl instead of string."""
manager = ResourceManager()
resource = FileResource(
uri=f"file://{temp_file}",
name="test",
path=temp_file,
)
manager.add_resource(resource)

# Verify resource exists
assert len(manager.list_resources()) == 1

# Remove using AnyUrl - should work
manager.remove_resource(AnyUrl(f"file://{temp_file}"))

# Verify resource is removed
assert len(manager.list_resources()) == 0

def test_remove_one_resource_from_multiple(self, temp_file: Path):
"""Test removing one resource when multiple resources exist."""
manager = ResourceManager()
resource1 = FileResource(
uri=f"file://{temp_file}1",
name="test1",
path=temp_file,
)
resource2 = FileResource(
uri=f"file://{temp_file}2",
name="test2",
path=temp_file,
)
resource3 = FileResource(
uri=f"file://{temp_file}3",
name="test3",
path=temp_file,
)
manager.add_resource(resource1)
manager.add_resource(resource2)
manager.add_resource(resource3)

# Verify all resources exist
assert len(manager.list_resources()) == 3

# Remove middle resource
manager.remove_resource(f"file://{temp_file}2")

# Verify only resource2 is removed
assert len(manager.list_resources()) == 2
assert manager.list_resources() == [resource1, resource3]

@pytest.mark.anyio
async def test_call_removed_resource_raises_error(self, temp_file: Path):
"""Test that calling a removed resource raises ValueError."""
manager = ResourceManager()
resource = FileResource(
uri=f"file://{temp_file}",
name="test",
path=temp_file,
)
manager.add_resource(resource)

# Verify resource works before removal
result = await manager.get_resource(resource.uri, Context())
assert result == resource

# Remove the resource
manager.remove_resource(f"file://{temp_file}")

# Verify getting removed resource raises error
with pytest.raises(ValueError, match="Unknown resource"):
await manager.get_resource(resource.uri, Context())


class TestRemoveTemplate:
"""Test ResourceManager.remove_template() functionality."""

def test_remove_existing_template(self):
"""Test removing an existing template."""
manager = ResourceManager()

def greet(name: str) -> str:
return f"Hello, {name}!"

manager.add_template(
fn=greet,
uri_template="greet://{name}",
)

# Verify template exists
assert len(manager.list_templates()) == 1

# Remove the template
manager.remove_template("greet://{name}")

# Verify template is removed
assert len(manager.list_templates()) == 0

def test_remove_nonexistent_template(self):
"""Test removing a non-existent template raises ResourceError."""
manager = ResourceManager()

with pytest.raises(ResourceError, match="Unknown template: nonexistent"):
manager.remove_template("nonexistent")

def test_remove_one_template_from_multiple(self):
"""Test removing one template when multiple templates exist."""
manager = ResourceManager()

def greet(name: str) -> str:
return f"Hello, {name}!"

def farewell(name: str) -> str:
return f"Goodbye, {name}!"

def ask(question: str) -> str:
return f"What is {question}?"

template1 = manager.add_template(fn=greet, uri_template="greet://{name}")
manager.add_template(fn=farewell, uri_template="farewell://{name}")
template3 = manager.add_template(fn=ask, uri_template="ask://{question}")

# Verify all templates exist
assert len(manager.list_templates()) == 3

# Remove middle template
manager.remove_template("farewell://{name}")

# Verify only farewell template is removed
assert len(manager.list_templates()) == 2
assert manager.list_templates() == [template1, template3]
Loading
Loading