Skip to content

Commit 41084e9

Browse files
maxisbeyclaude
andcommitted
feat: add remove_tool method to FastMCP
Add a remove_tool method to the FastMCP class that allows tools to be dynamically removed from a server at runtime. This enables use cases where tools should only be available based on initialization state or user actions. The implementation includes: - remove_tool() method on FastMCP class that wraps the existing ToolManager.remove_tool() functionality - Comprehensive test coverage for tool removal scenarios - Proper error handling when attempting to remove non-existent tools 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> Github-Issue: #1322
1 parent dcb9e35 commit 41084e9

File tree

2 files changed

+85
-0
lines changed

2 files changed

+85
-0
lines changed

src/mcp/server/fastmcp/server.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,17 @@ def add_tool(
374374
structured_output=structured_output,
375375
)
376376

377+
def remove_tool(self, name: str) -> None:
378+
"""Remove a tool from the server by name.
379+
380+
Args:
381+
name: The name of the tool to remove
382+
383+
Raises:
384+
ToolError: If the tool does not exist
385+
"""
386+
self._tool_manager.remove_tool(name)
387+
377388
def tool(
378389
self,
379390
name: str | None = None,

tests/server/fastmcp/test_server.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,80 @@ def get_settings() -> dict[str, str]:
603603
assert result.isError is False
604604
assert result.structuredContent == {"theme": "dark", "language": "en", "timezone": "UTC"}
605605

606+
@pytest.mark.anyio
607+
async def test_remove_tool(self):
608+
"""Test removing a tool from the server."""
609+
mcp = FastMCP()
610+
mcp.add_tool(tool_fn)
611+
612+
# Verify tool exists
613+
assert len(mcp._tool_manager.list_tools()) == 1
614+
615+
# Remove the tool
616+
mcp.remove_tool("tool_fn")
617+
618+
# Verify tool is removed
619+
assert len(mcp._tool_manager.list_tools()) == 0
620+
621+
@pytest.mark.anyio
622+
async def test_remove_nonexistent_tool(self):
623+
"""Test that removing a non-existent tool raises ToolError."""
624+
from mcp.server.fastmcp.exceptions import ToolError
625+
626+
mcp = FastMCP()
627+
628+
with pytest.raises(ToolError, match="Unknown tool: nonexistent"):
629+
mcp.remove_tool("nonexistent")
630+
631+
@pytest.mark.anyio
632+
async def test_remove_tool_and_list(self):
633+
"""Test that a removed tool doesn't appear in list_tools."""
634+
mcp = FastMCP()
635+
mcp.add_tool(tool_fn)
636+
mcp.add_tool(error_tool_fn)
637+
638+
# Verify both tools exist
639+
async with client_session(mcp._mcp_server) as client:
640+
tools = await client.list_tools()
641+
assert len(tools.tools) == 2
642+
tool_names = [t.name for t in tools.tools]
643+
assert "tool_fn" in tool_names
644+
assert "error_tool_fn" in tool_names
645+
646+
# Remove one tool
647+
mcp.remove_tool("tool_fn")
648+
649+
# Verify only one tool remains
650+
async with client_session(mcp._mcp_server) as client:
651+
tools = await client.list_tools()
652+
assert len(tools.tools) == 1
653+
assert tools.tools[0].name == "error_tool_fn"
654+
655+
@pytest.mark.anyio
656+
async def test_remove_tool_and_call(self):
657+
"""Test that calling a removed tool fails appropriately."""
658+
mcp = FastMCP()
659+
mcp.add_tool(tool_fn)
660+
661+
# Verify tool works before removal
662+
async with client_session(mcp._mcp_server) as client:
663+
result = await client.call_tool("tool_fn", {"x": 1, "y": 2})
664+
assert not result.isError
665+
content = result.content[0]
666+
assert isinstance(content, TextContent)
667+
assert content.text == "3"
668+
669+
# Remove the tool
670+
mcp.remove_tool("tool_fn")
671+
672+
# Verify calling removed tool returns an error
673+
async with client_session(mcp._mcp_server) as client:
674+
result = await client.call_tool("tool_fn", {"x": 1, "y": 2})
675+
assert result.isError
676+
content = result.content[0]
677+
assert isinstance(content, TextContent)
678+
assert "Unknown tool" in content.text
679+
606680

607681
class TestServerResources:
608682
@pytest.mark.anyio

0 commit comments

Comments
 (0)