From 0e525b0db0cfcfb6d2a073470abe5352a58982f1 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Thu, 13 Nov 2025 17:24:59 +0000 Subject: [PATCH] Add server filtering to agent_tools() method - 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 --- README.md | 9 ++++ src/mcpd/mcpd_client.py | 23 +++++++++- tests/unit/test_mcpd_client.py | 81 ++++++++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 558266d..aa76292 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,15 @@ from mcpd import McpdClient # Assumes the mcpd daemon is running client = McpdClient(api_endpoint="http://localhost:8090") +# Get all tools from all servers +all_tools = client.agent_tools() + +# Get tools from specific servers only +time_tools = client.agent_tools(servers=['time']) + +# Get tools from multiple servers +subset_tools = client.agent_tools(servers=['time', 'fetch']) + agent_config = AgentConfig( tools=client.agent_tools(), model_id="gpt-4.1-nano", # Requires OPENAI_API_KEY to be set diff --git a/src/mcpd/mcpd_client.py b/src/mcpd/mcpd_client.py index d919f36..2795483 100644 --- a/src/mcpd/mcpd_client.py +++ b/src/mcpd/mcpd_client.py @@ -367,7 +367,7 @@ def _get_tool_definitions(self, server_name: str) -> list[dict[str, Any]]: except requests.exceptions.RequestException as e: raise McpdError(f"Error listing tool definitions for server '{server_name}': {e}") from e - def agent_tools(self) -> list[Callable[..., Any]]: + def agent_tools(self, servers: list[str] | None = None) -> list[Callable[..., Any]]: """Generate callable Python functions for all available tools, suitable for AI agents. This method queries all servers via `tools()` and creates self-contained, @@ -378,6 +378,12 @@ def agent_tools(self) -> list[Callable[..., Any]]: The generated functions are cached for performance. Use clear_agent_tools_cache() to force regeneration if servers or tools have changed. + Args: + servers: Optional list of server names to filter by. + If None, returns tools from all servers. + If specified, only tools from the listed servers are included. + Non-existent server names are silently ignored. + Returns: A list of callable functions, one for each tool across all servers. Each function has the following attributes: @@ -404,6 +410,10 @@ def agent_tools(self) -> list[Callable[..., Any]]: >>> tools = client.agent_tools() >>> print(f"Generated {len(tools)} callable tools") >>> + >>> # Get tools from specific servers only + >>> time_tools = client.agent_tools(servers=['time']) + >>> subset_tools = client.agent_tools(servers=['time', 'fetch']) + >>> >>> # Use with an AI agent framework >>> agent_config = AgentConfig( ... tools=tools, @@ -423,7 +433,16 @@ def agent_tools(self) -> list[Callable[..., Any]]: agent_tools = [] all_tools = self.tools() - for server_name, tool_schemas in all_tools.items(): + # Determine which servers to use. + servers_to_use = all_tools.keys() if servers is None else servers + + # Fetch tools from selected servers. + for server_name in servers_to_use: + if server_name not in all_tools: + # Server doesn't exist or has no tools - skip silently. + continue + + tool_schemas = all_tools[server_name] for tool_schema in tool_schemas: func = self._function_builder.create_function_from_schema(tool_schema, server_name) agent_tools.append(func) diff --git a/tests/unit/test_mcpd_client.py b/tests/unit/test_mcpd_client.py index 6cff985..8d819a1 100644 --- a/tests/unit/test_mcpd_client.py +++ b/tests/unit/test_mcpd_client.py @@ -143,6 +143,87 @@ def test_agent_tools(self, mock_tools, client): assert result == [mock_func1, mock_func2] assert mock_create.call_count == 2 + @patch.object(McpdClient, "tools") + def test_agent_tools_filter_by_single_server(self, mock_tools, client): + """Test filtering tools by a single server name.""" + mock_tools.return_value = { + "server1": [{"name": "tool1", "description": "Test tool"}], + "server2": [{"name": "tool2", "description": "Another tool"}], + } + + with patch.object(client._function_builder, "create_function_from_schema") as mock_create: + mock_func1 = Mock() + mock_create.return_value = mock_func1 + + result = client.agent_tools(servers=["server1"]) + + assert result == [mock_func1] + assert mock_create.call_count == 1 + mock_create.assert_called_once_with({"name": "tool1", "description": "Test tool"}, "server1") + + @patch.object(McpdClient, "tools") + def test_agent_tools_filter_by_multiple_servers(self, mock_tools, client): + """Test filtering tools by multiple server names.""" + mock_tools.return_value = { + "server1": [{"name": "tool1", "description": "Test tool"}], + "server2": [{"name": "tool2", "description": "Another tool"}], + "server3": [{"name": "tool3", "description": "Third tool"}], + } + + with patch.object(client._function_builder, "create_function_from_schema") as mock_create: + mock_func1 = Mock() + mock_func2 = Mock() + mock_create.side_effect = [mock_func1, mock_func2] + + result = client.agent_tools(servers=["server1", "server2"]) + + assert result == [mock_func1, mock_func2] + assert mock_create.call_count == 2 + + @patch.object(McpdClient, "tools") + def test_agent_tools_with_nonexistent_server(self, mock_tools, client): + """Test filtering with server that doesn't exist.""" + mock_tools.return_value = { + "server1": [{"name": "tool1", "description": "Test tool"}], + } + + with patch.object(client._function_builder, "create_function_from_schema") as mock_create: + result = client.agent_tools(servers=["nonexistent"]) + + assert result == [] + assert mock_create.call_count == 0 + + @patch.object(McpdClient, "tools") + def test_agent_tools_with_empty_servers_list(self, mock_tools, client): + """Test filtering with empty server list.""" + mock_tools.return_value = { + "server1": [{"name": "tool1", "description": "Test tool"}], + } + + with patch.object(client._function_builder, "create_function_from_schema") as mock_create: + result = client.agent_tools(servers=[]) + + assert result == [] + assert mock_create.call_count == 0 + + @patch.object(McpdClient, "tools") + def test_agent_tools_without_servers_parameter(self, mock_tools, client): + """Test existing behavior - returns all tools when servers parameter not provided.""" + mock_tools.return_value = { + "server1": [{"name": "tool1", "description": "Test tool"}], + "server2": [{"name": "tool2", "description": "Another tool"}], + } + + with patch.object(client._function_builder, "create_function_from_schema") as mock_create: + mock_func1 = Mock() + mock_func2 = Mock() + mock_create.side_effect = [mock_func1, mock_func2] + + result = client.agent_tools() + + assert result == [mock_func1, mock_func2] + assert mock_create.call_count == 2 + @patch.object(McpdClient, "tools") def test_has_tool_exists(self, mock_tools, client): mock_tools.return_value = [{"name": "existing_tool"}, {"name": "another_tool"}]