Skip to content

Commit a5c80c6

Browse files
committed
remove max_tools, add kwargs
1 parent f657be0 commit a5c80c6

File tree

6 files changed

+59
-63
lines changed

6 files changed

+59
-63
lines changed

src/strands/agent/agent.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,9 @@ def cleanup(self) -> None:
524524
Note: This method uses a "belt and braces" approach with automatic cleanup
525525
through __del__ as a fallback, but explicit cleanup is recommended.
526526
"""
527+
if self._cleanup_called:
528+
return
529+
527530
run_async(self.cleanup_async)
528531

529532
async def cleanup_async(self) -> None:

src/strands/experimental/tools/mcp/mcp_tool_provider.py

Lines changed: 20 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""MCP Tool Provider implementation."""
22

33
import logging
4-
from typing import Callable, Optional, Pattern, Sequence, Union
4+
from typing import Any, Callable, Optional, Pattern, Sequence, Union
55

66
from typing_extensions import TypedDict
77

@@ -23,37 +23,39 @@ class ToolFilters(TypedDict, total=False):
2323
Tools are filtered in this order:
2424
1. If 'allowed' is specified, only tools matching these patterns are included
2525
2. Tools matching 'rejected' patterns are then excluded
26-
3. If the result exceeds 'max_tools', it's truncated
2726
"""
2827

2928
allowed: list[_ToolFilterPattern]
3029
rejected: list[_ToolFilterPattern]
31-
max_tools: int
3230

3331

3432
class MCPToolProvider(ToolProvider):
3533
"""Tool provider for MCP clients with managed lifecycle."""
3634

3735
def __init__(
38-
self, *, client: MCPClient, tool_filters: Optional[ToolFilters] = None, disambiguator: Optional[str] = None
36+
self,
37+
*,
38+
client: MCPClient,
39+
tool_filters: Optional[ToolFilters] = None,
40+
prefix: Optional[str] = None,
41+
**kwargs: Any,
3942
) -> None:
4043
"""Initialize with an MCP client.
4144
4245
Args:
4346
client: The MCP client to manage.
4447
tool_filters: Optional filters to apply to tools.
45-
disambiguator: Optional prefix for tool names.
48+
prefix: Optional prefix for tool names.
49+
**kwargs: Additional arguments for future compatibility.
4650
"""
47-
logger.debug(
48-
"tool_filters=<%s>, disambiguator=<%s> | initializing MCPToolProvider", tool_filters, disambiguator
49-
)
51+
logger.debug("tool_filters=<%s>, prefix=<%s> | initializing MCPToolProvider", tool_filters, prefix)
5052
self._client = client
5153
self._tool_filters = tool_filters
52-
self._disambiguator = disambiguator
54+
self._prefix = prefix
5355
self._tools: Optional[list[MCPAgentTool]] = None # None = not loaded yet, [] = loaded but empty
5456
self._started = False
5557

56-
async def load_tools(self) -> Sequence[AgentTool]:
58+
async def load_tools(self, **kwargs: Any) -> Sequence[AgentTool]:
5759
"""Load and return tools from the MCP client.
5860
5961
Returns:
@@ -77,12 +79,6 @@ async def load_tools(self) -> Sequence[AgentTool]:
7779
pagination_token = None
7880
page_count = 0
7981

80-
# Determine max_tools limit for early termination
81-
max_tools_limit = None
82-
if self._tool_filters and "max_tools" in self._tool_filters:
83-
max_tools_limit = self._tool_filters["max_tools"]
84-
logger.debug("max_tools_limit=<%d> | will stop when reached", max_tools_limit)
85-
8682
while True:
8783
logger.debug("page=<%d>, token=<%s> | fetching tools page", page_count, pagination_token)
8884
paginated_tools = self._client.list_tools_sync(pagination_token)
@@ -91,15 +87,10 @@ async def load_tools(self) -> Sequence[AgentTool]:
9187
for tool in paginated_tools:
9288
# Apply filters
9389
if self._should_include_tool(tool):
94-
# Apply disambiguation if needed
95-
processed_tool = self._apply_disambiguation(tool)
90+
# Apply prefix if needed
91+
processed_tool = self._apply_prefix(tool)
9692
self._tools.append(processed_tool)
9793

