Skip to content

Commit a1e2c67

Browse files
feat: add remove_prompt(), remove_resource(), and remove_resource_template()
Add removal APIs for prompts, resources, and resource templates to match the existing remove_tool() pattern. This completes the CRUD symmetry for all three MCP primitives, enabling per-instance filtering without reaching into private internals. Fixes #2331 Github-Issue:#2331
1 parent 92c693b commit a1e2c67

File tree

5 files changed

+242
-1
lines changed

5 files changed

+242
-1
lines changed

src/mcp/server/mcpserver/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,9 @@ class ToolError(MCPServerError):
1717
"""Error in tool operations."""
1818

1919

20+
class PromptError(MCPServerError):
21+
"""Error in prompt operations."""
22+
23+
2024
class InvalidSignature(Exception):
2125
"""Invalid signature for use with MCPServer."""

src/mcp/server/mcpserver/prompts/manager.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from typing import TYPE_CHECKING, Any
66

7+
from mcp.server.mcpserver.exceptions import PromptError
78
from mcp.server.mcpserver.prompts.base import Message, Prompt
89
from mcp.server.mcpserver.utilities.logging import get_logger
910

@@ -45,6 +46,12 @@ def add_prompt(
4546
self._prompts[prompt.name] = prompt
4647
return prompt
4748

49+
def remove_prompt(self, name: str) -> None:
50+
"""Remove a prompt by name."""
51+
if name not in self._prompts:
52+
raise PromptError(f"Unknown prompt: {name}")
53+
del self._prompts[name]
54+
4855
async def render_prompt(
4956
self,
5057
name: str,

src/mcp/server/mcpserver/resources/resource_manager.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from pydantic import AnyUrl
99

10+
from mcp.server.mcpserver.exceptions import ResourceError
1011
from mcp.server.mcpserver.resources.base import Resource
1112
from mcp.server.mcpserver.resources.templates import ResourceTemplate
1213
from mcp.server.mcpserver.utilities.logging import get_logger
@@ -53,6 +54,20 @@ def add_resource(self, resource: Resource) -> Resource:
5354
self._resources[str(resource.uri)] = resource
5455
return resource
5556

57+
def remove_resource(self, uri: AnyUrl | str) -> None:
58+
"""Remove a resource by URI.
59+
60+
Args:
61+
uri: The URI of the resource to remove
62+
63+
Raises:
64+
ResourceError: If the resource does not exist
65+
"""
66+
uri_str = str(uri)
67+
if uri_str not in self._resources:
68+
raise ResourceError(f"Unknown resource: {uri}")
69+
del self._resources[uri_str]
70+
5671
def add_template(
5772
self,
5873
fn: Callable[..., Any],
@@ -80,6 +95,19 @@ def add_template(
8095
self._templates[template.uri_template] = template
8196
return template
8297

98+
def remove_template(self, uri_template: str) -> None:
99+
"""Remove a resource template by URI template.
100+
101+
Args:
102+
uri_template: The URI template string to remove
103+
104+
Raises:
105+
ResourceError: If the template does not exist
106+
"""
107+
if uri_template not in self._templates:
108+
raise ResourceError(f"Unknown resource template: {uri_template}")
109+
del self._templates[uri_template]
110+
83111
async def get_resource(self, uri: AnyUrl | str, context: Context[LifespanContextT, RequestT]) -> Resource:
84112
"""Get resource by URI, checking concrete resources first, then templates."""
85113
uri_str = str(uri)

src/mcp/server/mcpserver/server.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,28 @@ def add_resource(self, resource: Resource) -> None:
615615
"""
616616
self._resource_manager.add_resource(resource)
617617

618+
def remove_resource(self, uri: str) -> None:
619+
"""Remove a resource from the server by URI.
620+
621+
Args:
622+
uri: The URI of the resource to remove
623+
624+
Raises:
625+
ResourceError: If the resource does not exist
626+
"""
627+
self._resource_manager.remove_resource(uri)
628+
629+
def remove_resource_template(self, uri_template: str) -> None:
630+
"""Remove a resource template from the server by URI template.
631+
632+
Args:
633+
uri_template: The URI template string to remove
634+
635+
Raises:
636+
ResourceError: If the template does not exist
637+
"""
638+
self._resource_manager.remove_template(uri_template)
639+
618640
def resource(
619641
self,
620642
uri: str,
@@ -735,6 +757,17 @@ def add_prompt(self, prompt: Prompt) -> None:
735757
"""
736758
self._prompt_manager.add_prompt(prompt)
737759

760+
def remove_prompt(self, name: str) -> None:
761+
"""Remove a prompt from the server by name.
762+
763+
Args:
764+
name: The name of the prompt to remove
765+
766+
Raises:
767+
PromptError: If the prompt does not exist
768+
"""
769+
self._prompt_manager.remove_prompt(name)
770+
738771
def prompt(
739772
self,
740773
name: str | None = None,

tests/server/mcpserver/test_server.py

Lines changed: 170 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from mcp.server.context import ServerRequestContext
1414
from mcp.server.experimental.request_context import Experimental
1515
from mcp.server.mcpserver import Context, MCPServer
16-
from mcp.server.mcpserver.exceptions import ToolError
16+
from mcp.server.mcpserver.exceptions import PromptError, ResourceError, ToolError
1717
from mcp.server.mcpserver.prompts.base import Message, UserMessage
1818
from mcp.server.mcpserver.resources import FileResource, FunctionResource
1919
from mcp.server.mcpserver.utilities.types import Audio, Image
@@ -785,6 +785,69 @@ def get_data() -> str: # pragma: no cover
785785
assert resource.name == "test_get_data"
786786
assert resource.mime_type == "text/plain"
787787

788+
async def test_remove_resource(self):
789+
"""Test removing a resource from the server."""
790+
mcp = MCPServer()
791+
792+
@mcp.resource("resource://test")
793+
def get_data() -> str: # pragma: no cover
794+
return "Hello"
795+
796+
assert len(mcp._resource_manager.list_resources()) == 1
797+
798+
mcp.remove_resource("resource://test")
799+
800+
assert len(mcp._resource_manager.list_resources()) == 0
801+
802+
async def test_remove_nonexistent_resource(self):
803+
"""Test that removing a non-existent resource raises ResourceError."""
804+
mcp = MCPServer()
805+
806+
with pytest.raises(ResourceError, match="Unknown resource: resource://nonexistent"):
807+
mcp.remove_resource("resource://nonexistent")
808+
809+
async def test_remove_resource_and_list(self):
810+
"""Test that a removed resource doesn't appear in list_resources."""
811+
mcp = MCPServer()
812+
813+
@mcp.resource("resource://first")
814+
def first() -> str: # pragma: no cover
815+
return "first"
816+
817+
@mcp.resource("resource://second")
818+
def second() -> str: # pragma: no cover
819+
return "second"
820+
821+
async with Client(mcp) as client:
822+
resources = await client.list_resources()
823+
assert len(resources.resources) == 2
824+
825+
mcp.remove_resource("resource://first")
826+
827+
async with Client(mcp) as client:
828+
resources = await client.list_resources()
829+
assert len(resources.resources) == 1
830+
assert resources.resources[0].uri == "resource://second"
831+
832+
async def test_remove_resource_and_read(self):
833+
"""Test that reading a removed resource fails appropriately."""
834+
mcp = MCPServer()
835+
836+
@mcp.resource("resource://test")
837+
def get_data() -> str: # pragma: no cover
838+
return "Hello"
839+
840+
async with Client(mcp) as client:
841+
result = await client.read_resource("resource://test")
842+
assert isinstance(result.contents[0], TextResourceContents)
843+
assert result.contents[0].text == "Hello"
844+
845+
mcp.remove_resource("resource://test")
846+
847+
async with Client(mcp) as client:
848+
with pytest.raises(MCPError, match="Unknown resource"):
849+
await client.read_resource("resource://test")
850+
788851

789852
class TestServerResourceTemplates:
790853
async def test_resource_with_params(self):
@@ -920,6 +983,50 @@ def get_csv(user: str) -> str:
920983
)
921984
)
922985

986+
async def test_remove_resource_template(self):
987+
"""Test removing a resource template from the server."""
988+
mcp = MCPServer()
989+
990+
@mcp.resource("resource://{name}/data")
991+
def get_data(name: str) -> str: # pragma: no cover
992+
return f"Data for {name}"
993+
994+
assert len(mcp._resource_manager._templates) == 1
995+
996+
mcp.remove_resource_template("resource://{name}/data")
997+
998+
assert len(mcp._resource_manager._templates) == 0
999+
1000+
async def test_remove_nonexistent_resource_template(self):
1001+
"""Test that removing a non-existent template raises ResourceError."""
1002+
mcp = MCPServer()
1003+
1004+
with pytest.raises(ResourceError, match="Unknown resource template: resource://\\{name\\}/data"):
1005+
mcp.remove_resource_template("resource://{name}/data")
1006+
1007+
async def test_remove_resource_template_and_list(self):
1008+
"""Test that a removed template doesn't appear in list_resource_templates."""
1009+
mcp = MCPServer()
1010+
1011+
@mcp.resource("resource://{name}/first")
1012+
def first(name: str) -> str: # pragma: no cover
1013+
return f"first {name}"
1014+
1015+
@mcp.resource("resource://{name}/second")
1016+
def second(name: str) -> str: # pragma: no cover
1017+
return f"second {name}"
1018+
1019+
async with Client(mcp) as client:
1020+
templates = await client.list_resource_templates()
1021+
assert len(templates.resource_templates) == 2
1022+
1023+
mcp.remove_resource_template("resource://{name}/first")
1024+
1025+
async with Client(mcp) as client:
1026+
templates = await client.list_resource_templates()
1027+
assert len(templates.resource_templates) == 1
1028+
assert templates.resource_templates[0].uri_template == "resource://{name}/second"
1029+
9231030

9241031
class TestServerResourceMetadata:
9251032
"""Test MCPServer @resource decorator meta parameter for list operations.
@@ -1418,6 +1525,68 @@ def prompt_fn(name: str) -> str: ... # pragma: no branch
14181525
with pytest.raises(MCPError, match="Missing required arguments"):
14191526
await client.get_prompt("prompt_fn")
14201527

1528+
async def test_remove_prompt(self):
1529+
"""Test removing a prompt from the server."""
1530+
mcp = MCPServer()
1531+
1532+
@mcp.prompt()
1533+
def fn() -> str: # pragma: no cover
1534+
return "Hello"
1535+
1536+
assert len(mcp._prompt_manager.list_prompts()) == 1
1537+
1538+
mcp.remove_prompt("fn")
1539+
1540+
assert len(mcp._prompt_manager.list_prompts()) == 0
1541+
1542+
async def test_remove_nonexistent_prompt(self):
1543+
"""Test that removing a non-existent prompt raises PromptError."""
1544+
mcp = MCPServer()
1545+
1546+
with pytest.raises(PromptError, match="Unknown prompt: nonexistent"):
1547+
mcp.remove_prompt("nonexistent")
1548+
1549+
async def test_remove_prompt_and_list(self):
1550+
"""Test that a removed prompt doesn't appear in list_prompts."""
1551+
mcp = MCPServer()
1552+
1553+
@mcp.prompt()
1554+
def first() -> str: # pragma: no cover
1555+
return "first"
1556+
1557+
@mcp.prompt()
1558+
def second() -> str: # pragma: no cover
1559+
return "second"
1560+
1561+
async with Client(mcp) as client:
1562+
prompts = await client.list_prompts()
1563+
assert len(prompts.prompts) == 2
1564+
1565+
mcp.remove_prompt("first")
1566+
1567+
async with Client(mcp) as client:
1568+
prompts = await client.list_prompts()
1569+
assert len(prompts.prompts) == 1
1570+
assert prompts.prompts[0].name == "second"
1571+
1572+
async def test_remove_prompt_and_get(self):
1573+
"""Test that getting a removed prompt fails appropriately."""
1574+
mcp = MCPServer()
1575+
1576+
@mcp.prompt()
1577+
def fn() -> str: # pragma: no cover
1578+
return "Hello"
1579+
1580+
async with Client(mcp) as client:
1581+
result = await client.get_prompt("fn")
1582+
assert result.messages[0].content == TextContent(text="Hello")
1583+
1584+
mcp.remove_prompt("fn")
1585+
1586+
async with Client(mcp) as client:
1587+
with pytest.raises(MCPError, match="Unknown prompt"):
1588+
await client.get_prompt("fn")
1589+
14211590

14221591
async def test_completion_decorator() -> None:
14231592
"""Test that the completion decorator registers a working handler."""

0 commit comments

Comments
 (0)