Skip to content

Commit afcb2b9

Browse files
authored
Add server filtering to agent_tools() method (#31)
- Add optional servers parameter to filter tools by server name - Support single server, multiple servers, or all servers (default) - Silently skip non-existent servers for graceful handling - Add comprehensive unit tests for all filtering scenarios - Update README with usage examples - Maintains full backward compatibility
1 parent b48657b commit afcb2b9

File tree

3 files changed

+111
-2
lines changed

3 files changed

+111
-2
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,15 @@ from mcpd import McpdClient
9090
# Assumes the mcpd daemon is running
9191
client = McpdClient(api_endpoint="http://localhost:8090")
9292

93+
# Get all tools from all servers
94+
all_tools = client.agent_tools()
95+
96+
# Get tools from specific servers only
97+
time_tools = client.agent_tools(servers=['time'])
98+
99+
# Get tools from multiple servers
100+
subset_tools = client.agent_tools(servers=['time', 'fetch'])
101+
93102
agent_config = AgentConfig(
94103
tools=client.agent_tools(),
95104
model_id="gpt-4.1-nano", # Requires OPENAI_API_KEY to be set

src/mcpd/mcpd_client.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,7 @@ def _get_tool_definitions(self, server_name: str) -> list[dict[str, Any]]:
367367
except requests.exceptions.RequestException as e:
368368
raise McpdError(f"Error listing tool definitions for server '{server_name}': {e}") from e
369369

370-
def agent_tools(self) -> list[Callable[..., Any]]:
370+
def agent_tools(self, servers: list[str] | None = None) -> list[Callable[..., Any]]:
371371
"""Generate callable Python functions for all available tools, suitable for AI agents.
372372
373373
This method queries all servers via `tools()` and creates self-contained,
@@ -378,6 +378,12 @@ def agent_tools(self) -> list[Callable[..., Any]]:
378378
The generated functions are cached for performance. Use clear_agent_tools_cache()
379379
to force regeneration if servers or tools have changed.
380380
381+
Args:
382+
servers: Optional list of server names to filter by.
383+
If None, returns tools from all servers.
384+
If specified, only tools from the listed servers are included.
385+
Non-existent server names are silently ignored.
386+
381387
Returns:
382388
A list of callable functions, one for each tool across all servers.
383389
Each function has the following attributes:
@@ -404,6 +410,10 @@ def agent_tools(self) -> list[Callable[..., Any]]:
404410
>>> tools = client.agent_tools()
405411
>>> print(f"Generated {len(tools)} callable tools")
406412
>>>
413+
>>> # Get tools from specific servers only
414+
>>> time_tools = client.agent_tools(servers=['time'])
415+
>>> subset_tools = client.agent_tools(servers=['time', 'fetch'])
416+
>>>
407417
>>> # Use with an AI agent framework
408418
>>> agent_config = AgentConfig(
409419
... tools=tools,
@@ -423,7 +433,16 @@ def agent_tools(self) -> list[Callable[..., Any]]:
423433
agent_tools = []
424434
all_tools = self.tools()
425435

426-
for server_name, tool_schemas in all_tools.items():
436+
# Determine which servers to use.
437+
servers_to_use = all_tools.keys() if servers is None else servers
438+
439+
# Fetch tools from selected servers.
440+
for server_name in servers_to_use:
441+
if server_name not in all_tools:
442+
# Server doesn't exist or has no tools - skip silently.
443+
continue
444+
445+
tool_schemas = all_tools[server_name]
427446
for tool_schema in tool_schemas:
428447
func = self._function_builder.create_function_from_schema(tool_schema, server_name)
429448
agent_tools.append(func)

tests/unit/test_mcpd_client.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,87 @@ def test_agent_tools(self, mock_tools, client):
143143
assert result == [mock_func1, mock_func2]
144144
assert mock_create.call_count == 2
145145

146+
@patch.object(McpdClient, "tools")
147+
def test_agent_tools_filter_by_single_server(self, mock_tools, client):
148+
"""Test filtering tools by a single server name."""
149+
mock_tools.return_value = {
150+
"server1": [{"name": "tool1", "description": "Test tool"}],
151+
"server2": [{"name": "tool2", "description": "Another tool"}],
152+
}
153+
154+
with patch.object(client._function_builder, "create_function_from_schema") as mock_create:
155+
mock_func1 = Mock()
156+
mock_create.return_value = mock_func1
157+
158+
result = client.agent_tools(servers=["server1"])
159+
160+
assert result == [mock_func1]
161+
assert mock_create.call_count == 1
162+
mock_create.assert_called_once_with({"name": "tool1", "description": "Test tool"}, "server1")
163+
164+
@patch.object(McpdClient, "tools")
165+
def test_agent_tools_filter_by_multiple_servers(self, mock_tools, client):
166+
"""Test filtering tools by multiple server names."""
167+
mock_tools.return_value = {
168+
"server1": [{"name": "tool1", "description": "Test tool"}],
169+
"server2": [{"name": "tool2", "description": "Another tool"}],
170+
"server3": [{"name": "tool3", "description": "Third tool"}],
171+
}
172+
173+
with patch.object(client._function_builder, "create_function_from_schema") as mock_create:
174+
mock_func1 = Mock()
175+
mock_func2 = Mock()
176+
mock_create.side_effect = [mock_func1, mock_func2]
177+
178+
result = client.agent_tools(servers=["server1", "server2"])
179+
180+
assert result == [mock_func1, mock_func2]
181+
assert mock_create.call_count == 2
182+
183+
@patch.object(McpdClient, "tools")
184+
def test_agent_tools_with_nonexistent_server(self, mock_tools, client):
185+
"""Test filtering with server that doesn't exist."""
186+
mock_tools.return_value = {
187+
"server1": [{"name": "tool1", "description": "Test tool"}],
188+
}
189+
190+
with patch.object(client._function_builder, "create_function_from_schema") as mock_create:
191+
result = client.agent_tools(servers=["nonexistent"])
192+
193+
assert result == []
194+
assert mock_create.call_count == 0
195+
196+
@patch.object(McpdClient, "tools")
197+
def test_agent_tools_with_empty_servers_list(self, mock_tools, client):
198+
"""Test filtering with empty server list."""
199+
mock_tools.return_value = {
200+
"server1": [{"name": "tool1", "description": "Test tool"}],
201+
}
202+
203+
with patch.object(client._function_builder, "create_function_from_schema") as mock_create:
204+
result = client.agent_tools(servers=[])
205+
206+
assert result == []
207+
assert mock_create.call_count == 0
208+
209+
@patch.object(McpdClient, "tools")
210+
def test_agent_tools_without_servers_parameter(self, mock_tools, client):
211+
"""Test existing behavior - returns all tools when servers parameter not provided."""
212+
mock_tools.return_value = {
213+
"server1": [{"name": "tool1", "description": "Test tool"}],
214+
"server2": [{"name": "tool2", "description": "Another tool"}],
215+
}
216+
217+
with patch.object(client._function_builder, "create_function_from_schema") as mock_create:
218+
mock_func1 = Mock()
219+
mock_func2 = Mock()
220+
mock_create.side_effect = [mock_func1, mock_func2]
221+
222+
result = client.agent_tools()
223+
224+
assert result == [mock_func1, mock_func2]
225+
assert mock_create.call_count == 2
226+
146227
@patch.object(McpdClient, "tools")
147228
def test_has_tool_exists(self, mock_tools, client):
148229
mock_tools.return_value = [{"name": "existing_tool"}, {"name": "another_tool"}]

0 commit comments

Comments
 (0)