98-
# Check if we've reached max_tools limit
99-
if max_tools_limit is not None and len(self._tools) >= max_tools_limit:
100-
logger.debug("max_tools_reached=<%d> | stopping pagination early", len(self._tools))
101-
return self._tools
102-
10394
logger.debug(
10495
"page=<%d>, page_tools=<%d>, total_filtered=<%d> | processed page",
10596
page_count,
@@ -134,14 +125,14 @@ def _should_include_tool(self, tool: MCPAgentTool) -> bool:
134125

135126
return True
136127

137-
def _apply_disambiguation(self, tool: MCPAgentTool) -> MCPAgentTool:
138-
"""Apply disambiguation to a single tool if needed."""
139-
if not self._disambiguator:
128+
def _apply_prefix(self, tool: MCPAgentTool) -> MCPAgentTool:
129+
"""Apply prefix to a single tool if needed."""
130+
if not self._prefix:
140131
return tool
141132

142-
# Create new tool with disambiguated agent name but preserve original MCP name
133+
# Create new tool with prefixed agent name but preserve original MCP name
143134
old_name = tool.tool_name
144-
new_agent_name = f"{self._disambiguator}_{tool.mcp_tool.name}"
135+
new_agent_name = f"{self._prefix}_{tool.mcp_tool.name}"
145136
new_tool = MCPAgentTool(tool.mcp_tool, tool.mcp_client, agent_facing_tool_name=new_agent_name)
146137
logger.debug("tool_rename=<%s->%s> | renamed tool", old_name, new_agent_name)
147138
return new_tool
@@ -160,7 +151,7 @@ def _matches_patterns(self, tool: MCPAgentTool, patterns: list[_ToolFilterPatter
160151
return True
161152
return False
162153

163-
async def cleanup(self) -> None:
154+
async def cleanup(self, **kwargs: Any) -> None:
164155
"""Clean up the MCP client connection."""
165156
if not self._started:
166157
return

src/strands/experimental/tools/tool_provider.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Tool provider interface."""
22

33
from abc import ABC, abstractmethod
4-
from typing import TYPE_CHECKING, Sequence
4+
from typing import TYPE_CHECKING, Any, Sequence
55

66
if TYPE_CHECKING:
77
from ...types.tools import AgentTool
@@ -15,18 +15,24 @@ class ToolProvider(ABC):
1515
"""
1616

1717
@abstractmethod
18-
async def load_tools(self) -> Sequence["AgentTool"]:
18+
async def load_tools(self, **kwargs: Any) -> Sequence["AgentTool"]:
1919
"""Load and return the tools in this provider.
2020
21+
Args:
22+
**kwargs: Additional arguments for future compatibility.
23+
2124
Returns:
2225
List of tools that are ready to use.
2326
"""
2427
pass
2528

2629
@abstractmethod
27-
async def cleanup(self) -> None:
30+
async def cleanup(self, **kwargs: Any) -> None:
2831
"""Clean up resources used by the tools in this provider.
2932
33+
Args:
34+
**kwargs: Additional arguments for future compatibility.
35+
3036
Should be called when the tools are no longer needed.
3137
"""
3238
pass

tests/strands/agent/test_agent.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1012,6 +1012,18 @@ def test_agent_cleanup_idempotent(agent):
10121012
mock_provider.cleanup.assert_called_once()
10131013

10141014

1015+
def test_agent_cleanup_early_return_avoids_thread_spawn(agent):
1016+
"""Test that cleanup returns early when already called, avoiding thread spawn cost."""
1017+
# Mark cleanup as already called
1018+
agent._cleanup_called = True
1019+
1020+
with unittest.mock.patch("strands.agent.agent.run_async") as mock_run_async:
1021+
agent.cleanup()
1022+
1023+
# Verify run_async was not called since cleanup already happened
1024+
mock_run_async.assert_not_called()
1025+
1026+
10151027
def test_agent__del__emits_warning_for_automatic_cleanup(agent):
10161028
"""Test that __del__ emits warning when cleanup wasn't called manually."""
10171029
# Add a mock tool provider so cleanup will be called

tests/strands/experimental/tools/mcp/test_mcp_tool_provider.py

Lines changed: 8 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -55,21 +55,21 @@ def test_init_with_client_only(mock_mcp_client):
5555

5656
assert provider._client is mock_mcp_client
5757
assert provider._tool_filters is None
58-
assert provider._disambiguator is None
58+
assert provider._prefix is None
5959
assert provider._tools is None
6060
assert provider._started is False
6161

6262

6363
def test_init_with_all_parameters(mock_mcp_client):
6464
"""Test initialization with all parameters."""
65-
filters = {"allowed": ["tool1"], "max_tools": 5}
66-
disambiguator = "test_prefix"
65+
filters = {"allowed": ["tool1"]}
66+
prefix = "test_prefix"
6767

68-
provider = MCPToolProvider(client=mock_mcp_client, tool_filters=filters, disambiguator=disambiguator)
68+
provider = MCPToolProvider(client=mock_mcp_client, tool_filters=filters, prefix=prefix)
6969

7070
assert provider._client is mock_mcp_client
7171
assert provider._tool_filters == filters
72-
assert provider._disambiguator == disambiguator
72+
assert provider._prefix == prefix
7373
assert provider._tools is None
7474
assert provider._started is False
7575

@@ -232,24 +232,8 @@ async def test_rejected_filter(mock_mcp_client):
232232

233233

234234
@pytest.mark.asyncio
235-
async def test_max_tools_filter(mock_mcp_client):
236-
"""Test max_tools filter functionality."""
237-
tools_list = [create_mock_tool(f"tool_{i}") for i in range(5)]
238-
239-
mock_mcp_client.list_tools_sync.return_value = PaginatedList(tools_list)
240-
241-
filters: ToolFilters = {"max_tools": 3}
242-
provider = MCPToolProvider(client=mock_mcp_client, tool_filters=filters)
243-
244-
tools = await provider.load_tools()
245-
246-
assert len(tools) == 3
247-
assert all(tool.tool_name.startswith("tool_") for tool in tools)
248-
249-
250-
@pytest.mark.asyncio
251-
async def test_disambiguator_renames_tools(mock_mcp_client):
252-
"""Test that disambiguator properly renames tools."""
235+
async def test_prefix_renames_tools(mock_mcp_client):
236+
"""Test that prefix properly renames tools."""
253237
original_tool = MagicMock(spec=MCPAgentTool)
254238
original_tool.tool_name = "original_name"
255239
original_tool.mcp_tool = MagicMock()
@@ -263,7 +247,7 @@ async def test_disambiguator_renames_tools(mock_mcp_client):
263247
new_tool.tool_name = "prefix_original_name"
264248
mock_agent_tool_class.return_value = new_tool
265249

266-
provider = MCPToolProvider(client=mock_mcp_client, disambiguator="prefix")
250+
provider = MCPToolProvider(client=mock_mcp_client, prefix="prefix")
267251

268252
tools = await provider.load_tools()
269253

tests_integ/test_mcp_tool_provider.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def test_mcp_tool_provider_filters():
2222
lambda: stdio_client(StdioServerParameters(command="python", args=["tests_integ/echo_server.py"]))
2323
)
2424

25-
# Test string filter, regex filter, callable filter, max_tools, and disambiguator
25+
# Test string filter, regex filter, callable filter, and prefix
2626
def short_names_only(tool) -> bool:
2727
return len(tool.tool_name) <= 20 # Allow most tools
2828

@@ -32,7 +32,7 @@ def short_names_only(tool) -> bool:
3232
"max_tools": 2,
3333
}
3434

35-
provider = MCPToolProvider(client=stdio_mcp_client, tool_filters=filters, disambiguator="test")
35+
provider = MCPToolProvider(client=stdio_mcp_client, tool_filters=filters, prefix="test")
3636
agent = Agent(tools=[provider])
3737
tool_names = agent.tool_names
3838

@@ -51,7 +51,7 @@ def test_mcp_tool_provider_execution():
5151
)
5252

