Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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."""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably say a word about the beahviour if the tool doesn't exists.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should raise if not found.

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