From d6f7dab9e65ae84987e1f90428cd5ad8d8b7fd38 Mon Sep 17 00:00:00 2001 From: davenpi Date: Tue, 13 May 2025 16:39:04 -0400 Subject: [PATCH 1/2] feat: add support for removing tools from server --- src/fastmcp/server/server.py | 12 ++++++++++++ src/fastmcp/tools/tool_manager.py | 14 ++++++++++++++ tests/server/test_server.py | 19 +++++++++++++++++++ tests/tools/test_tool_manager.py | 20 ++++++++++++++++++++ 4 files changed, 65 insertions(+) diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py index b17b4ecf0..90499e99f 100644 --- a/src/fastmcp/server/server.py +++ b/src/fastmcp/server/server.py @@ -457,6 +457,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 21e38c5f2..3114fa1ae 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"Tool {key!r} not found.") + 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 a777b755e..4d61d0520 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="Tool 'missing' not found"): + manager.remove_tool("missing") + def test_warn_on_duplicate_tools(self, caplog): """Test warning on duplicate tools.""" manager = ToolManager(duplicate_behavior="warn") From a6b7da45993df9c57b2b70aca70a652837b5b63e Mon Sep 17 00:00:00 2001 From: davenpi Date: Tue, 13 May 2025 16:54:16 -0400 Subject: [PATCH 2/2] chore: unify tool not found error message --- src/fastmcp/tools/tool_manager.py | 2 +- tests/tools/test_tool_manager.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/fastmcp/tools/tool_manager.py b/src/fastmcp/tools/tool_manager.py index 3114fa1ae..509f4e6de 100644 --- a/src/fastmcp/tools/tool_manager.py +++ b/src/fastmcp/tools/tool_manager.py @@ -106,7 +106,7 @@ class ToolManager: if key in self._tools: del self._tools[key] else: - raise NotFoundError(f"Tool {key!r} not found.") + raise NotFoundError(f"Unknown tool: {key}") async def call_tool( self, key: str, arguments: dict[str, Any] diff --git a/tests/tools/test_tool_manager.py b/tests/tools/test_tool_manager.py index 4d61d0520..9d35d177f 100644 --- a/tests/tools/test_tool_manager.py +++ b/tests/tools/test_tool_manager.py @@ -117,7 +117,7 @@ class TestAddTools: def test_remove_tool_missing_key(self): """Test removing a tool that does not exist raises NotFoundError.""" manager = ToolManager() - with pytest.raises(NotFoundError, match="Tool 'missing' not found"): + with pytest.raises(NotFoundError, match=f"Unknown tool: {'missing'}"): manager.remove_tool("missing") def test_warn_on_duplicate_tools(self, caplog):