diff --git a/src/mcp/server/mcpserver/exceptions.py b/src/mcp/server/mcpserver/exceptions.py index dd1b75e82..ad301a6e9 100644 --- a/src/mcp/server/mcpserver/exceptions.py +++ b/src/mcp/server/mcpserver/exceptions.py @@ -17,5 +17,9 @@ class ToolError(MCPServerError): """Error in tool operations.""" +class ToolNotFoundError(ToolError): + """Tool not found.""" + + class InvalidSignature(Exception): """Invalid signature for use with MCPServer.""" diff --git a/src/mcp/server/mcpserver/tools/tool_manager.py b/src/mcp/server/mcpserver/tools/tool_manager.py index 32ed54797..dcfd233e9 100644 --- a/src/mcp/server/mcpserver/tools/tool_manager.py +++ b/src/mcp/server/mcpserver/tools/tool_manager.py @@ -3,7 +3,7 @@ from collections.abc import Callable from typing import TYPE_CHECKING, Any -from mcp.server.mcpserver.exceptions import ToolError +from mcp.server.mcpserver.exceptions import ToolNotFoundError from mcp.server.mcpserver.tools.base import Tool from mcp.server.mcpserver.utilities.logging import get_logger from mcp.types import Icon, ToolAnnotations @@ -74,7 +74,7 @@ def add_tool( def remove_tool(self, name: str) -> None: """Remove a tool by name.""" if name not in self._tools: - raise ToolError(f"Unknown tool: {name}") + raise ToolNotFoundError(f"Unknown tool: {name}") del self._tools[name] async def call_tool( @@ -87,6 +87,6 @@ async def call_tool( """Call a tool by name with arguments.""" tool = self.get_tool(name) if not tool: - raise ToolError(f"Unknown tool: {name}") + raise ToolNotFoundError(f"Unknown tool: {name}") return await tool.run(arguments, context, convert_result=convert_result) diff --git a/tests/server/mcpserver/test_server.py b/tests/server/mcpserver/test_server.py index 49b6deb4b..5bd7e5aae 100644 --- a/tests/server/mcpserver/test_server.py +++ b/tests/server/mcpserver/test_server.py @@ -13,7 +13,7 @@ from mcp.server.context import ServerRequestContext from mcp.server.experimental.request_context import Experimental from mcp.server.mcpserver import Context, MCPServer -from mcp.server.mcpserver.exceptions import ToolError +from mcp.server.mcpserver.exceptions import ToolError, ToolNotFoundError from mcp.server.mcpserver.prompts.base import Message, UserMessage from mcp.server.mcpserver.resources import FileResource, FunctionResource from mcp.server.mcpserver.utilities.types import Audio, Image @@ -636,6 +636,13 @@ async def test_remove_nonexistent_tool(self): with pytest.raises(ToolError, match="Unknown tool: nonexistent"): mcp.remove_tool("nonexistent") + async def test_remove_nonexistent_tool_raises_tool_not_found_error(self): + """Test that removing a non-existent tool raises ToolNotFoundError.""" + mcp = MCPServer() + + with pytest.raises(ToolNotFoundError, match="Unknown tool: nonexistent"): + mcp.remove_tool("nonexistent") + async def test_remove_tool_and_list(self): """Test that a removed tool doesn't appear in list_tools.""" mcp = MCPServer() diff --git a/tests/server/mcpserver/test_tool_manager.py b/tests/server/mcpserver/test_tool_manager.py index e4dfd4ff9..10fd905c5 100644 --- a/tests/server/mcpserver/test_tool_manager.py +++ b/tests/server/mcpserver/test_tool_manager.py @@ -8,7 +8,7 @@ from mcp.server.context import LifespanContextT, RequestT from mcp.server.mcpserver import Context, MCPServer -from mcp.server.mcpserver.exceptions import ToolError +from mcp.server.mcpserver.exceptions import ToolError, ToolNotFoundError from mcp.server.mcpserver.tools import Tool, ToolManager from mcp.server.mcpserver.utilities.func_metadata import ArgModelBase, FuncMetadata from mcp.types import TextContent, ToolAnnotations @@ -820,6 +820,13 @@ def test_remove_nonexistent_tool(self): with pytest.raises(ToolError, match="Unknown tool: nonexistent"): manager.remove_tool("nonexistent") + def test_remove_nonexistent_tool_raises_tool_not_found_error(self): + """Test removing a non-existent tool raises ToolError.""" + manager = ToolManager() + + with pytest.raises(ToolNotFoundError, match="Unknown tool: nonexistent"): + manager.remove_tool("nonexistent") + def test_remove_tool_from_multiple_tools(self): """Test removing one tool when multiple tools exist.""" @@ -877,6 +884,10 @@ def greet(name: str) -> str: with pytest.raises(ToolError, match="Unknown tool: greet"): await manager.call_tool("greet", {"name": "World"}, Context()) + # Verify calling removed tool raises ToolNotFoundError + with pytest.raises(ToolNotFoundError, match="Unknown tool: greet"): + await manager.call_tool("greet", {"name": "World"}, Context()) + def test_remove_tool_case_sensitive(self): """Test that tool removal is case-sensitive.""" @@ -894,6 +905,10 @@ def test_func() -> str: # pragma: no cover with pytest.raises(ToolError, match="Unknown tool: Test_Func"): manager.remove_tool("Test_Func") + # Try to remove with different case - should raise ToolNotFoundError + with pytest.raises(ToolNotFoundError, match="Unknown tool: Test_Func"): + manager.remove_tool("Test_Func") + # Verify original tool still exists assert manager.get_tool("test_func") is not None