5353
filters: ToolFilters = {"allowed": ["echo"]}
54-
provider = MCPToolProvider(client=stdio_mcp_client, tool_filters=filters, disambiguator="filtered")
54+
provider = MCPToolProvider(client=stdio_mcp_client, tool_filters=filters, prefix="filtered")
5555
agent = Agent(
5656
tools=[provider],
5757
)
@@ -80,7 +80,7 @@ def test_mcp_tool_provider_reuse():
8080
)
8181

8282
filters: ToolFilters = {"allowed": ["echo"]}
83-
provider = MCPToolProvider(client=stdio_mcp_client, tool_filters=filters, disambiguator="shared")
83+
provider = MCPToolProvider(client=stdio_mcp_client, tool_filters=filters, prefix="shared")
8484

8585
# Create first agent with the provider
8686
agent1 = Agent(tools=[provider])
@@ -116,11 +116,11 @@ def test_mcp_tool_provider_multiple_servers():
116116
lambda: stdio_client(StdioServerParameters(command="python", args=["tests_integ/echo_server.py"]))
117117
)
118118

119-
# Create providers with different disambiguators
120-
provider1 = MCPToolProvider(client=client1, tool_filters={"allowed": ["echo"]}, disambiguator="server1")
119+
# Create providers with different prefixes
120+
provider1 = MCPToolProvider(client=client1, tool_filters={"allowed": ["echo"]}, prefix="server1")
121121
# Use correct tool name from echo_server.py
122122
provider2 = MCPToolProvider(
123-
client=client2, tool_filters={"allowed": ["echo_with_structured_content"]}, disambiguator="server2"
123+
client=client2, tool_filters={"allowed": ["echo_with_structured_content"]}, prefix="server2"
124124
)
125125

126126
# Create agent with both providers

0 commit comments

Comments
 (0)