diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py index b24051da5..5bca473ed 100644 --- a/src/fastmcp/server/server.py +++ b/src/fastmcp/server/server.py @@ -454,6 +454,18 @@ class FastMCP(Generic[LifespanResultT]): ) self._cache.clear() + def remove_tool(self, name: str) -> None: + """Remove a tool from the server. + + Args: + name: The name of the tool to remove + + Raises: + NotFoundError: If the tool is not found + """ + self._tool_manager.remove_tool(name) + self._cache.clear() + def tool( self, name: str | None = None, diff --git a/src/fastmcp/tools/tool_manager.py b/src/fastmcp/tools/tool_manager.py index 2fd163852..848f07139 100644 --- a/src/fastmcp/tools/tool_manager.py +++ b/src/fastmcp/tools/tool_manager.py @@ -94,6 +94,20 @@ class ToolManager: self._tools[key] = tool return tool + def remove_tool(self, key: str) -> None: + """Remove a tool from the server. + + Args: + key: The key of the tool to remove + + Raises: + NotFoundError: If the tool is not found + """ + if key in self._tools: + del self._tools[key] + else: + raise NotFoundError(f"Unknown tool: {key}") + async def call_tool( self, key: str, arguments: dict[str, Any] ) -> list[TextContent | ImageContent | EmbeddedResource]: diff --git a/tests/server/test_server.py b/tests/server/test_server.py index ead1dd673..f60959674 100644 --- a/tests/server/test_server.py +++ b/tests/server/test_server.py @@ -72,6 +72,25 @@ class TestTools: assert len(mcp_tools) == 1 assert mcp_tools[0].name == "custom_name" + async def test_remove_tool_successfully(self): + """Test that FastMCP.remove_tool removes the tool from the registry.""" + + mcp = FastMCP() + + @mcp.tool(name="adder") + def add(a: int, b: int) -> int: + return a + b + + mcp_tools = await mcp.get_tools() + assert "adder" in mcp_tools + + mcp.remove_tool("adder") + mcp_tools = await mcp.get_tools() + assert "adder" not in mcp_tools + + with pytest.raises(NotFoundError, match="Unknown tool: adder"): + await mcp._mcp_call_tool("adder", {"a": 1, "b": 2}) + class TestToolDecorator: async def test_no_tools_before_decorator(self): diff --git a/tests/tools/test_tool_manager.py b/tests/tools/test_tool_manager.py index b469b318e..7b82e26b1 100644 --- a/tests/tools/test_tool_manager.py +++ b/tests/tools/test_tool_manager.py @@ -100,6 +100,26 @@ class TestAddTools: ): manager.add_tool_from_fn(lambda x: x) + def test_remove_tool_successfully(self): + """Test removing an added tool by key.""" + manager = ToolManager() + + def add(a: int, b: int) -> int: + return a + b + + manager.add_tool_from_fn(add) + assert manager.get_tool("add") is not None + + manager.remove_tool("add") + with pytest.raises(NotFoundError): + manager.get_tool("add") + + def test_remove_tool_missing_key(self): + """Test removing a tool that does not exist raises NotFoundError.""" + manager = ToolManager() + with pytest.raises(NotFoundError, match=f"Unknown tool: {'missing'}"): + manager.remove_tool("missing") + def test_warn_on_duplicate_tools(self, caplog): """Test warning on duplicate tools.""" manager = ToolManager(duplicate_behavior="warn")