Skip to content
This repository was archived by the owner on Nov 10, 2025. It is now read-only.

Commit 923c7b0

Browse files
authored
Support to filter available MCP Tools (#345)
* feat: support to complex filter on ToolCollection * refactor: use proper tool collection methot to filter tool in CrewAiEnterpriseTools * feat: allow to filter available MCP tools
1 parent 1773033 commit 923c7b0

File tree

5 files changed

+193
-23
lines changed

5 files changed

+193
-23
lines changed

crewai_tools/adapters/mcp_adapter.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,18 @@ class MCPServerAdapter:
4646
with MCPServerAdapter({"url": "http://localhost:8000/sse"}) as tools:
4747
# tools is now available
4848
49+
# context manager with filtered tools
50+
with MCPServerAdapter(..., "tool1", "tool2") as filtered_tools:
51+
# only tool1 and tool2 are available
52+
4953
# manually stop mcp server
5054
try:
5155
mcp_server = MCPServerAdapter(...)
52-
tools = mcp_server.tools
56+
tools = mcp_server.tools # all tools
57+
58+
# or with filtered tools
59+
mcp_server = MCPServerAdapter(..., "tool1", "tool2")
60+
filtered_tools = mcp_server.tools # only tool1 and tool2
5361
...
5462
finally:
5563
mcp_server.stop()
@@ -61,18 +69,22 @@ class MCPServerAdapter:
6169
def __init__(
6270
self,
6371
serverparams: StdioServerParameters | dict[str, Any],
72+
*tool_names: str,
6473
):
6574
"""Initialize the MCP Server
6675
6776
Args:
6877
serverparams: The parameters for the MCP server it supports either a
6978
`StdioServerParameters` or a `dict` respectively for STDIO and SSE.
79+
*tool_names: Optional names of tools to filter. If provided, only tools with
80+
matching names will be available.
7081
7182
"""
7283

7384
super().__init__()
7485
self._adapter = None
7586
self._tools = None
87+
self._tool_names = list(tool_names) if tool_names else None
7688

7789
if not MCP_AVAILABLE:
7890
import click
@@ -127,7 +139,11 @@ def tools(self) -> ToolCollection[BaseTool]:
127139
raise ValueError(
128140
"MCP server not started, run `mcp_server.start()` first before accessing `tools`"
129141
)
130-
return ToolCollection(self._tools)
142+
143+
tools_collection = ToolCollection(self._tools)
144+
if self._tool_names:
145+
return tools_collection.filter_by_names(self._tool_names)
146+
return tools_collection
131147

132148
def __enter__(self):
133149
"""

crewai_tools/adapters/tool_collection.py

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import List, Optional, Union, TypeVar, Generic, Dict
1+
from typing import List, Optional, Union, TypeVar, Generic, Dict, Callable
22
from crewai.tools import BaseTool
33

44
T = TypeVar('T', bound=BaseTool)
@@ -24,36 +24,51 @@ def __init__(self, tools: Optional[List[T]] = None):
2424
self._build_name_cache()
2525

2626
def _build_name_cache(self) -> None:
27-
self._name_cache = {tool.name: tool for tool in self}
27+
self._name_cache = {tool.name.lower(): tool for tool in self}
2828

2929
def __getitem__(self, key: Union[int, str]) -> T:
3030
if isinstance(key, str):
31-
return self._name_cache[key]
31+
return self._name_cache[key.lower()]
3232
return super().__getitem__(key)
3333

3434
def append(self, tool: T) -> None:
3535
super().append(tool)
36-
self._name_cache[tool.name] = tool
36+
self._name_cache[tool.name.lower()] = tool
3737

3838
def extend(self, tools: List[T]) -> None:
3939
super().extend(tools)
4040
self._build_name_cache()
4141

4242
def insert(self, index: int, tool: T) -> None:
4343
super().insert(index, tool)
44-
self._name_cache[tool.name] = tool
44+
self._name_cache[tool.name.lower()] = tool
4545

4646
def remove(self, tool: T) -> None:
4747
super().remove(tool)
48-
if tool.name in self._name_cache:
49-
del self._name_cache[tool.name]
48+
if tool.name.lower() in self._name_cache:
49+
del self._name_cache[tool.name.lower()]
5050

5151
def pop(self, index: int = -1) -> T:
5252
tool = super().pop(index)
53-
if tool.name in self._name_cache:
54-
del self._name_cache[tool.name]
53+
if tool.name.lower() in self._name_cache:
54+
del self._name_cache[tool.name.lower()]
5555
return tool
5656

57+
def filter_by_names(self, names: Optional[List[str]] = None) -> "ToolCollection[T]":
58+
if names is None:
59+
return self
60+
61+
return ToolCollection(
62+
[
63+
tool
64+
for name in names
65+
if (tool := self._name_cache.get(name.lower())) is not None
66+
]
67+
)
68+
69+
def filter_where(self, func: Callable[[T], bool]) -> "ToolCollection[T]":
70+
return ToolCollection([tool for tool in self if func(tool)])
71+
5772
def clear(self) -> None:
5873
super().clear()
59-
self._name_cache.clear()
74+
self._name_cache.clear()

crewai_tools/tools/crewai_enterprise_tools/crewai_enterprise_tools.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,5 @@ def CrewaiEnterpriseTools(
4949
adapter = EnterpriseActionKitToolAdapter(**adapter_kwargs)
5050
all_tools = adapter.tools()
5151

52-
if actions_list is None:
53-
return ToolCollection(all_tools)
54-
5552
# Filter tools based on the provided list
56-
filtered_tools = [tool for tool in all_tools if tool.name.lower() in [action.lower() for action in actions_list]]
57-
return ToolCollection(filtered_tools)
53+
return ToolCollection(all_tools).filter_by_names(actions_list)

tests/adapters/mcp_adapter_test.py

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ def echo_tool(text: str) -> str:
1919
"""Echo the input text"""
2020
return f"Echo: {text}"
2121
22+
@mcp.tool()
23+
def calc_tool(a: int, b: int) -> int:
24+
"""Calculate a + b"""
25+
return a + b
26+
2227
mcp.run()
2328
'''
2429
)
@@ -37,6 +42,11 @@ def echo_tool(text: str) -> str:
3742
"""Echo the input text"""
3843
return f"Echo: {text}"
3944
45+
@mcp.tool()
46+
def calc_tool(a: int, b: int) -> int:
47+
"""Calculate a + b"""
48+
return a + b
49+
4050
mcp.run("sse")
4151
'''
4252
)
@@ -69,16 +79,20 @@ def test_context_manager_syntax(echo_server_script):
6979
)
7080
with MCPServerAdapter(serverparams) as tools:
7181
assert isinstance(tools, ToolCollection)
72-
assert len(tools) == 1
82+
assert len(tools) == 2
7383
assert tools[0].name == "echo_tool"
84+
assert tools[1].name == "calc_tool"
7485
assert tools[0].run(text="hello") == "Echo: hello"
86+
assert tools[1].run(a=5, b=3) == '8'
7587

7688
def test_context_manager_syntax_sse(echo_sse_server):
7789
sse_serverparams = echo_sse_server
7890
with MCPServerAdapter(sse_serverparams) as tools:
79-
assert len(tools) == 1
91+
assert len(tools) == 2
8092
assert tools[0].name == "echo_tool"
93+
assert tools[1].name == "calc_tool"
8194
assert tools[0].run(text="hello") == "Echo: hello"
95+
assert tools[1].run(a=5, b=3) == '8'
8296

8397
def test_try_finally_syntax(echo_server_script):
8498
serverparams = StdioServerParameters(
@@ -87,9 +101,11 @@ def test_try_finally_syntax(echo_server_script):
87101
try:
88102
mcp_server_adapter = MCPServerAdapter(serverparams)
89103
tools = mcp_server_adapter.tools
90-
assert len(tools) == 1
104+
assert len(tools) == 2
91105
assert tools[0].name == "echo_tool"
106+
assert tools[1].name == "calc_tool"
92107
assert tools[0].run(text="hello") == "Echo: hello"
108+
assert tools[1].run(a=5, b=3) == '8'
93109
finally:
94110
mcp_server_adapter.stop()
95111

@@ -98,8 +114,76 @@ def test_try_finally_syntax_sse(echo_sse_server):
98114
mcp_server_adapter = MCPServerAdapter(sse_serverparams)
99115
try:
100116
tools = mcp_server_adapter.tools
117+
assert len(tools) == 2
118+
assert tools[0].name == "echo_tool"
119+
assert tools[1].name == "calc_tool"
120+
assert tools[0].run(text="hello") == "Echo: hello"
121+
assert tools[1].run(a=5, b=3) == '8'
122+
finally:
123+
mcp_server_adapter.stop()
124+
125+
def test_context_manager_with_filtered_tools(echo_server_script):
126+
serverparams = StdioServerParameters(
127+
command="uv", args=["run", "python", "-c", echo_server_script]
128+
)
129+
# Only select the echo_tool
130+
with MCPServerAdapter(serverparams, "echo_tool") as tools:
131+
assert isinstance(tools, ToolCollection)
101132
assert len(tools) == 1
102133
assert tools[0].name == "echo_tool"
103134
assert tools[0].run(text="hello") == "Echo: hello"
135+
# Check that calc_tool is not present
136+
with pytest.raises(IndexError):
137+
_ = tools[1]
138+
with pytest.raises(KeyError):
139+
_ = tools["calc_tool"]
140+
141+
def test_context_manager_sse_with_filtered_tools(echo_sse_server):
142+
sse_serverparams = echo_sse_server
143+
# Only select the calc_tool
144+
with MCPServerAdapter(sse_serverparams, "calc_tool") as tools:
145+
assert isinstance(tools, ToolCollection)
146+
assert len(tools) == 1
147+
assert tools[0].name == "calc_tool"
148+
assert tools[0].run(a=10, b=5) == '15'
149+
# Check that echo_tool is not present
150+
with pytest.raises(IndexError):
151+
_ = tools[1]
152+
with pytest.raises(KeyError):
153+
_ = tools["echo_tool"]
154+
155+
def test_try_finally_with_filtered_tools(echo_server_script):
156+
serverparams = StdioServerParameters(
157+
command="uv", args=["run", "python", "-c", echo_server_script]
158+
)
159+
try:
160+
# Select both tools but in reverse order
161+
mcp_server_adapter = MCPServerAdapter(serverparams, "calc_tool", "echo_tool")
162+
tools = mcp_server_adapter.tools
163+
assert len(tools) == 2
164+
# The order of tools is based on filter_by_names which preserves
165+
# the original order from the collection
166+
assert tools[0].name == "calc_tool"
167+
assert tools[1].name == "echo_tool"
104168
finally:
105169
mcp_server_adapter.stop()
170+
171+
def test_filter_with_nonexistent_tool(echo_server_script):
172+
serverparams = StdioServerParameters(
173+
command="uv", args=["run", "python", "-c", echo_server_script]
174+
)
175+
# Include a tool that doesn't exist
176+
with MCPServerAdapter(serverparams, "echo_tool", "nonexistent_tool") as tools:
177+
# Only echo_tool should be in the result
178+
assert len(tools) == 1
179+
assert tools[0].name == "echo_tool"
180+
181+
def test_filter_with_only_nonexistent_tools(echo_server_script):
182+
serverparams = StdioServerParameters(
183+
command="uv", args=["run", "python", "-c", echo_server_script]
184+
)
185+
# All requested tools don't exist
186+
with MCPServerAdapter(serverparams, "nonexistent1", "nonexistent2") as tools:
187+
# Should return an empty tool collection
188+
assert isinstance(tools, ToolCollection)
189+
assert len(tools) == 0

tests/tools/tool_collection_test.py

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
class TestToolCollection(unittest.TestCase):
99
def setUp(self):
1010

11-
self.search_tool = self._create_mock_tool("search", "Search Tool")
11+
self.search_tool = self._create_mock_tool("SearcH", "Search Tool") # Tool name is case sensitive
1212
self.calculator_tool = self._create_mock_tool("calculator", "Calculator Tool")
1313
self.translator_tool = self._create_mock_tool("translator", "Translator Tool")
1414

@@ -26,7 +26,7 @@ def _create_mock_tool(self, name, description):
2626

2727
def test_initialization(self):
2828
self.assertEqual(len(self.tools), 3)
29-
self.assertEqual(self.tools[0].name, "search")
29+
self.assertEqual(self.tools[0].name, "SearcH")
3030
self.assertEqual(self.tools[1].name, "calculator")
3131
self.assertEqual(self.tools[2].name, "translator")
3232

@@ -169,4 +169,63 @@ def test_getitem_with_tool_name_as_int(self):
169169
self.assertEqual(self.tools["123"], numeric_name_tool)
170170

171171
with self.assertRaises(IndexError):
172-
_ = self.tools[123]
172+
_ = self.tools[123]
173+
174+
def test_filter_by_names(self):
175+
176+
filtered = self.tools.filter_by_names(None)
177+
178+
self.assertIsInstance(filtered, ToolCollection)
179+
self.assertEqual(len(filtered), 3)
180+
181+
filtered = self.tools.filter_by_names(["search", "translator"])
182+
183+
self.assertIsInstance(filtered, ToolCollection)
184+
self.assertEqual(len(filtered), 2)
185+
self.assertEqual(filtered[0], self.search_tool)
186+
self.assertEqual(filtered[1], self.translator_tool)
187+
self.assertEqual(filtered["search"], self.search_tool)
188+
self.assertEqual(filtered["translator"], self.translator_tool)
189+
190+
filtered = self.tools.filter_by_names(["search", "nonexistent"])
191+
192+
self.assertIsInstance(filtered, ToolCollection)
193+
self.assertEqual(len(filtered), 1)
194+
self.assertEqual(filtered[0], self.search_tool)
195+
196+
filtered = self.tools.filter_by_names(["nonexistent1", "nonexistent2"])
197+
198+
self.assertIsInstance(filtered, ToolCollection)
199+
self.assertEqual(len(filtered), 0)
200+
201+
filtered = self.tools.filter_by_names([])
202+
203+
self.assertIsInstance(filtered, ToolCollection)
204+
self.assertEqual(len(filtered), 0)
205+
206+
def test_filter_where(self):
207+
filtered = self.tools.filter_where(lambda tool: tool.name.startswith("S"))
208+
209+
self.assertIsInstance(filtered, ToolCollection)
210+
self.assertEqual(len(filtered), 1)
211+
self.assertEqual(filtered[0], self.search_tool)
212+
self.assertEqual(filtered["search"], self.search_tool)
213+
214+
filtered = self.tools.filter_where(lambda tool: True)
215+
216+
self.assertIsInstance(filtered, ToolCollection)
217+
self.assertEqual(len(filtered), 3)
218+
self.assertEqual(filtered[0], self.search_tool)
219+
self.assertEqual(filtered[1], self.calculator_tool)
220+
self.assertEqual(filtered[2], self.translator_tool)
221+
222+
filtered = self.tools.filter_where(lambda tool: False)
223+
224+
self.assertIsInstance(filtered, ToolCollection)
225+
self.assertEqual(len(filtered), 0)
226+
filtered = self.tools.filter_where(lambda tool: len(tool.name) > 8)
227+
228+
self.assertIsInstance(filtered, ToolCollection)
229+
self.assertEqual(len(filtered), 2)
230+
self.assertEqual(filtered[0], self.calculator_tool)
231+
self.assertEqual(filtered[1], self.translator_tool)

0 commit comments

Comments
 (0)