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
7 changes: 7 additions & 0 deletions src/mcp/server/fastmcp/tools/tool_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,13 @@ def add_tool(
self._tools[tool.name] = tool
return tool

def remove_tool(self, name: str) -> None:
"""Remove a tool by name."""
if name not in self._tools:
logger.warning(f"Tried to remove unknown tool: {name}")
return
del self._tools[name]

async def call_tool(
self,
name: str,
Expand Down
118 changes: 118 additions & 0 deletions tests/server/fastmcp/test_tool_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -633,3 +633,121 @@ def get_scores() -> dict[str, int]:
# Test converted result
result = await manager.call_tool("get_scores", {})
assert result == expected


class TestRemoveTools:
"""Test tool removal functionality in the tool manager."""

def test_remove_existing_tool(self, caplog: pytest.LogCaptureFixture):
"""Test removing an existing tool."""

def add(a: int, b: int) -> int:
"""Add two numbers."""
return a + b

manager = ToolManager()
manager.add_tool(add)

# Verify tool exists
assert manager.get_tool("add") is not None
assert len(manager.list_tools()) == 1

# Remove the tool
with caplog.at_level(logging.WARNING):
manager.remove_tool("add")
# Should not log a warning for removing existing tool
assert "Tried to remove unknown tool: add" not in caplog.text

# Verify tool is removed
assert manager.get_tool("add") is None
assert len(manager.list_tools()) == 0

def test_remove_nonexistent_tool(self, caplog: pytest.LogCaptureFixture):
"""Test removing a non-existent tool logs a warning."""
manager = ToolManager()

with caplog.at_level(logging.WARNING):
manager.remove_tool("nonexistent")
assert "Tried to remove unknown tool: nonexistent" in caplog.text

def test_remove_tool_from_multiple_tools(self):
"""Test removing one tool when multiple tools exist."""

def add(a: int, b: int) -> int:
"""Add two numbers."""
return a + b

def multiply(a: int, b: int) -> int:
"""Multiply two numbers."""
return a * b

def divide(a: int, b: int) -> float:
"""Divide two numbers."""
return a / b

manager = ToolManager()
manager.add_tool(add)
manager.add_tool(multiply)
manager.add_tool(divide)

# Verify all tools exist
assert len(manager.list_tools()) == 3
assert manager.get_tool("add") is not None
assert manager.get_tool("multiply") is not None
assert manager.get_tool("divide") is not None

# Remove middle tool
manager.remove_tool("multiply")

# Verify only multiply is removed
assert len(manager.list_tools()) == 2
assert manager.get_tool("add") is not None
assert manager.get_tool("multiply") is None
assert manager.get_tool("divide") is not None

@pytest.mark.anyio
async def test_call_removed_tool_raises_error(self):
"""Test that calling a removed tool raises ToolError."""

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

manager = ToolManager()
manager.add_tool(greet)

# Verify tool works before removal
result = await manager.call_tool("greet", {"name": "World"})
assert result == "Hello, World!"

# Remove the tool
manager.remove_tool("greet")

# Verify calling removed tool raises error
with pytest.raises(ToolError, match="Unknown tool: greet"):
await manager.call_tool("greet", {"name": "World"})

def test_remove_tool_case_sensitive(self, caplog: pytest.LogCaptureFixture):
"""Test that tool removal is case-sensitive."""

def test_func() -> str:
"""Test function."""
return "test"

manager = ToolManager()
manager.add_tool(test_func)

# Verify tool exists
assert manager.get_tool("test_func") is not None

# Try to remove with different case
with caplog.at_level(logging.WARNING):
manager.remove_tool("Test_Func")
assert "Tried to remove unknown tool: Test_Func" in caplog.text

# Verify original tool still exists
assert manager.get_tool("test_func") is not None

# Remove with correct case
manager.remove_tool("test_func")
assert manager.get_tool("test_func") is None
Loading